İplikler

İşletim Sistemleri
İşletim Sistemleri
Dr. Binnur Kurt
[email protected]
Omega Eğitim ve Danışmanlık
http://www.omegaegitim.com
Bölüm 4
İplikler
1|Sayfa
İşletim Sistemleri
İÇİNDEKİLER
1. İşletim Sistemi
2. Kabuk
3. Prosesler
4. İplikler
5. Prosesler Arası İletişim
6. İş Sıralama
7. Ölümcül Kilitlenme
8. Çok İplikli Programlama
9. Bellek Yönetimi
10. Dosya Sistemi
11. Soket Haberleşme
Bölüm 4
İplikler
2|Sayfa
İşletim Sistemleri
BÖLÜM 4
İplikler
Bölümün Amacı
Bölüm sonunda aşağıdaki konular öğrenilmiş olacaktır:
► İplik Modeli
► Linux’da PTHREAD Kütüphanesi kullanarak çok iplikli uygulama geliştirmek
► C++11’de çok iplikli uygulama geliştirmek
► Java’da çok iplikli uygulama geliştirmek
Bölüm 4
İplikler
3|Sayfa
İşletim Sistemleri
4.1 Giriş
İplikler de prosesler gibi birden fazla görevi yerine getirmek için kullanılır. İplikler
de prosesler gibi işlemciyi zamanda paylaşarak kullanılırlar. Eğer birden fazla işlemci
ya da çekirdek varsa çekirdek sayısı kadar iplik ya da proses gerçekten paralel olarak
çalışabilir. İplikler proseslere göre daha hafif sıklet bir çözüm sunar. Yeni bir iplik
yaratıldığında sadece yığın için yer ayrılır. Ait olduğu prosesin heap ve text alanlarını
diğer ipliklerle beraber paylaşır. İplikler ile veri ve görev paralelliği daha ince ölçekte
gerçeklenebilir.
Proses fork sistem çağrısı kullanılarak yaratılır:
#include <unistd.h>
pid_t fork(void);
Çağrıyı yapan prosesin bellek görüntüsü birebir kopyalanarak çocuk proses
yaratılır. Bu yüzden fork ile proses yaratmak maliyetlidir. Eğer çocuk proses
ebeveyn prosesin bellek alanında bir değişiklik yapmayacaksa, Linux işletim
sisteminde, fork sistem çağrısına göre daha hızlı çalışan vfork sistem çağrısını
kullanmak gerekir:
#include <unistd.h>
pid_t vfork(void);
vfork paylaşılan bellek alanına yazma yapıldığında kopyalama yapar. Bu en iyileme
literatürde copy-on-write olarak adlandırılmaktadır. Özellikle çocuk proses
exec sistem çağrılarından birini kullanacaksa, fork yerine vfork kullanılmalıdır.
İplik yaratmak için ise clone sistem çağrısı kullanılır:
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,int flags,
void *arg,...);
Aşağıda clone sistem çağrısı kullanılarak yaratılmış bir ipliğin yer aldığı örnek bir
uygulama bulunmaktadır:
Kod 4.1:
#include
#include
#include
#include
<unistd.h>
<stdio.h>
<stdlib.h>
<sched.h>
#define CHILD_STACK_SIZE 16384
int var;
int do_something(void *) {
printf("I am in thread. Assigning 42 to the variable.\n");
var = 42;
}
int main(int argc, char *argv[]) {
void **child_stack;
var = 9;
child_stack = (void **) malloc(CHILD_STACK_SIZE);
printf("I am in the process! The variable was %d\n", var);
clone(do_something,
child_stack+CHILD_STACK_SIZE/sizeof(void**),
Bölüm 4
İplikler
4|Sayfa
İşletim Sistemleri
CLONE_SIGHAND|CLONE_FS|CLONE_VM|CLONE_FILES,
NULL);
sleep(1);
printf("I am in the process. The variable is %d\n", var);
return 0;
}
CLONE_VM sabiti ipliğin prosesin bellek uzayını paylaşmasına neden olur. var
değişkenine P prosesi tarafından CLONE_VM seçeneği ile yaratılan tüm iplikler
erişebilir (Şekil-4.1).
P
t1
t2
var
t3
Şekil-4.1 Proses ve ipliklerin bellek erişimleri
3.2 PTHREAD Kütüphanesini Kullanarak İplik Yaratmak
clone gibi sistem çağrılarını kullanarak iplik programlamak güçtür. İplik
programlamayı bu nedenle POSIX Thread Kütüphanesi gibi bir üst düzey API
kullanarak gerçekleştiriyoruz. Yaratılan her ipliğe o ipliğin çalıştırmasını istediğimiz
fonksiyonun adresini ve parametresini veriyoruz.
PThread Kütüphanesi kullanarak iplik yaratmak için pthread_create
fonksiyonunu kullanıyoruz:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void*(*start_routine)(void*),
void *arg)
Burada birinci parametre ipliğin kimliğini saklamak istediğimiz bellek gözünün
adresini ve ikinci parametre ise yaratmak istediğimiz ipliğin özelliklerini almaktadır.
Üçüncü parametre ile ipliğin çalıştırmasını istediğimiz fonksiyonun adresini ve
dördüncü parametre ile bu fonksiyona geçirmek istediğimiz parametrenin değerini
veriyoruz. PTHREAD kütüphanesindeki tüm fonksiyonlar ve veri yapıları pthread_
ön eki ile başlar.
Aşağıdaki uygulamada (Kod-4.2) 5 adet iplik yaratılmaktadır. İplikler çalışmaya
başladıktan sonra ekrana bir ileti gönderip uyutulmaktadır. Ebeveyn proses ise bu
beş ipliğin işini bitirmesini beklemek üzere pthread_join çağrısı yapmaktadır.
Yaratılan her ipliğe ulaşmak üzere pthread_t tipinde bir veri yapısı iliştirilir. Tüm
PTHREAD kütüphane fonksiyonlarda iplik üzerinde işlem yapmak amacı ile referans
olarak bu değer kullanılır.
Bölüm 4
İplikler
5|Sayfa
İşletim Sistemleri
Yaratılan ipliğin yürüteceği fonksiyon herhangi bir tipten değer alabilir ve benzer
şekilde herhangi bir tipten değer döndürebilir. Parametre aktarımı
pthread_create çağrısı ile iplik yaratılırken gerçekleşir:
pthread_create(
&tid[i],
NULL,
sleeping,
(void *)(SLEEP_TIME+2*i)
);
İpliğin döndürdüğü değere ise pthread_join çağrısı üzerinden erişilir:
pthread_join( tid[i], (void **)&return_value);
Kod 4.2:
#include
#include
#include
#include
#include
<pthread.h>
<stdio.h>
<stdlib.h>
<unistd.h>
<iostream>
using namespace std;
#define NUM_THREADS 5
#define SLEEP_TIME 10
pthread_t tid[NUM_THREADS];
void* sleeping(void *);
void thread_start() {
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create( &tid[i],
NULL,
sleeping,
(void *)(SLEEP_TIME+2*i));
}
}
void thread_wait() {
int return_value;
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join( tid[i], (void **)&return_value);
cout << "Thread " << i << " joins...!" << endl ;
}
}
void* sleeping(void * arg) {
int sleep_time= *((int*)(&arg));
cout << endl << "Sleeping for " << sleep_time
<< " seconds" << endl ;
sleep(sleep_time);
Bölüm 4
İplikler
6|Sayfa
İşletim Sistemleri
return ((void *)sleep_time);
}
int main() {
thread_start();
thread_wait();
return 0;
}
Bazen iplik fonksiyonu birden fazla parametre alması ya da birden fazla değer
döndürmesi gerekebilir. Bu durumda parametre ya da dönüş değeri için struct ile
bir tip tanımlanır. Birden fazla değer struct alanları olarak tanımlanır:
struct problem {
long *begin;
long *end;
problem(long *_begin,long *_end):
begin(_begin), end(_end){
}
};
Aşağıdaki örnekte yüz milyon elemanlı bir dizinin elemanları toplamı, çekirdek sayısı
kadar iplik yaratılarak, paralel olarak çözülmeye çalışılmaktadır:
Kod 4.3:
#include <iostream>
#include <pthread.h>
#include <time.h>
using namespace std;
const long PROBLEM_SIZE=100000000;
const int NUMBER_OF_CORE= pthread_num_processors_np();
struct problem {
long *begin;
long *end;
problem(long *_begin,long *_end):
begin(_begin), end(_end){
}
};
long numbers[PROBLEM_SIZE];
void init_array(long *array,long size){
for (long i=0,*p=array;i<size;++i,++p)
*p=i;
}
void * parallel_sum(void* _problem){
problem* prob= (problem*) _problem;
long sum=0;
for (long *q=prob->begin;q!=prob->end;++q)
{
Bölüm 4
İplikler
7|Sayfa
İşletim Sistemleri
sum += *q;
}
return new long(sum);
}
void serial_sum(long *array,int size){
long sum=0L;
long* end=array+size;
for (long *p=array;p!=end;++p)
sum += *p;
cout << "Sum (serial): " << sum << endl;
}
int main(){
init_array(numbers,PROBLEM_SIZE);
pthread_t threads[NUMBER_OF_CORE];
problem **probs= new problem*[NUMBER_OF_CORE];
long per_thread= PROBLEM_SIZE / NUMBER_OF_CORE;
long* beg= numbers;
long* end= beg + per_thread;
for (int i=0;i<NUMBER_OF_CORE;++i){
probs[i]= new problem(beg,end);
beg = beg + per_thread;
end= end + per_thread;
}
for (int i=0;i<NUMBER_OF_CORE;++i){
pthread_create(
threads+i,
0L,
parallel_sum,
(void*)probs[i]
);
}
long sum=0L;
for (int i=0;i<NUMBER_OF_CORE;++i){
int *result;
pthread_join(threads[i],
(void**)&result);
sum += *result;
delete result;
}
cout << "Sum (parallel): " << sum << endl;
for (int i=0;i<NUMBER_OF_CORE;++i){
delete probs[i];
}
delete[] probs;
serial_sum(numbers,PROBLEM_SIZE);
}
Bölüm 4
İplikler
8|Sayfa
İşletim Sistemleri
3.3 C++11’de İplik Yaratmak
C++, 2011 yılında çıkan yeni sürümü ile günümüz modern programlama dillerinde
bulunan birçok ileri düzey özelliğe kavuşmuştur. Nihayet paralel programlama için
kullandığımız iplikler işletim sistemine bağımlı olmaktan kurtuldu ve dilin bir parçası
haline geldi. C++11’de thread kütüphanesi ile gelen thread adında yeni bir sınıf
yer alır. C++11’de iplik yaratmanın 3 farklı yolu vardır.
1. Fonksiyon kullanımı
thread sınıfının kurucu fonksiyonu ilk parametre olarak çalıştırılacak fonksiyonu alır
(Kod 4.4). Aşağıdaki örnekte t1 ve t2 adında iki iplik yaratılır. Bu iplikler hello
fonksiyonunu çalıştırırlar.
Kod 4.4:
#include <iostream>
#include <thread>
using namespace std;
void hello(){
cout << "Hello Mars!" << endl ;
}
int main(){
thread t1(hello);
thread t2(hello);
t1.join();
t2.join();
cout << "Done." << endl;
}
2. struct Kullanımı
Burada ilk önce, () operatörüne işlev yüklenen bir struct tasarlanır. thread
sınıfının kurucu fonksiyonu ilk parametre olarak tasarlanan bu struct bir nesne alır
(Kod 4.5). Aşağıdaki örnekte t1 ve t2 adında iki iplik yaratılır. Bu iplikler struct
içinde tanımlı void operator()() fonksiyonunu çalıştırırlar.
Kod 4.5:
#include <iostream>
#include <thread>
using namespace std;
struct fun {
void operator()(){
cout << "Hello Mars!" << endl ;
}
};
Bölüm 4
İplikler
9|Sayfa
İşletim Sistemleri
int main(){
thread t1((fun()));
thread t2((fun()));
t1.join();
t2.join();
cout << "Done." << endl;
}
3.  İfadesi Kullanımı
C++11 ile birçok yenilik geldi. Bu yeniliklerden bir diğeri, C++11’de artık fonksiyonel
programlama yapılabiliyor olmamızdır. thread sınıfının kurucu fonksiyonu ilk
parametre olarak artık  ifadesi alabiliyor (Kod 4.6). Aşağıdaki örnekte t1 ve t2
adında iki iplik yaratılır. Bu iplikler run ve gun isimli  ifadelerini çalıştırırlar.
Kod 4.6:
#include <iostream>
#include <thread>
using namespace std;
int main(){
function<void()> run =[](){
cout << "Run forrest run!" << endl;
};
auto gun = [](){
cout << "Hello Mars!" << endl;
};
thread t1(gun);
thread t2(run);
t1.join();
t2.join();
cout << "Done." << endl;
}
İplik fonksiyonuna parametre aktarmak mümkündür. Bunun için basitçe thread
sınıfının yapıcısına çalıştırılacak fonksiyondan sonra sırayla parametre değerleri
geçilir (Kod 4.7): thread t1(f,4) ve thread t2(f,8).
Kod 4.7:
#include <thread>
#include <iostream>
using namespace std;
struct fun {
void operator()(int value){
cout << "value= " << value << endl;
}
Bölüm 4
İplikler
10 | S a y f a
İşletim Sistemleri
} ;
int main(){
fun f;
thread t1(f,4);
thread t2(f,8);
t1.join();
t2.join();
}
Aşağıdaki örnekte yüz milyon elemanlı bir dizinin elemanları toplamı, çekirdek sayısı
kadar iplik yaratılarak, paralel olarak çözülmeye çalışılmaktadır (Kod 4.8). PTHREAD
kullanılarak verilen çözüm ile karşılaştırıldığında C++11’de daha az kod yazılarak
çözüme ulaşıldığı görülebilir. Kodda karşılaştığınız auto, async ve future C++11
ile gelen yenilikler. async ile yarattığımız ipliğin ürettiği sonuca future üzerinden
asenkron bir şekilde erişilebilir. future sınıfının get çağrısını yapan proses, eğer
iplik sonlanmamış ise bloke olmasına neden olur. Bloke olan proses iplik
tamamlandığında kaldığı yerden devam eder. Eğer get çağrısı yapıldığında iplik
sonlanmış ise çağrıyı yapan proses bloke olmadan sonucu ulaşır.
Kodda üretken programlamanın (=Generic Programming) gücü hissediliyor. Aynı testi
std::vector ile kodda neredeyse hiçbir değişiklik yapmadan gerçekleştirebiliriz.
Kod 4.8:
#include
#include
#include
#include
<iostream>
<thread>
<future>
<algorithm>
using namespace std;
const int SIZE=80000000;
int numbers[SIZE];
template <typename iter>
void init_problem(iter beg,iter end){
int i=1;
for (iter p=beg;p!=end;p++,++i)
*p= i;
}
template <typename iter>
int parallel_sum(iter begin, iter end){
long len= distance(begin,end);
if (len <= 10000000){
return accumulate(begin,end,int());
}
iter mid= begin + len /2;
auto handle_left=
async(launch::async,parallel_sum<iter>,begin,mid);
auto handle_right=
async(launch::async,parallel_sum<iter>,mid,end);
Bölüm 4
İplikler
11 | S a y f a
İşletim Sistemleri
return handle_left.get()+
handle_right.get();
}
int main(){
init_problem(numbers,numbers+SIZE);
int sum= parallel_sum(numbers,numbers+SIZE);
cout << "Sum (parallel): " << sum << endl ;
}
3.4 Java 8’de İplik Yaratmak
Java 8 platformu üzerinde veri paralelliğini üç yöntemle gerçeklemek mümkündür.
1. Callable İplikler, Future ve Executor Kullanımı
Java programlama dili ilk çıktığından itibaren ipliklerle programlama için bir çözüm
sunuyor. Runnable arayüzünü gerçekleyen bir sınıf tasarlamakla işe başlıyoruz:
public class HelloRunner implements Runnable {
@Override
public void run(){
System.out.println("Hello Mars!");
}
}
Ardından HelloRunner sınıfından bir nesne yaratıyoruz:
HelloRunner runner= new HelloRunner();
Son olarak Thread sınıfından bir nesne yaratıyoruz ve Thread kurucusuna
runner nesnesini parametre olarak veriyoruz:
Thread t= new Thread(runner);
C++11’deki thread sınıfından farklı olarak, Java’da Thread sınıfından nesne
yaratmak, ipliğin çalışması için yeterli değildir. Son olarak start metodunun
çalıştırılması gerekir:
t.start();
Runnable ipliklerin en kötü yönü run() metodunun herhangi bir parametre
alamaması ve ipliğin bir değer döndürememesidir. Bu yüzden görevi yaratan iplik ile
sonucu üretecek iplik arasında mutlaka bir eş güdüm oluşturmak gerekir. Java SE 5 ile
gelen Callable arayüzü ile artık sonucu call() metodundan dönmek
ve Future arayüzünün get() çağrısı ile bu sonuca asenkron bir şekilde ulaşmak
mümkündür. C++11’in future sınıfı ile Java’nın Future sınıfı aynı işlevi
sunmaktadır. Java SE 5 ile gelen yeniliklerden biri de İplik havuzudur (=Thread Pool).
Farklı havuz türleri ihtiyaca göre seçilebilir: Her zaman sabit sayıda ipliğin yer
aldığı Fixed Thread Pool, Parlamalı çalışma modu için Cached Thread Pool, Yığın işler
için Single Thread Pool, Periyodik işler için Scheduled Thread Pool. Ancak buradaki
tüm yapılar alt düzeydedir. Bu yüzden bu yapı üzerinde uygulama geliştirmek vakit
alır, test etmesi zordur, yarış durumları (=race conditions) ve ölümcül kilitlenme
(=deadlock) gibi durumların iyi düşünülmüş olması gerekir.
Örnek uygulama için kullanılan alan sınıfı Kod 4.9’da verilmiştir. Burada amacımız 60
milyon Programmer nesnesi arasından, 40 yaşın üstünde "en bilgili" Java
programcısını bulmaktır.
Bölüm 4
İplikler
12 | S a y f a
İşletim Sistemleri
Kod 4.9:
public class Programmer implements Serializable {
private
private
private
private
private
. . .
int id;
String name;
String surname;
int age;
ProgrammingLanguage programmingLanguage;
}
public class ProgrammingLanguage implements Serializable {
private String name;
private int level;
. . .
}
Önce seri çözüme bir bakalım. Seri çözümde liste içindeki Programmer sınıfı
nesnelerini teker teker ziyaret edip en deneyimli Java programcısını tek bir iplik
kullanarak bulmaya çalışıyoruz (Kod 4.10).
Kod 4.10:
private static Programmer serialSolve(List<Programmer> list) {
Programmer oldest = null;
for (Programmer programmer : list) {
if (programmer.getAge() > 40 &&
programmer.getProgrammingLanguage()
.getName().equalsIgnoreCase("java")) {
if (oldest == null) {
oldest = programmer;
} else if (programmer
.getProgrammingLanguage()
.getLevel() >
oldest.getProgrammingLanguage()
.getLevel())
{
oldest = programmer;
}
}
}
return oldest;
}
Callable iplik kullanılan çözüm ise Kod 4.11’de verilmiştir.
Kod 4.11:
private static long callableThread(List<Programmer> list) throws
InterruptedException {
Programmer oldest = null;
int numberOfSegments = DATA_SIZE / SERIAL_THRESHOLD;
ExecutorService es = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
List<Callable<Programmer>> callables =
new ArrayList<>(numberOfSegments);
for (int i = 0, j = 0;
Bölüm 4
İplikler
13 | S a y f a
İşletim Sistemleri
i < numberOfSegments;
++i, j += SERIAL_THRESHOLD) {
callables.add(
new ProcessingThread(j, SERIAL_THRESHOLD, list)
);
}
List<Future<Programmer>> partialSolutions =
es.invokeAll(callables);
try {
oldest = partialSolutions.get(0).get();
for (int i = 1; i < partialSolutions.size(); ++i) {
Programmer programmer =
partialSolutions.get(i).get();
if (programmer.getProgrammingLanguage().getLevel() >
oldest.getProgrammingLanguage().getLevel()) {
oldest = programmer;
}
}
} catch (InterruptedException | ExecutionException ex) {
System.err.println(ex.getMessage());
}
es.shutdown();
return oldest;
}
public class ProcessingThread implements Callable<Programmer> {
private final int start;
private final int length;
private final List<Programmer> list;
public ProcessingThread(int start, int length,
List<Programmer> list) {
this.start = start;
this.length = length;
this.list = list;
}
@Override
public Programmer call() throws Exception {
Programmer oldest = list.get(start);
Programmer programmer;
for (int i = start + 1, j = 1; j < length; ++j, ++i) {
programmer = list.get(i);
if (programmer.getAge() > 40 &&
programmer.getProgrammingLanguage()
.getName().equalsIgnoreCase("java")) {
if (programmer.getProgrammingLanguage().getLevel() >
oldest.getProgrammingLanguage().getLevel()) {
oldest = programmer;
}
}
}
return oldest;
}
}
Bölüm 4
İplikler
14 | S a y f a
İşletim Sistemleri
2. Fork/Join Çatısının (=Framework) Kullanımı
Geliştirdiğimiz uygulamaların yüksek başarımla çalışması hepimizin arzu ettiği bir
durum. Ancak bu her zaman ulaşılması kolay bir hedef olmayabilir. Özellikle
günümüzde yazılımdan beklentilerin sürekli arttığı göz önüne alındığında, bu hedefe
ulaşmak daha da zorlaşmaktadır. Başarımı iyileştirmeyi mutlaka yazılım geliştirme
sürecinin (müşteriden isteklerin alınması, sistem çözümleme, detaylı tasarım, mimari
tasarım, gerçekleme, sınama, bakım) bir parçası haline getirmeliyiz. Başarımı en
iyilemek yazılım ortaya çıktıktan sonra yapılan bir etkinlik olmamalı. Java
uygulamalarının başarımının en iyilenmesi ise farklı katmanlarda çalışmayı gerektirir.
Donanım katmanı, İşletim sistemi katmanı, Java Sanal Makinası (JSM) katmanı ve
elbette uygulama katmanı. Burada yer alan her bir katmandaki başarımı artırmak için
ne yazık ki elimizde sihirli bir değnek bulunmuyor. Donanım katmanındaki gelişmeler
elimizi güçlendiriyor: Çok çekirdekli mimariler, büyük bellekli bilgisayar sistemleri,
daha yüksek kapasiteli cep bellekler (L0, L1, L2, L3), katı hal diskler, daha hızlı ara
bağlaşım birimleri, çok çekirdekli yüksek başarımlı ekran kartı işlemcileri. İşletim
sistemleri bu donanım kaynaklarını uygulamalar ve kullanıcılar arasında verimli bir
şekilde dağıtmaktan ve yönetmekten sorumlu çok sayıda servisten oluşuyor: proses
sıralayıcı, sanal bellek yönetimi, giriş/çıkış yönetimi, kullanıcı yönetimi, dosya sistemi,
pencereleme sistemi. Linux (Red Hat, Suse), Unix (Oracle Solaris, IBM AIX, HP-UX),
Microsoft Windows gibi işletim sistemleri bu güncel sistem kaynaklarını iyi
yönettikleri söylenebilir. Yine de genel amaçlı işletim sistemi olmalarından
kaynaklanan bazı darboğazlar yaşıyorlar.
Java uygulamaları doğrudan işletim sistemi üzerinde çalışmazlar. Bir sanal makinaya
ihtiyaç duyarlar: Java Sanal Makinası (JSM). JSM'nin görevi, kabaca, Java
uygulamalarını oluşturan bytecode olarak isimlendirilen makina komutlarını,
üzerinde çalıştığı işletim sisteminin ve işlemcinin anlayacağı ve çalıştırabileceği forma
çevirmektir. Java platformunun en güçlü tarafının JSM olduğu söylenebilir. JSM bu
dönüşümü yaparken devingen en iyileme yapabilmektedir. C/C++'da kod yazdığınızda
ise derleyici ancak durağan en iyileme yapabilir. Durağan en iyileme ise başarımda
kısıtlı bir iyileşme sağlayabilir. JSM içinde başarımı belirlemede önemli işlevi olan iki
bileşen yer alır: Tam Anında Derleme ve Çöp Toplayıcı. Java uygulamaları çalışmaya
başladıklarında yorumlamalı olarak çalışırlar. JSM uygulamayı çalıştırırken kesitini de
alır. Eğer bir iyileştirme görüyorsa sınıf metotlarını bytecode'dan işlemcinin doğrudan
çalıştırabileceği doğal komutlara dönüştürür.
JSM satın alabileceğiniz, üretebileceğiniz bir işlemci tanımlar. Bu işlemcinin bir komut
kümesi, yığın temelli adresleme kipleri, saklayıcı kümesi, yığın göstergesi, program
sayacı, bellek modeli vardır. Çoğu zaman JSM'yi yazılımsal olarak ediniriz. Oracle,
hemen hemen tüm işletim sistemleri için bir JSM yayınlıyor. Bu adresten elinizdeki
işletim sistemi için bir JRE ya da JDK indirebilirsiniz. Eğer amacınız sadece Java
uygulamalarını çalıştırmak ise Java Runtime Environment (JRE) yeterli olacaktır. Eğer
uygulama geliştirmek isterseniz Java Geliştirme Çantasını (Java Development Kit, JDK)
indirmeniz gerekir. Bu yazının yazıldığı tarihte, Java’nın 8 sürümü vardı. Her yeni
sürümde dilde bazı eklentiler, mevcut API'lerde değişiklikler ve yeni API'ler ile
tanışıyoruz. Bu arada yeni sürümlerde bazen JSM'de de değişiklikler olabiliyor. Şu ana
kadar ki yeni sürümlerde Java 1.2 ve Java SE 5'de dilde ciddi sayılabilecek yenilik geldi.
Java 7 ile dilde gelen yenilikler daha çok geliştiricinin kodlama başarımını iyileştiren
Bölüm 4
İplikler
15 | S a y f a
İşletim Sistemleri
türden. Java 7’de, Böl/Katıl çatısı olarak adlandırılan çok çekirdekli sistemlerde
uygulama geliştirmek için yeni bir çözüm bulunuyor. Bu çözüm, Concurrency API ile
gelen yeni bir iş parçası havuzu olan ForkJoinPool ve bu havuza atayacağımız iş
parçacıklarını tanımladığımız RecursiveAction ve RecursiveTask sınıflarını
kullanmaktadır. Fork/Join çatısı 1'e göre kodlaması daha kolay olsa da hala alt düzey
bir API sunmaktadır. Üstelik problemi iki alt parçaya ayırmak ile seri olarak çözümü
arasında genellikle veri boyutu üzerinden verilmesi gereken bir karar vardır.
Fork/Join çatısı kullanılarak elde edilen çözüm ise Kod 4.12’de verilmiştir.
Kod 4.12:
private static long forkJoin(List<Programmer> list) {
long start = System.nanoTime();
ForkJoinPool pool = new ForkJoinPool();
ForkListProcessing flp =
new ForkListProcessing(0, list.size(), list);
Programmer oldest = pool.invoke(flp);
System.err.println("Oldest [FJ]: " + oldest);
long stop = System.nanoTime();
return (stop - start);
}
public class ForkListProcessing extends RecursiveTask<Programmer> {
private
private
private
private
final int start;
final int length;
final List<Programmer> list;
static final int SERIAL_THRESHOLD = 5_000_000;
public ForkListProcessing(int start,
int length,
List<Programmer> list) {
this.start = start;
this.length = length;
this.list = list;
}
@Override
public Programmer compute() {
if (length <= SERIAL_THRESHOLD) {
return serialSolver();
} else {
int startLeft = start;
int lengthLeft = length / 2;
int startRight = start + lengthLeft;
int lengthRight = length - lengthLeft;
RecursiveTask<Programmer> left =
new ForkListProcessing(startLeft, lengthLeft, list);
RecursiveTask<Programmer> right =
new ForkListProcessing(startRight, lengthRight, list);
left.fork();
right.fork();
Programmer leftSolution = left.join();
Programmer rightSolution = right.join();
if (leftSolution.getProgrammingLanguage().getLevel() >=
Bölüm 4
İplikler
16 | S a y f a
İşletim Sistemleri
rightSolution.getProgrammingLanguage().getLevel()) {
return leftSolution;
} else {
return rightSolution;
}
}
}
private Programmer serialSolver() {
Programmer oldest = list.get(start);
Programmer programmer;
for (int i = start + 1, j = 1; j < length; ++j, ++i) {
programmer = list.get(i);
if (programmer.getAge() > 40 &&
programmer.getProgrammingLanguage()
.getName().equalsIgnoreCase("java")) {
if (programmer.getProgrammingLanguage().getLevel() >
oldest.getProgrammingLanguage().getLevel()) {
oldest = programmer;
}
}
}
return oldest;
}
}
3. Dönüştür-İndirge (=Map-Reduce) Çatısının Kullanımı
Dönüştür-İndirge çatısı Java SE 8 ile gelmiştir ve alt tarafta Fork/Join çatısını kullanır.
Collection API ile hazır olarak gelen paralel kaplar ve Dönüştür-İndirge çerçevesi
kullanılarak, çekirdek sayısına göre ölçeklenebilir çözümlere hızlıca ulaşabilir.
Dönüştür-İndirge çerçevesi kullanılarak elde edilen çözüm ise Kod 4.13’de verilmiştir.
Koddan görüldüğü gibi Java 8 soyutlama düzeyini en üst düzeye çıkarıyor. Hiç iplik
kullanmadan aynı problemi paralel olarak çözmeyi başardık. Bunu sağlayan Java 8 ile
birlikte gelen Stream API’sidir. Stream bir iş hattı gibi düşünülebilir. İş hattından
sadece Programmer sınıfından nesneler akıyor. Bu nesneler akarken yapılacak işleri
bir metot zinciri olarak verebiliyoruz: filter() ve reduce(). filter() ile
akan nesneler arasından seçim yapabiliyoruz. İş hattından, sadece verdiğimiz koşula
uyan programcıların akmasını sağlıyoruz.
Bölüm 4
İplikler
17 | S a y f a
İşletim Sistemleri
Kod 4.13:
private static Programmer mapReduce(List<Programmer> list) {
long start = System.nanoTime();
Programmer oldest =
list.parallelStream()
.filter( (Programmer prg) ->
prg.getAge() > 40 &&
prg.getProgrammingLanguage()
.getName().equalsIgnoreCase("java")
).reduce( (left, right) ->
left.getProgrammingLanguage().getLevel() >=
right.getProgrammingLanguage().getLevel()
? left : right
).get();
return oldest;
}
Bölüm 4
İplikler
18 | S a y f a