İş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
© Copyright 2024 Paperzz