YILDIZ TEKNİK ÜNİVERSİTESİ BİLGİSAYAR MÜHENDİSLİĞİ İŞLETİM SİSTEMLERİ (POSIX THREADS v1) ARŞ. GRV. UĞUR ÇEKMEZ
Multi Thread Programlama Thread Çalışma Mantığı Basit manada threadler, bir prosesin içinde var olan ve işle:m sistemi tara;ndan o prosesin kendi işleyişinden bağımsız olarak çalış?rılabilen ayrık kod parçalarıdır. Threadler için ayrık kelimesini karşılayan faktörler aşağıdaki şekilde sıralanabilir: - Stack pointer, register, schedule, thread specific data Sıralanan faktörler, her bir threadin kendi kontrolünde işlenmektedir. Threadlerin parent prosesleri var olduğu müddetçe kendilerinin de bir kontrol akışı vardır ve yalnızca (temel manada) hayama kalabilmek için gereksinim duydukları kaynakları parent proseslerinden kopyalarlar. Parent proses sonlandığında, onun al?nda çalış?rılan tüm threadler de sonlanır. Threadler, kaynaklarını aynı proses al?nda oluşturulan diğer bağımsız (veya bağımlı) threadler ile paylaşabilirler. Bu demek oluyor ki bir threadin paylaşılan değişkenler üzerindeki hareketleri doğrudan veya dolaylı yoldan diğer threadleri etkileyebilir ve aynı proses içindeki threadler bu değişikliklerden haberdar olurlar. Aynı kaynağa erişebilen threadlerin ilgili kaynak üzerinde aynı anda erişim gereksinimleri doğabilir. Örneğin bir thread t anında F dosyasının sa?rlarını okumak isterken, başka bir thread bu dosyaya hali hazırda veri girmekte olabilir. Thread dizaynı yapılırken, paylaşılan kaynaklar için belirli kri:k noktalar senkronize edilip paylaşım problemlerinin önüne geçilebilir. Bu dokümanın ilerleyen örneklerinde bu tarz problemlere değineceğiz. (POSIX thread yapısı, IEEE POSIX standartlarına göre belirlenmektedir)
Proses vs. Thread Thread man?ğının işlenmesine geçilmeden, öncelikle UNIX proses yapısının anlaşılması fayda sağlayacak?r. Bir proses, gelen komutlarla birlikte işle:m sistemi vasıtasıyla oluşturulur ve bir prosesin oluşturulması için belli bir sistem kaynağı tahsis edilir. Bunun da sisteme belli bir yükü vardır. Prosesler özet olarak aşağıdaki kaynaklara sahip:rler: - Proses ID, grup ID, user ID, çalışılan dizin, program komutları, register, stack, heap, diğer proseslerle mesajlaşma araçları vs. Bir prosesin oluşturulması ve yöne:lmesi ile aynı işlemlerin thread ayağı kıyaslandığında, thread oluşturma, işleme ve sonlandırma yükü çok daha hafizir. Threadler yukarıda belir:len proseslerin içerisinde çalışırlar. Thread modelleri Manager / Worker : Yöne:ci (manager) thread, diğer (worker) threadlere iş paslar. İşin bölümlenmesi ve dağı?lması yöne:ciye ai`r. Bu dağı?m sta:k veya dinamik şekilde yapılabilir. Pipeline : Bir görev alt görevlere bölünür ve bu alt görevlerden paralel olarak çalışması mümkün olanların her biri farklı bir thread tara;ndan yürütülür. Peer : Manager/Worker ile benzer yapıda olup diğerinden farkı, yöne:ci threadin de görev bölümleme haricinde işin içine girmesidir. Pthread API POSIX threadleri kullanabileceğimiz API, aşağıdaki 4 temel kategoride incelenebilir: Thread management : Bu kategoride threadler üzerinde yapılan işlemler ele alınır. Thread oluşumu, görev atanması, işleme geçilmesi vs. Mutex : Senkronizasyonla ilgili konuları kapsayan kategoridir. Mutex işlemleri, bunları oluşturma, sonlandırma, kilitleme ve kilitlenmiş mutexleri açma olarak tanımlanabilir. Condi:on variables : Mutex leri paylaşan threadler arasındaki ile:şimi kapsayan kategoridir. Programı yazan geliş:riciye bağlıdır. Mutexleri paylaşan threadlerin bu mutexleri kullanması, diğer threadlerin bekle:lmesi ve paylaşılan değişkenlerden ilgili threadlerin çıkarılması gibi işlemler yapılır. Synchroniza:on : Okuma ve yazma kilitlerini ve bariyerlerin oluşumunu kapsayan kategoridir.
Thread Management C programı çalış?rıldığında, varsayılan olarak birinci oluşturulan thread, programın main(..) fonksiyonudur. Diğer threadlerin tümü el ile oluşturulabilir. pthread_create(..) fonksiyonu ile yeni bir thread oluşturup bu thread çalış?rılabilir hale ge:rilir. Oluşturulmak istenen her bir thread için bu fonksiyon kullanılabilir. Fonksiyonun alacağı parametreler aşağıdaki şekildedir: - thread : thread için önceden tanımlanmış bir adres (değişken adresi) - amr : thread için verilebilecek diğer parametreler. Bir object değeri verilebilir. Yok ise NULL verilir. - start_rou:ne : thread oluşturulduğunda ilk çalış?racağı fonksiyon - arg : start_rou0ne fonksiyonuna verilecek parametre Bir programın oluşturabileceği maksimum (limitlenmiş) bir thread sayısı vardır. İlgili limitler işle:m sistemi tara;ndan sistem kaynaklarına göre belirlenir. Bundan daha fazla thread oluşturulmak istenirse program tara;ndan hata üre:lebilir. Burada sistem kaynakları, ilgili threadlerin paylaş?ğı sistem kaynaklarıdır (örneğin bir prosesin sahip olduğu stack alanı gibi.) ulimit -a Maximum size of core files created (kb, -c) 0 Maximum size of a process s data segment (kb, -d) unlimited Maximum size of files created by the shell (kb, -f) unlimited Maximum size that may be locked into memory (kb, -l) unlimited Maximum resident set size (kb, -m) unlimited Maximum number of open file descriptors (-n) 256 Maximum stack siz (kb, -s) 8192 Maximum amount of cpu time in seconds (seconds, -t) unlimited Maximum number of processes available to a single user (-u) 709 Maximum amount of virtual memory available to the shell (kb, -v) unlimited Sarı renk ile çizilmiş alan bir kullanıcının bir proseste kullanabileceği maksimum stack boyutunu verir. Prosesler içinde oluşturulan threadler de bunu paylaşırlar. Dolayısı ile toplamda üre:lebilecek thread sayısı için yaklaşık olarak: sistemin sahip olduğu bellek / stack boyutuna karşılık gelir diyebiliriz Threadler, oluşturulduktan sonra ar?k başka threadleri de oluşturabilirler.
Oluşturulan threadler, aşağıda anla?lan durumlarda sonlandırılabilir: - Thread normal döngüsü içinde kendisine atanan görevi tamamladığında - Thread pthread_exit(..) fonksiyonunu çağırdığında (görev tamamlanmasa bile çağırabilir) - Thread, başka bir thread tara;ndan pthread_cancel(..) fonksiyonu ile kesildiğinde - Threadin içinde bulunduğu parent proses herhangi bir şekilde sonlandırıldığında (Örneğin exit() ile) - main() fonksiyonu pthread_exit(..) fonksiyonunu çağırmadan sonlandığında (Eğer thread herhangi bir dosyayı düzenlemek için aç?ysa ve henüz kapatmadıysa, thread sonlandırıldığında da bu dosya açık kalabilir) Örnek bir thread oluşturma, işleme ve sonlandırma uygulaması aşağıdaki şekilde özetlenmiş:r: #include <pthread.h> #include <stdio.h> #include <stdlib.h> #define THREAD_SAYISI 5 void * giris_fonksiyonu(void * thread_id) { printf("merhaba, ben %ld. thread\n", (long)thread_id); pthread_exit(null); int main () { pthread_t threads[thread_sayisi]; int donus_degeri; long i; for(i=0; i<thread_sayisi; i++){ printf(" %ld. thread olusturuluyor...\n", i); donus_degeri = pthread_create(&threads[i], NULL, giris_fonksiyonu, (void *)i); if (donus_degeri){ printf("hata : %d\n", donus_degeri); exit(-1); pthread_exit(null);
Threadler arası senkronizasyon için pthread_join(..) fonksiyonu kullanılır. pthread_join(..) fonksiyonuna parametre olarak ilgili threadin ID si verilir ve bu thread bitene kadar ilgili komut satırı bekleme moduna geçer. Thread sonlandıktan sonra, join fonksiyonunu çağıran thread normal işleyişine döner. Sonlanan thread in bitiş durumunu görmek için, ilgili thread in pthread_exit(..) çıkış fonksiyonuna parametre olarak status değeri verilir ve bu değer join fonksiyonunun dönüş değeri olarak alınabilir. Bir join fonksiyonu yalnızca bir thread ile ilişkilendirilebilir. Her bir thread için ayrı ayrı join çağrımı yapılmalıdır. - pthread_self(..) fonksiyonu, içinde çağrıldığı threadin sistem tarafından atanmış olan ID sini döndürür. - pthread_equals(t_id1, T_ID2) fonksiyonu, ID leri verilmiş iki threadin aynı olup olmadığını kontrol eder. Threadler farklı ise 0, aynı ise farklı bir değer döndürür.
Aşağıda örnek bir join işlemi vardır: #include <pthread.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #define THREAD_SAYISI 10 void * giris_fonksiyonu(void * thread_id) { int rastgele_sayi = rand() % 4; sleep(rastgele_sayi); printf("merhaba, ben %ld. thread\n", (long)thread_id); pthread_exit(null); int main () { srand(time(null)); pthread_t threadler[thread_sayisi]; int donus_degeri; long i; for(i=0; i<thread_sayisi; i++){ printf(" %ld. thread olusturuluyor...\n", i); donus_degeri = pthread_create(&threadler[i], NULL, giris_fonksiyonu, (void *)i); if (donus_degeri){ printf("hata : %d\n", donus_degeri); exit(-1); // tum threadler bitene kadar bekle for(i=0; i<thread_sayisi;i++) { donus_degeri = pthread_join(threadler[i], NULL); if (donus_degeri){ printf("hata : %d. threadde bir sorun oldu\n", donus_degeri); exit(-1);
Mutex Mutex değişkeni, paylaşılan bir veri kaynağını kilitleyerek erişimi koruma amacıyla kullanılır. Posix thread lerde bu manadaki temel mantık, anlık olarak yalnızca bir thread in bir mutex değişkenini kilitleyebilmesi üzerinedir. Bu demek oluyor ki, atomik seviyede düşünülebilecek bir işlem olan mutex değişkenini kilitlemeyi, ortamda birden fazla thread bulunsa bile yalnızca biri başarıp bu değişkene sahip olabilecektir. Kilitlenmiş bir mutex değişkenini, ancak onu kilitleyen thread tekrar açabilir ve diğer threadlerin ona erişebilmelerine izin verebilir. Mutex değişkenleri, pthread_mutex_init(mutex, a@r), pthread_mutex_destroy(mutex) komutlarıyla oluşturulup kaldırılabilirler. Mutex değişkenleri, pthread_mutex_lock(mutex), pthread_mutex_trylock(mutex) pthread_mutex_unlock(mutex) komutlarıyla kilitlenirler ve kilitleri açılabilir. ve Birden fazla thread, kilitlenmiş bir mutex değişkeni için bekleme yapıyorsa, hangi thread in mutex değişkeninin kilidi açıldığında ona erişeceği, eğer sistemde bir priority scheduling yapısı tanımlanmadıysa, çoğunlukla rastgele bir yapıda işletim sistemi tarafından belirlenir. Mutex değişkenlerinin kullanıldığı bir örnek aşağıdaki şekildedir #include <pthread.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #define THREAD_SAYISI 10 void * giris_fonksiyonu(void * thread_id) { pthread_mutex_lock(&lock); sleep(rand()%5); int i; sayac += 1; printf("%d. islem basladi...\n",sayac); for(i=0; i<(0xfffff);i++); printf("%d. islem bitti...\n",sayac); pthread_mutex_unlock(&lock); pthread_exit(null); int main() { pthread_t threadler[thread_sayisi]; // threadlerimize yer aciyoruz pthread_mutex_init(&lock,null); // muteximizi olusturuyoruz long i,donus_degeri; srand(time(null)); for(i=0; i<thread_sayisi; i++){ donus_degeri = pthread_create(&threadler[i], NULL, giris_fonksiyonu, (void *)i); if (donus_degeri){ printf("hata : %ld\n", donus_degeri); exit(-1); pthread_mutex_destroy(&m); return 0;
Condition Variables Bu yapı, threadlerin senkron bir halde çalışabilmesi için bir başka yoldur. Mutex değişkenleri ile bir threadin veriye erişip erişemeyeceğini kontrol edebilirken, CV ile threadlerin bir verinin güncel durumundaki değişikliğe göre o veriye erişip erişemeyeceğini kontrol edebiliriz. CV yardımıyla program geliştiricinin, örneğin bir sayaç değişkeninin değerinin sıfırdan büyük olup olmadığını elle kontrol etmesine gerek kalmamaktadır. Bu da hem yazılımın karmaşıklığı hem de sürekli manüel kontroller yaparak sistem kaynaklarını harcamasının önüne geçmektedir. Koşul değişkenleri, pthread_cond_init(cond,a@r), pthread_cond_destroy(cond), pthread_conda@r_init(a@r) ve pthread_conda@r_destroy(a@r) fonksiyonları ile oluşturulup kaldırılabilirler. Threadlerin belli şartlar sağlanana kadar paylaşılan kaynaklara erişiminin kısıtlanması ise pthread_cond_wait(cond, mutex), pthread_cond_signam(cond) ve pthread_cond_broadcast(cond) fonksiyonları ile tanımlanır. Örnek bir kod aşağıdaki şekildedir. #include <stdio.h> #include <stdlib.h> #include <pthread.h> pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t c = PTHREAD_COND_INITIALIZER; int sayac = 0, aralik1 = 3, aralik2 = 6; void * f1() { while(1) { pthread_mutex_lock(&m); // mutexi kilitle pthread_cond_wait(&c,&m); // f2 calisirken bekle, sinyal gönderecek sayac++; printf("f1 : %d\n", sayac); pthread_mutex_unlock(&m); // mutexi kilidini aç if(sayac >= 10) return(null); void * f2() { while(1) { pthread_mutex_lock(&m); if (sayac < aralik1 sayac > aralik2) { pthread_cond_signal(&c); else { sayac++; printf("**f2 : %d\n", sayac); pthread_mutex_unlock(&m); if(sayac >= 10) return(null); int main() { pthread_t t1, t2; pthread_create(&t1, NULL, &f1, NULL); pthread_create(&t2, NULL, &f2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("sayacin son durumu : %d\n", sayac); return 0;