> "Threads in POSIX" : >> "thread" kavramı, 90'lı yıllarda işletim sistemlerinde kullanılmaya başlanmıştır. Fakat ilk ortaya çıkış tarihi evvelki yıllara dayanmaktadır. >> Bizler bu konu altında yüksek seviyeli dillerin "thread" kütüphanelerini değil, işletim sisteminin "thread" kavramı için sunduğu hizmetleri göreceğiz. Sonuçta C++ dili, C# ve Java gibi dillerdeki "thread" kütüphaneleri, işletim sisteminin "thread" kavramı için sunduğu hizmetleri bir nevi "wrap" etmektedir ve bu dillerdeki ilgili kütüphaneler "thread" kavramını "cross-platform" yani platform-bağımsız hale getirmektedir. C diline de C99 ile eklenmiş fakat "optional" olarak bırakılmıştır. "MSVC" derleyicileri tarafından henüz desteklenmemekte, "glibc" kütüphanesinde desteklenmektedir. Özetle; bu konu altında anlatılacak başlıklar, yukarıdaki dillerin kütüphanelerince kullanılmaktadır. >> Pekiyi "thread" nedir? Bir prosesin bağımsız olarak çizelgelenen akışlarına denmektedir. Anımsanacağınız üzere işletim sisteminin çizelgeleyicisi, prosesleri değil "thread" leri "quanta" süresi boyunca işletmektedir. Tabii "quanta" süresi dolduktan sonra yeni gelecek "thread" aynı proses de ait olabilir başka bir prosese de ait olabilir. Buradan hareketle diyebiliriz ki bir proses yalnızca bir "thread" den değil, birden fazla "thread" den oluşabilir. >> Bir proses çalışmaya tek bir "thread" ile başlar. Örneğin, "fork" işlemi ile alt proses oluşturduğumuzda alt proseste sadece bir adet "thread" oluşmaktadır. Halihazırda üst proseste bulunan diğer "thread" ler, programcı tarafından oluşturulmalıdır. Benzer biçimde "exec" yaptıktan sonra yeni proses tek bir "thread" ile çalışmaya başlar. Önceki proseste bulunan "thread" ler yok edilir. >> "thread" mekanizması şu faydaları sağlamaktadır: >>> Arka planda yapılması istenen işler için güzel bir araçtır. Çünkü bizim prosesimiz "block" olduğunda aslında "thread" bloke edilmektedir. Örneğin, klavyeden bir karakter okumak isteyelim. Aynı zamanda da ekrana bir saat basılmasını isteyelim. "getchar()" fonksiyonu blokeye yol açacağı için ekrana saat basılma işi de duracaktır. İşte bu işi "thread" ler ile yaparsak, sadece "getchar()" çağrısını yapan "thread" bloke olacaktır. Fakat ekrana saat basılmaya devam edecektir. >>> Diyelim ki o an sistemimizde dört adet proses koşuyor olsun. "thread" kavramı olmasaydı, 1/4 oranı ile bir proses çalışacaktı. Şimdi "thread" mekanizması ile proseslerden bir tanesine iki tane daha "thread" ekleyelim. Şimdi toplamda 6 adet, fakat ilgili proses bazında 3 adet "thread" var olacaktır. Dolayısıyla ilgili proses artık 3/6 oranı ile çalışacaktır. Bu da demektir ki o proses daha hızlı çalışacaktır. Örneğin, "Unilever" isimli marka bünyesinde "Cornetto", "Algida", "Carte Dor", "Magnum" gibi dondurma markalarını barındırmaktadır. Böylelikle müşteriler açısından bir çeşit oluşturulmakta fakat kazanç tek bir kişinin cebine yansımaktadır. İşte "thread" mekanizmasının sunduğu performans artışı da bunun gibidir. >>> Paralel programlama yapabilmek için de "thread" leri mutlat suretle kullanması gerekmektedir. >> "thread" kavramı ile şu aşağıdaki kavramlar birbiriyle ilişki içerisindedir: >>> "Concurrent Computing" : Birden fazla akışın söz konusu olduğu bütün durumlar için kullanılabilmektedir. Genel bir terimdir. >>> "Multithreading Programming" : Bir işin birden fazla "thread" ile gerçekleştirildiği uygulamalara denir. >>> "Reentrancy" : Bu terim genellikle fonksiyonlar için kullanılır. Fonksiyon akışının iç içe geçebilmesidir. Örneğin, iki farklı "thread" in aynı fonksiyon akışına girmesidir. Özyineleme, "reentrancy" demek değildir. Özyinelemede tek bir akış vardır. >>> "Parallel Programming" : Aynı makinada bir programın çeşitli "thread" lerinin farklı CPU'da eş zamanlı çalıştırma gayretine denmektedir. >>> "Distributed Computing" : Bir işi farklı bilgisayarlara dağıtarak, eş zamanlı bir biçimde ele almaktır. >> "thread" ile ilgili fonksiyonlar, "pthread" isimli POSIX başlık dosyasında yer alırlar ve bu fonksiyonların isimleri "pthread_" ön eki ile başlarlar. Örneğin, "pthread_join". >> "pthread" kütüphanesindeki fonksiyonları kullanabilmek için, derleme sırasında "-lpthread" seçeneğini kullanmamız gerekmektedir. Böylelikle ilgili kütüphane "linker" tarafından işleme sokulacaktır. >> "thread" ler aynı prosesin farklı akışlarıdır. Bu açıdan bir işi iki "thread" e yaptırmak ile iki ayrı prosese yaptırmak açısından şöyle farklılıklar vardır. Örneğin, >>> "thread" lerin oluşturulması ve yok edilmesi proseslere nazaran daha hızlıdır. >>> "thread" ler, içinde bulunduğu prosesin bellek alanını kullanır. Fakat prosesler, kendilerine has alanları kullanır. Bu nedenledir ki iki prosesin haberleşmesi, aynı proses içerisindeki "thread" lerin haberleşmesine nazaran daha maliyetlidir. >>> "thread" ler daha az sistem kaynağı harcama eğilimindedirler. >>> Proses kavramı daha kapsayıcı bir terimdir. Çünkü prosesler, o an çalışmakta olan bir programın bütün bilgilerini içerir. Fakat "thread" ler sadece birer akış belirtmektedir. Bu sebepledir ki; >>>> "thread" lerin Gerçek/Etkin Kullanıcı ID ve Gerçek/Etkin Grup ID değerleri yoktur. Fakat prosesler bu ID değerlerine sahiptirler. >>>> Prosesler "Current Working Directory", "Environment Variables", "Dosya Betimleyici Tablosu" vb. kavramalara sahiptir fakat "thread" ler için böyle kavramlar söz konusu değildir. Yani bir prosese özgü "Dosya Betimleyici Tablosu" varken "thread" e özgü "Dosya Betimleyici Tablosu" mevcut değildir. Dolayısıyla bir "thread" in açtığı dosyayı diğer "thread" de görecektir. >>> "thread" ler arasında "parent-child" ilişkisi yoktur. Bu sebepledir ki herhangi bir "thread", herhangi bir "thread" tarafından oluşturulabilir. Benzer şekilde bir "thread" in "exit code" bilgisi diğer "thread" ler tarafından da temin edilebilir. >> Bir "thread" oluşturulurken, "thread" akışının başlatılacağı bir fonksiyon da belirtilmelidir. Nasılki bir program ilk çalışmaya başladığında sadece "main-thread" varsa ve bu da "main" fonksiyonundan çalışmaya başlıyorsa, bizlerin oluşturacağı diğer "thread" de bizim göstereceğimiz fonksiyondan çalışmaya başlayacaktır. >> "thread" ler o prosesin "static" veri elemanlarını ortak kullanırlar. Yani bir "thread" bu tip bir veri elemanını değiştirdiğinde, diğer "thread" bu değişikliği görecektir. Anımsanacağınız üzere "static" veri elemanları "global namespace" alanındakiler veya "static" anahtar kelimesi ile nitelenen değişkenlerdi. Hakeza otomatik ömürlü değişkenler, "thread" lere özgüdür. Yani her "thread" için bir kopya oluşturulur. Dolayısıyla bir "thread", o değişken üzerinde bir değişiklik yaptığında, diğer "thread" bunu göremeyecektir. Bunun da sebebi "thread" in "stack" alanları birbirinden ayrılmış olmasıdır. >>> Bir "thread" in "stack" büyüklüğü "64-bit" Linux sistemlerinde 8MB büyüklüğünde, "32-bit" Linux sistemlerde ise 2MB büyüklüğündedir. Windows sistemlerde ise 1MB büyüklüğündedir. >> Proseslerin ID değerleri "pid_t" türü ile temsil edilirken, "thread" lerin ID değerleri "pthread_t" türü ile temsil edilmektedir. Öte yandan proseslerin ID değerleri sistem genelinde "unique" haldeyken, "thread" lerin ID değerleri o proses genelinde "unique" haldedir.Yani farklı proseslerin oluşturduğu "thread" lerin ID değerleri birbiriyle aynı olabilir. >> POSIX "thread" kütüphanesindeki ilgili fonksiyonların ortak özellikleri şu şekildedir; >>> Bütün fonksiyonların isimleri "pthread_" ön eki ile başlamaktadır. >>> Çoğu fonksiyonun geri dönüş değeri "int" türdendir. Bu fonksiyonlar başarı durumunda "0" değerine, başarısızlık durumunda "errno" değişkeninin alacağı değeri geri döndürmektedir ve "errno" değişkenini "set" ETMEMEKTEDİR. Çünkü her "thread" kendisinin "errno" değişkenini "set" etmektedir. Diğer "thread" lerin "errno" değişkenleri bundan etkilenmeyecektir. "perror" fonksiyonu da kendi "thread" inin "errno" değişkenini kullanmaktadır. Dolayısıyla hata mesajını ekrana yazarken aşağıdaki gibi bir fonksiyonu kullanabiliriz: void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Aşağıda ise bir hata kontrolünün tipik uygulanışı yer almaktadır: int result; //... if((result = pthread_xxx(...)) != 0) exit_sys("pthread_xxx", result); //... >>> Bu fonksiyonların bulunduğu kütüphanenin ismi ise "pthread.h" biçimindedir. >> Pekiyi bizler bir "thread" i nasıl oluştururuz? Anımsanacağınız üzere bir program hayata tek bir "thread" ile gelmektedir ve buna da "main-thread" denmekteydi. İkinci ve diğer "thread" leri bizler oluşturmalıyız. İşte bir "thread" oluşturmak için kullanacağımız fonksiyonun ismi "pthread_create" biçimindedir. >>> "pthread_create" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg); Fonksiyonun birinci parametresi, oluşturulacak "thread" in ID değerini elde etmek için kullanılan adres değeridir. "pthread_t" türünden bir nesnenin adresini bu parametreye geçerek, oluşturulan "thread" in ID değerini "get" edebiliriz. İş bu "pthread_t" türünün hangi gerçek türe karşılık geldiği sistemden sisteme değişiklik göstermektedir. Aritmetik türlere karşılık geldiği gibi bir yapı türüne de denk gelebilmektedir. İşte bu sebepten ötürü iki "pthread_t" türden değişkenleri kendi aralarında karşılaştırmaya çalışmamalıyız. Bunun için özel olarak yazılmış fonksiyonları kullanmalıyız. Fonksiyonun ikinci parametresi, oluşturulacak olan "thread" in bir takım özelliklerini önceden "set" etmek için kullanılır. Bu parametreye "NULL" değerini geçmemiz, varsayılan özelliklerin kullanılacağı anlamındadır. Bu parametre daha sonra tekrar ele alınacaktır. Fonksiyonun üçüncü parametresi, oluşturulacak "thread" in akışının başlayacağı fonksiyonun adresini belirtmektedir. Bu fonksiyon öyle bir fonksiyon olmalıdır ki geri dönüş değeri ve parametresi "void*" olmalıdır. Fonksiyonun dördüncü ve son parametresi, üçüncü parametresine geçtiğimiz fonksiyona geçilecek değeri belirtmektedir. * Örnek 1, Aşağıdaki kodu derlerken "-lpthread" seçeneğini de kullanmalıyız. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 sub-thread: 2 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 3; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 3; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda "thread" in akışının başladığı fonksiyona parametre de geçilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); #define TOTAL_THREADS 3 int main(int argc, char** argv) { int result; pthread_t tid[TOTAL_THREADS]; char* buffer; for(int i = 0; i < TOTAL_THREADS; ++i) { if((buffer = (char*)malloc(1024)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } snprintf(buffer, 1024, "%d. Thread\n", i); if((result = pthread_create(&tid[i], NULL, thread_proc, buffer)) != 0) exit_sys("pthread_create", result); } puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { char* buffer = (char*)param; for(int i = 0; i < 5; ++i) { printf("%d. Thread: %s\n", i, buffer); sleep(1); } free(param); return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> Bizler bir "thread" oluşturduğumuzda, o "thread" i oluşturan "main-thread" mi ilk çalışmaya başlar yoksa oluşturulan "thread" mi? POSIX standartlarınca hangisinin önce başlayacağına dair bir garanti yoktur. Bu iş tamamiyle işletim sisteminin çizelgeleyicisine bağlıdır. Bu noktada garanti olan şey, "thread" lerin oluşturulduktan sonra otomatik olarak çalışmaya başlamalarıdır. Çünkü bazı sistemlerde "thread" ler oluşturulduktan sonra onlara ayrıca "start" vermek gerekmektedir. >> Pekiyi bir "thread" nasıl sonlanır? Birden fazla yöntem vardır. Bunlar; >>> Yöntemlerden bir tanesi doğal ölümdür. Yani "thread" in akışı sona geldiğinde "thread" otomatik olarak yok edilir. >>> Bir diğer yöntem ise "pthread_exit" fonksiyonunun kullanımıdır. Nasılki "exit" / "_exit" fonksiyonları kendi proseslerini sonlandırıyorsa, "pthread_exit" fonksiyonu da kendi "thread" ini sonlandırmaktadır. Yani hangi "thread" akışı "pthread_exit" fonksiyonunu görürse sonlandırılır. >>>> "pthread_exit" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include void pthread_exit(void *value_ptr); Fonksiyonun tek parametresi, ilgili "thread" i sonlandırırken alacağı "exit code" bilgisidir. Tıpkı "exit" veya "_exit" fonksiyonlarının kullanımı gibi. Öte yandan şu noktaya da dikkat etmeliyiz: "main" fonksiyonunun geri dönüş değer "int" türden ve "exit" fonksiyonunun parametresi de "int" türden. İşte "pthread_xxx" fonksiyonlarının geri dönüş değeri "void*" türden ve "pthread_exit" fonksiyonunun parametresi de "void*" türden. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 main-thread: 0 sub-thread: 1 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { if(i == 2) pthread_exit(NULL); printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> Üçüncü yöntem ise "exit" ya da "_exit" fonksiyonlarının çağrılmasıdır. Bir programda bu iki fonksiyondan birisinin çağrılması durumunda proses sonlanacağı için o prosesin bütün "thread" leri de otomatik olarak sonlanacaktır. Tabii bu iki fonksiyonu "main-thread" çağırabildiği gibi daha sonra oluşturulan "thread" ler de çağırabilir. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 main-thread: 0 sub-thread: 1 main-thread: 1 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { if(i == 2) exit(EXIT_SUCCESS); printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi bu noktada dikkat etmemiz gereken bir nokta vardır: Hiç bir "thread", yukarıdaki "exit", "_exit" veya "pthread_exit" fonksiyonlarından birini henüz çağırmamışken "main-thrad" in akışı "main" fonksiyonunun sonuna gelirse, otomatik olarak "exit" fonksiyonu çağrılacağı için, yine bütün "thread" ler sonlanacaktır. Fakat bu demek değildir ki "main-thread" bittiğinde proses komple sonlanacaktır. Buradaki kilit nokta, "exit" çağrıldığı için prosesin sonlanmasıdır. Dolayısıyla diyebiliriz ki eğer "main-thread", "pthread_exit" çağrısı yaparsa, o prosesin diğer "thread" leri SONLANMAZ. YALNIZCA "main-thread" SONLANIR. Diğerleri devam eder. * Örnek 1, Aşağıdaki programda sonradan oluşturulan "thread" in elliye kadar sayması beklenirken "main-thread" in akışı "main" fonksiyonunun sonuna gelmesinden dolayı "exit" fonksiyonu çağrılacak ve ilgili bütün "thread" ler yok edilecektir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 sub-thread: 2 main-thread: 3 sub-thread: 3 main-thread: 4 sub-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 50; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "main-thread" in akışı "main" fonksiyonunu bitirmiş ve dolayısıyla "exit" çağrısı yapılmıştır. Bu sebeple bütün "thread" ler yok edilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 sub-thread: 0 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 50; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekteki "main-thread", "pthread_exit" ile sonlandırılmıştır. Daha sonra oluşturulan yeni "thread" in akışı bitmiş ve proses komple sonlanmıştır. Buradan da görüleceği üzere "main-thread" dediğimiz "threat" ile sonradan oluşturulan "thread" arasında bir fark yoktur. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 sub-thread: 2 sub-thread: 3 sub-thread: 4 sub-thread: 5 sub-thread: 6 sub-thread: 7 sub-thread: 8 sub-thread: 9 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 3) pthread_exit(NULL); printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 10; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Pekiyi akla şu soru gelmektedir: "main-thread" sonlanacağı için, onun akışı "main" fonksiyonunun sonuna gelmeyecektir. Pekiyi bu durumda proses nasıl sonlanacaktır? İşte bir prosesin hayattaki son "thread" i sonlandığında, otomatik olarak proses de sonlandırılmaktadır. >>> Dördüncü sonlandırma yöntemi ise sinyallerin kullanımıdır. Bir prosese sinyal gönderilmişse ve ilgili proses de bu sinyali ele almamışsa prosesimiz sonlandırılabilir. Özellikle bazı sinyaller prosesi sonlandırma potansiyeline sahiptir. >>> Beşinci sonlandırma yöntemi ise "pthread_cancel" fonksiyonu ile başka "thread" ler tarafından sonlandırılma yöntemidir. >>>> "pthread_cancel" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_cancel(pthread_t thread); Fonksiyonun tek parametresi, "pthread_create" fonksiyonunun birinci parametresine geçilen, "thread" in ID değeridir. Bu fonksiyon geri döndüğünde, sonlandırılmak istenen "thread" in sonlanmış olması GARANTİ DEĞİLDİR. Sadece sonlanma süreci başlamıştır. >> "thread" lerin hayata getirilmesi (devam): Anımsayacağımız üzere bir "thread" oluştururken "pthread_create" fonksiyonunu kullanılmaktaydık. >>> "pthread_create" fonksiyonunun son parametresine, üçüncü parametresine geçilen fonksiyona gönderilecek olan değeri geçiyorduk. Normal şartlarda dördüncü parametreye bizler birer adres değerleri geçmekteyiz. Eğer adres yerine bir değer geçmek istiyorsak önce bu değeri "void*" a "cast" etmeli, sonrasında "pthread_create" fonksiyonuna geçmeliyiz. Fonksiyonun geri dönüş değeri ise başarı durumunda "0" değerine, başarısızlık durumunda ise yukarıda da açıklandığı üzere hata koduna geri dönmektedir. >> "thread" lerin sonlandırılması (devam): >>> Beşinci sonlandırma yöntemi için "pthread_cancel" fonksiyonundan bahsetmiştik. İş bu fonksiyon bazı detaylara sahiptir. Fakat bu detaylara ilerleyen vakitlerde değinilecektir. >> Biten "thread" lerin "exit code" bilgilerini nasıl alacağız? Anımsanacağınız üzere tamamlanan proseslerin "exit code" bilgileri "wait" fonksiyonları ile temin edilmekteydi. Öte yandan "thread" ler arasında da "parent-child" ilişkisi olmadığından bahsetmiştik. İşte "thread" ler de "pthread_join" isimli fonksiyonu kullanarak, bir "thread" in "exit code" bilgisini elde edebiliriz. >>> "pthread_join" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_join(pthread_t thread, void **value_ptr); Fonksiyonun birinci parametresi "exit code" bilgisi temin edilecek "thread" in ID değeridir. Fonksiyonun ikinci parametresi ise "exit code" bilgisinin yazılacağı "void*" türden göstericinin adresini belirtmektedir. Daha doğrusu bu fonksiyon başarılı bir şekilde geri dönerse, "pthread_create" fonksiyonunun üçüncü parametresine geçilen fonksiyonun geri döndürdüğü adres değeri bu fonksiyonun ikinci parametresi ile geçilen adrese yazılacaktır. O fonksiyon da "void*" türden bir değer döndürdüğü için, o değerin adresi de "void**" türden olmaktadır. Eğer bu "exit code" değerini istemiyorsak, ikinci parametreye "NULL" değerini geçebiliriz. Fonksiyonun geri dönüş değeri başarı durumunda "0", başarısızlık durumunda ise hata kodu biçimindedir. Öte yandan iş bu fonksiyon, kendisini çağıran "thread" i bloke etmektedir. Eğer halihazırda sonlanmış bir "thread" in "exit code" değerini almak istersek, bloke oluşmayacaktır. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 sub-thread: 1 sub-thread: 2 sub-thread: 3 sub-thread: 4 Success!... Exit Code: [31] */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); printf("Exit Code: [%d]\n", (int)exit_code); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Tıpkı proseslerde olduğu gibi, "thread" lerde de "zombie" olma durumu oluşabilir. Örneğin, hayata getirdiğimiz bir "thread" i "join" etmezsek, bu "thread" in akışı bittiğinde, "zombie thread" oluşacaktır. Fakat "thread" lerin "zombie" olması, proseslerin "zombie" olmasından daha az zararlı da olsa yine de sistem için sorunlara yol açabilir. Dolayısıyla taviye edilen şey bir "thread" i hayata getirdikten sonra onu "join" etmektir. Pekiyi bizler hayata getirdiğimiz "thread" i bekleyemiyorsak ne yapmalıyız? O vakit "thread" lerin "detach" modda oluşturulmaları gerekmektedir. "thread" lerin "detach" moda sokulmasına ilerleyen vakitlerde değinilecektir. * Örnek 1, Aşağıdaki örnekte oluşturulan "thread" beklenmiş fakat "exit code" bilgisi alınmamıştır. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 sub-thread: 1 sub-thread: 2 sub-thread: 3 sub-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> "thread" lerin dışarıdan sonlandırılması, "cancel" edilmesi: Anımsayacağımız üzere "thread" ler başka "thread" ler tarafından sonlandırılabilmektedir. İşte bu işi yapan POSIX fonksiyonunun ismi "pthread_cancel" biçimindedir. Fakat unutmamalıyız "thread" lerin ID değerleri, o proseste anlamlıdır. Dolayısıyla başka bir prosesin "thread" sonlandıramayız. >>> "pthread_cancel" fonksiyonu, aşağıdaki parametrik yapıya sahiptir: #include int pthread_cancel(pthread_t thread); Fonksiyon parametre olarak sonlandırılacak "thread" in ID değerini alır. Geri dönüş değeri de başarı durumunda "0", başarısızlık durumunda hata kodudur. * Örnek 1, Aşağıdaki örnekte bizim tarafımızdan oluşturulan "thread" sonlandırılmış, "main-thread" ise akışına devam etmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi bir "thread" in başka bir "thread" tarafından sonlandırılma işleminin bazı detayları vardır. Örneğin, sonlandırma işlemi aslında bir nevi talep olarak ele alınmaktadır. Dolayısıyla bir "thread" i sonlandırmak istediğimizde, direkt olarak sonlandırma işlemi yapılmamaktadır. İlgili "thread" in akışı "cancellation point" diye isimlendirilen noktalara geldiğinde gerçek sonlandırma yapılmaktadır. >>> "Cancellation Point" içeren fonksiyonların listesine aşağıdaki link üzerinden ulaşabiliriz. https://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_09.html#tag_02_09_05_02 Fakat kabaca gerekirse "read", "write", "wait" vb. temel fonksiyonlar "cancellation point" barındırmaktadır. Eğer bir "thread" iş bu "cancellation point" leri görmez ise o "thread" i sonlandıramayız. Dolayısıyla beklemeye devam edeceğiz. * Örnek 1, Aşağıdaki örnekte diğer "thread" için sonlandırma talebi gönderilmiş fakat onun akışı "for" döngüsünden çıkamadığı için sonlandırma işlemi gerçekleştirilememiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(;;); for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Pekiyi bizler yukarıdaki "cancellation point" içeren fonksiyonları çağırmak istemiyorsak ve ilgili "thread" imizin de sonlanabilmesini istiyorsak ne yapmalıyız? İşte bu durumda devreye içinde boş bir "cancellation point" olan POSIX fonksiyonu girmektedir. Yani yalnızda "cancellation point" için yazılmış olan "pthread_testcancel" isimli bu fonksiyonu kullanmalıyız. >>> "pthread_testcancel" aşağıdaki parametrik yapıya sahiptir: #include void pthread_testcancel(void); Fonksiyon herhangi bir parametre almamakta ve herhangi bir değer geriye döndürmemektedir. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(;;) pthread_testcancel(); for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Öte yandan bir "thread" in "cancellation point" gördüğündeki davranışını da değiştirebiliriz. Bunun için iki adet POSIX fonksiyonu bulunmaktadır. Bu fonksiyonlar "pthread_setcancelstate" ve "pthread_setcanceltype" ismindedirler. >>> "pthread_setcanceltype" fonksiyonu, bu fonksiyonu çağıran "thread" in sonlandırma biçimini belirtir. Aşağıdaki gibi bir parametrik yapıya sahiptir: #include int pthread_setcanceltype(int type, int *oldtype); Fonksiyonun birinci parametresi şu değerlerden birisini almaktadır; "PTHREAD_CANCEL_DEFERRED" ve "PTHREAD_CANCEL_ASYNCHRONOUS". Bu parametrelerden, -> "PTHREAD_CANCEL_DEFERRED" : Bu değer kullanıldığında, "ilgili 'thread' in akışı bir 'cancellation point' e girdiğinde sonlandır" anlamına gelir. Bir "thread" oluşturulduğunda sahip olduğu varsayılan özellik budur. -> "PTHREAD_CANCEL_ASYNCHRONOUS" : Bu değer kullanıldığında, ilgili "thread" anında sonlandırılacaktır. Fonksiyonun ikinci parametresi ise önceki sonlandırma biçiminin yerleştirileceği "int" türden nesnenin adresini almaktadır. POSIX standartlarınca bu parametreye "NULL" geçilebilmesi için bir şey belirtilmemiştir. Dolayısıyla bu parametreye "NULL" değerini geçmemeliyiz. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda ilgili hata kodudur. * Örnek 1, Aşağıdaki programda tarafımızca oluşturulan "thread", direkt olarak sonlandırılmıştır. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_status); for(;;); return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> "pthread_setcancelstate" fonksiyonu, kendisini çağıran "thread" in sonlandırma durumunun değiştirilmesi için kullanılmaktadır. Fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int pthread_setcancelstate(int state, int *oldstate); Fonksiyonun birinci parametresi şu iki değerden birisi olabilir; "PTHREAD_CANCEL_ENABLE" ve "PTHREAD_CANCEL_DISABLE". -> "PTHREAD_CANCEL_ENABLE" değerinin kullanılması demek ilgili "thread" in "pthread_cancel" ile sonlandırılabileceği anlamındadır. Bir "thread" oluşturulduğunda sahip olduğu varsayılan özellik budur. -> "PTHREAD_CANCEL_DISABLE" değerinin kullanılması demek ilgili "thread" in "pthread_cancel" ile sonlandırılamayacağı anlamındadır. Eğer bir "thread" bu duruma sokulursa, "pthread_cancel" çağrısı sonrasında "thread" sonlandırılmaz ama sonlandırma isteği askıda bekletilir. Eğer "thread" in durumu "PTHREAD_CANCEL_ENABLE" durumuna çekilirse, sonlandırma isteği işleme alınır. "PTHREAD_CANCEL_DEFERRED" / "PTHREAD_CANCEL_ASYNCHRONOUS" durumuna göre ya o an sonlandırılır ya da bir "cancellation point" e girdikten sonra sonlandırılır. Fonksiyonun ikinci parametresi ise önceki sonlandırma biçiminin yerleştirileceği "int" türden nesnenin adresini almaktadır. POSIX standartlarınca bu parametreye "NULL" geçilebilmesi için bir şey belirtilmemiştir. Dolayısıyla bu parametreye "NULL" değerini geçmemeliyiz. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda ilgili hata kodudur. * Örnek 1, Aşağıdaki programda sonlandırılma özelliği pasif hale getirilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread : 0 main-thread: 1 sub-thread : 1 sub-thread : 2 main-thread: 2 sub-thread : 3 main-thread: 3 sub-thread : 4 main-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); if(PTHREAD_CANCELED == exit_code) printf("\nThe sub-thread has been terminated!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_status); for(int i = 0; i < 5; ++i) { printf("sub-thread : %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda sonlandırılma özelliği aktif hale getirilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread : 0 main-thread: 1 sub-thread : 1 sub-thread : 2 main-thread: 2 main-thread: 3 main-thread: 4 The sub-thread has been terminated!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); if(PTHREAD_CANCELED == exit_code) printf("\nThe sub-thread has been terminated!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_status); for(int i = 0; i < 5; ++i) { printf("sub-thread : %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Son olarak "pthread_cancel" ile sonlandırılan "thread" lerin "exit code" değeri, "PTHREAD_CANCELED" olarak, "pthread_join" fonksiyonundan elde edilmektedir. Bu değer pek çok sistemde aşağıdaki gibi tanımlanmıştır: #define PTHREAD_CANCELED ((void*)-1) Aşağıda bunun için bir örnek verilmiştir: * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 The sub-thread has been terminated!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); if(PTHREAD_CANCELED == exit_code) printf("\nThe sub-thread has been terminated!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_status); for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> Pekiyi bizler bir "thread" in ID değerini nasıl "get" edebiliriz? Böylesi bir durumda "pthread_self" isimli POSIX fonksiyonu devreye girmektedir. Böylelikle bu fonksiyonu çağıran "thread" in ID değerini elde edebileceğiz. Fonksiyonun imzası aşağıdaki gibidir: #include pthread_t pthread_self(void); Fonksiyon başarısız olamamaktadır. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... -464677056: 0 -464681408: 0 -464677056: 1 -464681408: 1 -464677056: 2 -464681408: 2 -464677056: 3 -464681408: 3 -464677056: 4 -464681408: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("%d: %d\n", (int)pthread_self(),i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("%d: %d\n", (int)pthread_self(),i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> Bir "thread" oluştururken varsayılan özelliklerde oluşturmamak için, "pthread_create" fonksiyonunun ikinci parametresine "pthread_attr_t" yapı türünden bir nesnenin adresini geçmemiz gerekmektedir. Hatırlayacağınız üzere daha önceki örneklerde bu parametre için "NULL" değeri geçilmişti. Pekiyi iş bu yapı türü neyin nesidir? Aslında bu tür "pthread.h" ve "sys/types.h" başlıklarında tür eş ismi olarak tanımlanmıştır fakat hangi türe karşılık geldiğine dair kesin bir şey söylenmemiştir. Tipik olarak bu tür bir yapı türünü temsil etmektedir. Ancak bu yapının hangi elemanlardan oluştuğu POSIX standartlarınca açıklanmamıştır. Dolayısıyla bu yapının elemanlarının ne olacağı sistemden sisteme değişiklik göstermektedir. Dolayısıyla bu yapı türünü kullanmak için bir takım fonksiyonlara çağrı yapmamız gerekmektedir. Örneğin, nesne yönelimli programlamada sınıfın veri elemanlarının "private" yapılması ve bu veri elemanlarına "ctor", "setter" ve "getter" fonksiyonları ile ulaşılması yukarıdaki duruma örnek olabilir. Pekiyi bu fonksiyonlar nelerdir? Tipik olarak "get" amacı taşıyan fonksiyonlar "pthread_attr_getXXX" biçiminde, "set" amacı taşıyanlar ise "pthread_attr_setXXX" biçiminde bir isimlendirmeye sahiptir. Öte yandan bu yapının içini doldurmak, bir nevi "initialize" etmek için, "pthread_attr_init" fonksiyonunu kullanırken bu yapıyı yok etmek için de "pthread_attr_destroy" fonksiyonunu kullanacağız. Özetle aşağıdaki sıralamayı takip edeceğiz; >>> İlk olarak "pthread_attr_t" türünden bir nesne hayata getireceğiz. * Örnek 1, //... #include //... int main(int argc, char** argv) { pthread_attr_t tattr; //... return 0; } //... >>> Daha sonra bu yapı nesnesine "pthread_attr_init" fonksiyonu ile ilk değer verilir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_attr_init(pthread_attr_t *attr); Fonksiyon argüman olarak "pthread_attr_t" türünden nesnenin adresini alarak ona ilk değer verir. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda ise hata kodunun kendisi ile geri dönmektedir. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); //... return 0; } //... >>> Artık "pthread_attr_setXXX" ve/veya "pthread_attr_getXXX" biçimindeki fonksiyonları kullanarak "thread" in özellikleri "set" ve/veya "get" edilebilir. Tabii bu bahsi geçen fonksiyonlar farklı alt konular ile ilgili olduğu için zamanla bu fonksiyonlar incelenecektir. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); //... return 0; } //... >>> Sonrasında "pthread_create" fonksiyonu ile belirlediğimiz özelliklerde bir "thread" hayata getirebiliriz. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); //... return 0; } //... >>> En sonunda da "pthread_attr_destroy" fonksiyonu ile "pthread_attr_init" ile yaptığımız işlemler geri alınabilir. Fonksiyonun parametrik yapısı aşağıdaki gibidir: #include int pthread_attr_destroy(pthread_attr_t *attr); Fonksiyon argüman olarak "pthread_attr_t" türünden yapının adresini almaktadır. POSIX standartlarınca bu fonksiyon için başarısızlık için bir şey söylenmemiştir. Dolayısıyla bu fonksiyon başarısız olamaz. Başarı durumunda ise "0" ile geri dönecektir. Öte yandan bu fonksiyonu, "pthread_create" fonksiyonundan hemen sonra çağrılabilir. Çünkü "pthread_create" fonksiyonu argüman olarak aldığı "pthread_attr_t" türünden nesnenin adresini saklamak yerine, bu adres içerisindekileri kopyalamaktadır. Bir nevi "pthread_attr_t" içerisindekileri başka yere almaktadır. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); //... return 0; } //... Şimdi de bütün bu süreci içine alan tek bir örnek verelim: * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("%d: %d\n", (int)pthread_self(),i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi "thread" özelliklerinden bir kaçına değinelim. >>>> "Stack Size": Bir "thread" in varsayılan "stack" büyüklüğü POSIX sistemlerinde 8MB, Windows sistemlerinde ise 1MB büyüklüğündedir. "pthread_attr_getstacksize" fonksiyonu ile bu büyüklüğü temin edebilir, "pthread_attr_setstacksize" ile yeni bir büyüklük değeri girebiliriz. >>>>> "pthread_attr_setstacksize" fonksiyonunun prototipi aşağıdaki biçimdedir: #include int pthread_attr_setstacksize(const pthread_attr_t *attr, size_t stacksize); Fonksiyonun birinci parametresi, "thread" özelliklerine ilişkin "pthread_attr_t" türünden nesnenin adresini almaktadır. İkinci parametre ise "thread" in yeni "stack" uzunluk bilgisidir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda hata kodunun kendisidir. >>>>> "pthread_attr_getstacksize" fonksiyonunun prototipi aşağıdaki biçimdedir: #include int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t* stacksize); Fonksiyonun birinci parametresi, "thread" özelliklerine ilişkin "pthread_attr_t" türünden nesnenin adresini almaktadır. İkinci parametre ise "thread" in yeni "stack" bilgisinin yerleştirileceği adrestir. POSIX standartlarında, fonksiyonun başarısızlığından söz edilmemektedir. Başarı durumunda ise "0" ile geri dönmektedir. Aşağıda bu hususa ilişkin örnek verilmiştir: * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Default Stack Size: 8388608 Default Stack Size: 2097152 Ok Done */ int result; pthread_attr_t tattr; /* * 1. İlk önce "tattr" yapısına ilk değer veriyoruz. */ if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); /* * 2. Daha sonra "main-thread" in "stack" bilgisini * elde ediyoruz. */ ssize_t size; if((result = pthread_attr_getstacksize(&tattr, &size)) != 0) exit_sys("pthread_attr_setXXX", result); printf("Default Stack Size: %zd\n", size); /* * 3. Daha sonra "main-thread" in "stack" bilgisini * güncelliyoruz. */ if((result = pthread_attr_setstacksize(&tattr, 2097152)) != 0) exit_sys("pthread_attr_setXXX", result); /* * 4. Daha sonra "main-thread" in "stack" bilgisini * yeniden elde ediyoruz. */ if((result = pthread_attr_getstacksize(&tattr, &size)) != 0) exit_sys("pthread_attr_setXXX", result); printf("Default Stack Size: %zd\n", size); /* * 5. Daha sonra özellikleri güncellenmiş bir "thread" oluşturuyoruz. */ pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("Done"); return 0; } void* thread_proc(void* param) { puts("Ok"); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> Anımsanacağınız üzere bir "thread" oluşturduktan sonra "zombie thread" oluşmaması için ya bu "thread" i beklemeli ya da ilgili "thread" in "detach" moda sokulması gerektiğinden bahsetmiştik. Pekiyi nedir bu "detach" mevzusu? Hayata getirdiğimiz "thread" in akışı bittiğinde kaynakları otomatik olarak boşaltılmaktadır. Dolayısıyla "pthread_join" ile "thread" i beklememize gerek kalmıyor. Unutmamalıyız ki bir "thread" hayata geldiğinde ya onu "pthread_join" ile beklemeli veya onu "detach" moda sokarak kendi halinde çalışmasını sağlamalıyız. Pekiyi bizler bir "thread" i nasıl "detach" moda sokabiliriz? Burada bir kaç farklı yöntem devreye girmektedir. Bunlar "pthread_detach" fonksiyonunu kullanmak ve "pthread_attr_setdetachstate" ile uygun özellikleri belirtip "pthread_create" fonksiyonuna çağrı yaparak. >>> "pthread_attr_setdetachstate" ve "pthread_create" fonksiyonlarını kullanmak: Bu yöntemde bizler bir "thread" oluşturmadan evvel onun bir takım özelliklerini "pthread_attr_setdetachstate" ile değiştiriyoruz. İş bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); Fonksiyonun birinci parametresi "pthread_attr_t" türünden yapı nesnesinin adresidir. İkinci parametre ise şu iki değerden birisini alabilir; PTHREAD_CREATE_DETACHED ve PTHREAD_CREATE_JOINABLE. -> PTHREAD_CREATE_DETACHED: Bu değeri alması durumunda oluşturulacak "thread" artık "detach" modda olacaktır. Yani akışı bittiğinde kaynakları otomatik olarak geri verilecektir. -> PTHREAD_CREATE_JOINABLE: Bu değeri alması durumunda oluşturulacak "thread" artık "joinable" modda olacaktır. Yani akışı bittiğinde kaynakları otomatik olarak geri VERİLMEYECEKTİR. Dolayısıyla bizler bu "thread" i "pthread_join" fonksiyonu ile beklemeliyiz. Aksi halde "zombie thread" meydana gelecektir. Varsayılan özellik olarak bu özelliğe sahiptir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda hata kodunun kendisine dönmektedir. Son olarak belirtmek gerekir ki "detach" moda sokulan bir "thread" artık "pthread_join" fonksiyonu ile BEKLENEMEZ. Bu durum "Tanımsız Davranış" a neden olacaktır. * Örnek 1, Aşağıdaki programda oluşturulacak olan "thread", "detach" moda sokularak oluşturulmuştur. Dolayısıyla bizim "main-thread", onu beklememiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done */ int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setdetachstate(&tattr, PTHREAD_CREATE_DETACHED)) != 0) exit_sys("pthread_attr_setdetachstate", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); puts("Done"); return 0; } void* thread_proc(void* param) { puts("Ok"); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> "pthread_detach" fonksiyonunu çağırmak da bir diğer yöntemdir. Fonksiyon, aşağıdaki prototipe sahiptir: #include int pthread_detach(pthread_t thread); Fonksiyon "detach" duruma sokulacak "thread" in ID değerini parametre olarak alır. Başarı durumunda "0" ile başarısızlık durumunda "non-zero" bir değere dönmektedir. Eğer "detach" edilecek "thread" zaten "detach" modda ise bu fonksiyon çağrısı "Tanımsız Davranış" meydana getirecektir, POSIX standartlarınca. * Örnek 1, Aşağıdaki programda bir "thread" oluşturulduktan sonra "detach" edilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_detach(tid)) != 0) exit_sys("pthread_detach", result); puts("Done"); return 0; } void* thread_proc(void* param) { puts("Ok"); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> "thread" lerin senkronize edilmesi: "thread" lerin birlikte ortak bir amacı gerçekleştirmesi, yani uyum içerisinde çalışması gerekebilmektedir. Özellikle ortak kaynaklara erişen "thread" ler için senkronizasyon önemli bir meseledir. Örneğin, aşağıdaki programda sonuç bazı durumlarda 2'000'000 çıkmamaktadır. * Örnek 1, #include #include #include #include #include int g_count = 0; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done => 1217532 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Bunun yegane sebebi "quanta" süresi bittiği anda thread" in o anki akışı ters bir noktada olmasından kaynaklanmaktadır. Örneğin, aşağıdaki "assembly" kodu "g_count" değişkeninin değerini bir arttıran kod olduğunu varsayalım: MOV reg, g_count INC reg MOV g_count, reg Burada yapılan şey ise şudur: -> "g_count" değişkeninin değeri "register" bölgesine çekiliyor. -> Daha sonra o değer bir arttırılıyor. -> Sonrasında bu yeni değer "register" bölgesinden tekrardan "g_count" değişkeninin adresine çekiliyor. İşte "quanta" süresi birinci aşamanın sonunda ve ikinci aşamanın başında, yani "g_count" değişkeninin değeri "register" bölgesine çekildikten hemen sonra ama oradaki değerin bir arttırılmasından evvel, bitmesi durumunda "g_count" değeri henüz bir arttırılmamış olacaktır. Yani, MOV reg, g_count ----> "quanta" süresi bitti. Bu "thread" artık bir süre CPU'da işletilmeyecektir. INC reg MOV g_count, reg Bu durumda diğer "thread" ise "g_count" değişkeninin bu "thread" tarafından güncellenmemiş değerini güncelleyecektir. Fakat bu "thread" tekrar çalışmaya başladığında, diğer "thread" tarafından güncellenmiş değer yerine, "register" bölgesine çekmiş olduğu değeri güncelleyecektir. Görüleceği üzere tek bir C deyimi kullanmamıza rağmen arka planda bir kaç adet "assembly" kodu çalıştırılmaktadır. Pekiyi bizler yukarıdaki bu problemi nasıl çözebiliriz? Bunun için iki yöntem vardır. Bunlar, >>> İşlemlerin atomik yapılması, yani tek hamlede yapılması. Yukarıdaki örneği ele alırsak arttırma işlemi için üç adet makina kodu çalıştırılmaktadır. Bu işi tek bir makina kodu ile yapmamız halinde sorunu çözmüş olacağız. Fakat bu örnek için böyle bir şey mümkün değildir. >>> Söz konusu işlemler sırasında "thread" ler arası geçiş olduğunda diğer "thread" in bekletilmesi, o "thread" in işleme konmaması. Böylelikle ilk "thread" yukarıdaki üç makina komutunu tamamladıktan sonra diğer "thread" işleme devam edecektir. Genellikle kullanılan yöntem de bu yöntemdir. Yani "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. Ancak ilk "thread" bu bölgeden çıktıktan sonra diğer "thread" in girmesine izin verilmektedir. Bu yöntemde devreye "Kritik Kod" kavramı girmektedir. >>>> "Kritik Kod" : Bir "thread" in akışı "Kritik Kod" bölgesine girdiğinde, "quanta" süresi dolmasına rağmen, başka bir "thread" in o bölgeye girmemesi demektir. Ta ki ilk "thread" in akışı "Kritik Kod" bölgesinden çıkana kadar. Burada "thread" in akışının kesilmesi engellenemez fakat başka "thread" lerin o bölgeye girmesi engellenebilir. Tabii ikinci "thread", ilk "thread" in akışının komple bitmesini değil sadece "Kritik Kod" alanından çıkmasını beklemektedir. Bir diğer deyişle öyle kodlardır ki bilinmeyen bir "t" anında sadece bir adet "thread" in işlettiği kodlardır. Pekiyi "Kritik Kod" alanları nasıl oluşturulur? Bu alanlar elle oluşturulamaz. Örneğin, aşağıdaki programda uygulanmaya çalışılan şey sonuçsuz kalacaktır: * Örnek 1, Aşağıdaki örnekte "g_flag" bayrağı "0" olduğunda "Kritik Kod" çalıştırılması ve "1" olduğunda çalıştırılmaması hedeflenmiştir. #include #include #include #include #include int g_count = 0; int g_flag = 0; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done => 1797364 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i) { while(g_flag == 1) ; ----> "quanta" süresi bitti. Bu "thread" artık bir süre CPU'da işletilmeyecektir. g_flag = 1; ++g_count; // Kritik Kod g_flag = 0; } return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i) { while(g_flag == 1) ; g_flag = 1; ++g_count; // Kritik Kod g_flag = 0; } return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Bu örnekte iki adet sıkıntılı nokta vardır. Yukarıda belirtilen noktada kesinti olduğunda, yani "while" bloğundan sonra fakat "g_flag" değişkenine "1" atanmadan evvel, yine yukarda verilen diğer örnekteki durum gerçekleşecektir. Öte yandan ilgili "thread" ler "while" içerisinde "busy loop" biçimde CPU zamanı harcamaktadır. Arzu edilen ise CPU zamanı harcamamasıdır. Öyleyse bizler "Kritik Kod" alanlarını nasıl oluşturabiliriz? Bu noktada artık işletim sistemi de mevzuya dahil olmaktadır. Artık genel ismi "Senkronizasyon Nesneleri" olan bir takım nesneler kullanılacaktır. Bu nesneler genel itibariyle "kernel moda" a geçiş yapabilmektedir. Çünkü "kernel mode" olarak atomik işlemler yapmak, işlemcilerin sağladığı bazı özel makina komutları ile mümkün olabilmektedir. Bazı senkronizasyon nesneleri ise daha az "kernel mode" a geçiş yaparak bu makina komutlarının da yardımıyla daha etkin işlev görmektedir. >>>>> "Senkronizasyon Nesneleri / mutex" : "Kritik Kod" alanı oluşturmak için yaygın kullanılan senkronizasyon nesnelerinden birisi "mutex" nesneleridir. "Mutual Exlusion" isminden türetilmiştir. Pek çok farklı işletim sistemlerinde benzer biçimde bulunmaktadır. Linux sistemlerinde ise senkronizasyon nesneleri, ismi "futex" olan, bir sistem fonksiyonu yoluyla gerçekleştirilmektedir. Dolayısıyla Linux sistemlerde kullanılan senkronizasyon nesneleri arka planda "futex" isimli sistem fonksiyonunu çağırmaktadır. "mutex" nesneleri "pthread_mutex_t" türü ile temsil edilmektedir. Diğer tür eş isimleri gibi "thread.h" ve/veya "sys/types.h" dosyaları içerisinde "typedef" edilmişlerdir. Herhangi bir türe karşılık gelebilir. Örneğin, Linux sistemlerinde bir "struct" türünün eş ismidir. Pekiyi bizler bu "mutex" nesnelerini nasıl kullanacağız? Şöyleki; >>>>>> Bu türden "global" bir nesne tanımlar. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { //... return 0; } //... >>>>>> Daha sonra iş bu "mutex" nesnesine tabiri caizse ilk değer vereceğiz. Burada iki farklı yöntemi izleyebiliriz; "PTHREAD_MUTEX_INITIALIZER" isimli makroyu kullanmak veya "pthread_mutex_init" fonksiyonunu kullanmak. >>>>>>> "PTHREAD_MUTEX_INITIALIZER" makrosu bir C makroosudur. Bu makro varsayılan özellikler ile ilk değer verilmesine olanak sağlar. * Örnek 1, //... pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; int main(int argc, char** argv) { //... return 0; } //... >>>>>>> "pthread_mutex_init" fonksiyonu aşağıdaki prototipe sahiptir: #include int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); Fonksiyonun birinci parametresi ilk değer verilecek "mutex" nesnesinin adresini, ikinci parametresi ise ilgili "mutex" nesnesinin özelliklerini içeren "özellik bilgisinin" adresini almaktadır. Bu ikinci parametreye daha sonra değinilecektir. Fakat bu ikinci adrese "NULL" değerinin geçilmesi durumunda varsayılan özellikler ile "mutex" nesnesi oluşturulur. Yani aslında "PTHREAD_MUTEX_INITIALIZER" makrosunun kullanılması gibi olacaktır. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda ise hata kodunun kendisine dönecektir. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); //... return 0; } //... Pekiyi hangi durumda makroyu hangi durumda da ilgili fonksiyonu çağırmalıyız? Eğer ilgili "mutex" nesnesi bir yapının içerisindeyse ve bu yapıyı da bizler dinamik bir biçimde tahsis edeceksek, "mutex" nesnesi için "pthread_mutex_init" fonksiyonunu kullanmalıyız. >>>>>> Öte yandan POSIX standartlarınca yukarıdaki iki yöntemden biri kullanılarak ilk değer verilen "mutex" nesnesi "pthread_mutex_destroy" fonksiyonu ile yok edilmelidir. Çünkü bazı sistemler ilgili "mutex" nesnesi içerisinde dinamik ömürlü nesneler oluşturabilir. İlgili fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_mutex_destroy(pthread_mutex_t *mutex); Fonksiyon, parametre olarak yok edilecek "mutex" nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine dönüş yapacaktır. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); //... if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } //... Pekiyi bizler yukarıdaki örneklerde "mutex" nesnesini oluşturduktan sonra "Kritik Kod" bölgesini nasıl oluşturacağız? Bu durumda bizler "pthread_mutex_lock" ve "pthread_mutex_unlock" fonksiyonları girmektedir. "Kritik Kod" bölgesinin başında "pthread_mutex_lock" fonksiyonu, sonunda ise "pthread_mutex_unlock" fonksiyonu çağrılmalıdır. Eğer bir "thread" in akışı ilk defa "pthread_mutex_lock" çağrısına denk gelirse, artık kullanılan "mutex" nesnesinin sahibi o "thread" e ait olacaktır. Bu noktada eğer başka "thread" lerin akışı da "pthread_mutex_lock" fonksiyonuna gelirse, o "thread" ler bloke edilecektir. Eğer ilgili "mutex" nesnesinin sahibi olan "thread" in akışı "pthread_mutex_unlock" fonksiyonuna gelirse, ilgili "mutex" nesnesinin sahipliği ortadan kaldırılacaktır. Eğer bu noktada birden fazla "thread" var ise uygun olan "thread", ilgili "mutex" nesnesinin sahipliğini alacaktır. Fakat bekleyen "thread" ler arasında seçim yapılırken "FIFO" uygulanmaz. İşletim sistemi adil bir şekilde davranmaya çalışır. Buradaki önemli nokta sahipliği alınan "mutex" nesnesi artık kilitlenmiş olmaktadır. Bu noktada diğer "thread" lerin akışı bloke edilir. Eğer sahiplik geri verilmişse diğer "thread" lerden uygun olan sahipliği alır. İşte sahipliği kazandıran fonksiyon "pthread_mutex_lock" fonksiyonuyken, sahipliği geri veren fonksiyon ise "pthread_mutex_unlock" fonksiyonudur. >>>>>> "pthread_mutex_lock" ve "pthread_mutex_unlock" fonksiyonları: #include int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); Bu iki fonksiyonun da argüman olarak "lock" edilecek ve "unlock" edilecek, yani sahiplini alınacak ve geri verilecek olan, "mutex" nesnesinin adresini almaktadır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine geri dönüş yaparlar. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); //... if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; for(int i = 0; i < 1000000; ++i) { /* * I. Bir "thread" in akışı buraya geldikten sonra artık * "g_mutex" nesnesinin sahibi artık o "thread" olur. Dolayısıyla * diğer "thread" lerin akışı bu noktaya geldiğinde eğer sahiplik * hala başka "thread" de ise o "thread" lerin akışları bloke * edilir. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; /* * Ancak ilgili "mutex" nesnesine sahip olan "thread" in akışı bu * fonksiyon çağrısından sonra, sahip olduğu "mutex" nesnesinin * kilidini kaldırır. */ if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } void* thread_proc2(void* param) { int result; for(int i = 0; i < 1000000; ++i) { /* * İlgili "g_mutex" nesnesi kilitlendiği için akışı buraya * gelen "thread" artık bloke edilir. Ta ki kilit kaldırılıncaya * kadar. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } //... Buradaki kilit nokta ortak kullanılan kaynak adedince "mutex" nesnesinin olması gerektiğidir. 3 farklı kaynak ortak olarak kullanılacaksa, üç adet "mutex" nesnesine ihtiyacımız vardır. * Örnek 1, Aşağıdaki programda ilgili "mutex" nesnesi sıra ile kilitlenmiştir. İlk önce hayata "tid1" ID numarasına sahip "thread" geldiği için ilgili "mutex" nesnesinin sahibi o olacaktır. Aşağıda da görüleceği üzere "Kritik Kod" bölgesinin tek bir noktada olması GEREKMEMEKTEDİR. Önemli olan aynı "mutex" nesnesi ile ortak bir alanda çalışma yapılıyor oluşudur. #include #include #include #include #include int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # Done => 2000000 */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; for(int i = 0; i < 1000000; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } void* thread_proc2(void* param) { int result; for(int i = 0; i < 1000000; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda ise "Kritik Kod" bölgesi tek bir noktada oluşturulmuştur. Buradaki önemli olan nokta ortak kullanılan kaynak bir adettir, yani "g_count" nesnesi. Dolayısıyla bizim bir adet "mutex" nesnesine ihtiyacımız vardır. O da "g_mutex" nesnesi. #include #include #include #include #include int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void increment_g_count(void); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { increment_g_count(); return NULL; } void* thread_proc2(void* param) { increment_g_count(); return NULL; } void increment_g_count(void) { int result; for(int i = 0; i < 9000000; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Buradaki en önemli bir diğer husus ise "mutex" sahipliğinin "thread" temelli olmasıdır. Yani bir "thread" bir "mutex" nesnesinin sahipliğini aldıktan sonra sadece kendisi geri verebilir. Başka "thread" ler bu sahipliği geri veremezler. Yani bir "mutex" nesnesi, sadece kendisini "lock" eden "thread" tarafından "unlock" edilebilir. Böylesi bir durumda "pthread_mutex_lock" fonksiyonu ya başarısız olur ya da "Tanımsız Davranış" a neden olur. Bu durum ise ilgili "mutex" nesnesinin özelliğine göre değişiklik göstermektedir. Yukarıdaki "mutex" fonksiyonlarına ek olarak bir takım yardımcı "mutex" fonksiyonları da mevcuttur. Bunlar, "pthread_mutex_trylock", "pthread_mutex_timedlock" vb. isimli fonksiyonlardır. Bunlardan, >>>>> "pthread_mutex_trylock" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_mutex_trylock(pthread_mutex_t *mutex); Bu fonksiyon yine argüman olarak kilitlenecek "mutex" nesnesinin adresini alır. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda ise hata kodu ile geri dönmektedir. Eğer ilgili "mutex" nesnesi başka bir "thread" tarafından kilitlenmiş ise bu fonksiyonu çağıran "thread" bloke olmaz ve "EBUSY" hata kodu ile geri döner. Dolayısıyla bizler aşağıdaki gibi bir kontrol mekanizması kullanmalıyız: if((result = pthread_mutex_trylock(&g_mutex)) != 0 && result == EBUSY) /* Başka şeyler yap... */ else exit_sys("pthread_mutex_trylock", result); Fakat bu fonksiyonu kullanırken ilgili "mutex" nesnesinin hala kilitli olup olmadığının sorgulanması biz programcılara bırakılmıştır. Yani otomatik olarak sorgulama YAPILMAMAKTADIR. Burada senaryoyu bizler oluşturmalıyız. >>>>> "pthread_mutex_timedlock" fonksiyonu, "pthread_mutex_lock" fonksiyonunun zaman aşımlı versiyonudur. Bu fonksiyon ile bir "mutex" nesnesi daha önce kilitlenmiş ise ilgili "mutex" nesnesi açılana kadar değil, bir müddet geçtikten sonra hala açılmamış ise geri dönülmesi sağlanmaktadır. Yani kilit açılana kadar değil, bir süre boyunca bu fonksiyonu çağıran "thread" bloke edilecektir. Fonksiyonun prototipi aşağıdaki gibidir; #include #include int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abstime); Fonksiyonun ilk parametresi yine kilitlenecek "mutex" nesnesinin adresini, ikinci parametresi ise 1.1.1970 tarihinden geçen mutlak zaman bilgisidir. Yani ilk önce şimdiki zamanı alıp, onun üzerine istenilen bekleme süresini ekledikten sonra ikinci parametresine geçeceğiz. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisini döndürmektedir. İlgili fonksiyonun kullanımı şöyledir; struct timespec ts; //... if(clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); ts.tv_sec += 5; if((result = pthread_timedlock(&g_mutex, &ts)) != 0 && result == ETIMEDOUT) /* İlgili "mutex" nesnesi beş saniye geçmesine rağmen hala kilitli olduğu için fonksiyon başarısız oldu. */ else exit_sys("pthread_timedlock", result); Şimdi de pekiştirici bir örnek yapalım: * Örnek 1, Aşağıdaki örnekte ilgili "mutex" elemanı KİLİTLENMEMİŞTİR. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void do_something(const char* name); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # thread_I: I. Step thread_II: I. Step thread_II: II. Step thread_I: II. Step thread_I: III. Step thread_I: IV. Step thread_II: III. Step thread_II: IV. Step thread_I: V. Step thread_II: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: III. Step --------------------- thread_II: I. Step thread_II: II. Step thread_II: III. Step thread_I: IV. Step thread_I: V. Step thread_II: IV. Step --------------------- thread_I: I. Step thread_II: V. Step thread_I: II. Step --------------------- thread_II: I. Step thread_I: III . Step thread_II: II. Step thread_I: IV. Step thread_II: III. Step thread_II: IV. Step thread_I: V. Step --------------------- thread_II: V. Step --------------------- */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, "thread_I")) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, "thread_II")) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void do_something(const char* name) { printf("%s: I. Step\n", name); usleep(rand() % 100); printf("%s: II. Step\n", name); usleep(rand() % 200); printf("%s: III. Step\n", name); usleep(rand() % 300); printf("%s: IV. Step\n", name); usleep(rand() % 400); printf("%s: V. Step\n", name); usleep(rand() % 500); puts("---------------------"); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise ilgili "mutex" nesnesi kilitlenmiştir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void do_something(const char* name); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # thread_II: I. Step thread_II: II. Step thread_II: IIII. Step thread_II: IV. Step thread_II: V. Step --------------------- thread_II: I. Step thread_II: II. Step thread_II: IIII. Step thread_II: IV. Step thread_II: V. Step --------------------- thread_II: I. Step thread_II: II. Step thread_II: IIII. Step thread_II: IV. Step thread_II: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: IIII. Step thread_I: IV. Step thread_I: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: IIII. Step thread_I: IV. Step thread_I: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: IIII. Step thread_I: IV. Step thread_I: V. Step --------------------- */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, "thread_I")) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, "thread_II")) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void do_something(const char* name) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); printf("%s: I. Step\n", name); usleep(rand() % 100); printf("%s: II. Step\n", name); usleep(rand() % 200); printf("%s: III . Step\n", name); usleep(rand() % 300); printf("%s: IV. Step\n", name); usleep(rand() % 400); printf("%s: V. Step\n", name); usleep(rand() % 500); puts("---------------------"); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte kullanılan "mutex" nesnesi kilitlenmemiştir. #include #include #include #include #include typedef struct tagNODE{ int val; struct tagNODE* next; }NODE; typedef struct tagLLIST{ size_t count; NODE* head; NODE* tail; pthread_mutex_t mutex; }LLIST; LLIST* create_list(void); NODE* add_item(LLIST* llist, int value); void walk_list(LLIST* llist); void destroy_list(LLIST* llist); void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # [12825] => 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 0 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 292 293 185 186 187 188 189 190 191 192 193 194 304 195 196 197 198 309 310 311 12 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 292 293 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 420 421 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 */ LLIST* llist; if((llist = create_list()) == NULL) { fprintf(stderr, "cannot create a linked list!...\n"); exit(EXIT_FAILURE); } int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("[%zd] => ", llist->count); walk_list(llist); destroy_list(llist); return 0; } LLIST* create_list(void) { LLIST* llist; if((llist = (LLIST*)malloc(sizeof(LLIST))) == NULL) return NULL; llist->count = 0; llist->head = NULL; llist->tail = NULL; return llist; } NODE* add_item(LLIST* llist, int value) { NODE* new_node; if((new_node = (NODE*)malloc(sizeof(NODE))) == NULL) /* Yeni bir düğüm oluşturduk. */ return NULL; if(llist->head == NULL) /* Eğer bağlı listede hiç düğüm yok ise ilk düğüm "new_node" olacaktır. */ llist->head = new_node; else /* Aksi halde son düğümün içindeki gösterici, "new_node" düğümünü gösterecektir. */ llist->tail->next = new_node; /* Son düğümün hangi düğüm olduğu bilgisi güncellendi. */ llist->tail = new_node; /* Yeni oluşturulan düğümün içindeki bilgi güncellendi. */ new_node->val = value; /* Bağlı listedeki düğüm adedi güncellendi. */ ++llist->count; return new_node; } void walk_list(LLIST* llist) { NODE* node; node = llist->head; /* Bağlı listenin ilk düğümünü temin ettik. */ /* * Son düğüme geldiğimiz zaman, son düğmün içindeki gösterici * "NULL" olacağından döngü sonra erecektir. */ while(node != NULL) { printf("%d ", node->val); fflush(stdout); node = node->next; } } void destroy_list(LLIST* llist) { NODE* node, *temp_node; node = llist->head; while(node != NULL) { temp_node = node->next; /* Silinecek düğümden bir sonraki düğümü saklıyoruz. */ free(node); /* Daha sonra silmek istediğimiz düğümü siliyoruz. */ node = temp_node; /* Daha sonra yukarıda sakladığımızı silmek istediğimizi belirtiyoruz. */ } free(llist); } void* thread_proc1(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item(llist, i) == NULL) { fprintf(stderr, "cannot allocate a linked list...\n"); exit(EXIT_FAILURE); } return NULL; } void* thread_proc2(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item(llist, i) == NULL) { fprintf(stderr, "cannot allocate a linked list...\n"); exit(EXIT_FAILURE); } return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Aşağıdaki örnekte üç adet "thread" oluşturulmuştur. #include #include #include #include #include #include typedef struct tagNODE{ int val; struct tagNODE* next; }NODE; typedef struct tagLLIST{ size_t count; NODE* head; NODE* tail; pthread_mutex_t mutex; }LLIST; LLIST* create_list(void); NODE* add_item(LLIST* llist, int value); NODE* add_item_head(LLIST* llist, int value); int walk_list(LLIST* llist); int destroy_list_thread_safe(LLIST* llist); void destroy_list(LLIST* llist); void* thread_proc1(void* param); void* thread_proc2(void* param); void* thread_proc3(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 ... = [20000] */ LLIST* llist; if((llist = create_list()) == NULL) exit_sys("create_list", errno); int result; pthread_t tid1, tid2, tid3; if((result = pthread_create(&tid1, NULL, thread_proc1, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid3, NULL, thread_proc3, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys("pthread_join", result); printf(" = [%zd]\n", llist->count); destroy_list(llist); puts("Ok"); return 0; } LLIST* create_list(void) { LLIST* llist; if((llist = (LLIST*)malloc(sizeof(LLIST))) == NULL) return NULL; llist->count = 0; llist->head = NULL; llist->tail = NULL; int result; if((result = pthread_mutex_init(&llist->mutex, NULL)) != 0) { free(llist); errno = result; /* "errno" değeri "thread" e özgüdür. */ return NULL; } return llist; } NODE* add_item(LLIST* llist, int value) { NODE* new_node; if((new_node = (NODE*)malloc(sizeof(NODE))) == NULL) /* Yeni bir düğüm oluşturduk. */ return NULL; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) { free(new_node); goto FAIL; } if(llist->head == NULL) llist->head = new_node; else llist->tail->next = new_node; llist->tail = new_node; new_node->val = value; ++llist->count; if((result = pthread_mutex_unlock(&llist->mutex)) != 0) { /* * Bu noktada bizler "new_node" düğümünü geri vermedik * çünkü ilgili düğüm halihazırda bağlı listeye dahil * edilmiştir. */ goto FAIL; } return new_node; FAIL: errno = result; /* "errno" değeri "thread" e özgüdür. */ return NULL; } NODE* add_item_head(LLIST* llist, int value) { NODE* new_node; if((new_node = (NODE*)malloc(sizeof(NODE))) == NULL) /* Yeni bir düğüm oluşturduk. */ return NULL; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) { free(new_node); goto FAIL; } if(llist->head == NULL) llist->tail = new_node; new_node->next = llist->head; llist->head = new_node; new_node->val = value; ++llist->count; if((result = pthread_mutex_unlock(&llist->mutex)) != 0) goto FAIL; return new_node; FAIL: errno = result; return NULL; } int walk_list(LLIST* llist) { NODE* node; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) { return errno = result; } node = llist->head; while(node != NULL) { printf("%d ", node->val); fflush(stdout); node = node->next; } if((result = pthread_mutex_unlock(&llist->mutex)) != 0) { return errno = result; } return 0; } int destroy_list_thread_safe(LLIST* llist) { NODE* node, *temp_node; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) goto FAIL; node = llist->head; while(node != NULL) { temp_node = node->next; free(node); node = temp_node; } if((result = pthread_mutex_unlock(&llist->mutex)) != 0) goto FAIL; if((result = pthread_mutex_destroy(&llist->mutex)) != 0) { free(llist); goto FAIL; } free(llist); return 0; FAIL: return errno = result; } void destroy_list(LLIST* llist) { NODE* node, *temp_node; node = llist->head; while(node != NULL) { temp_node = node->next; free(node); node = temp_node; } int result; if((result = pthread_mutex_destroy(&llist->mutex)) != 0) free(llist); free(llist); } void* thread_proc1(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item(llist, i) == NULL) exit_sys("add_item", errno); return NULL; } void* thread_proc2(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item_head(llist, i) == NULL) exit_sys("add_item", errno); return NULL; } void* thread_proc3(void* param) { LLIST* llist = (LLIST*)(param); walk_list(llist); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Anımsayacağınız üzere "mutex" nesnesine ilk değer verirken "mutex" özelliklerini es geçmiştik. Tıpkı "thread" nesnesinde yaptıklarımız gibi burada da ilk önce "mutex" özellik nesnesini oluşturup, onu "init" yapmalıyız. Daha sonra değiştirmek istediğimiz özellik var ise ilgili "set" fonksiyonlarını çağırıyoruz. En sonunda da bu özellik nesnesini kullanarak "mutex" nesnesini hayata getiriyoruz. >>>>>> "mutex" özellik nesnesinin tanımlanması ve "init" edilmesi: "mutex" özellik nesnesi "pthread_mutexattr_t" türünden bir nesnedir. Bu nesneye ilk değer vermek için de "pthread_mutexattr_init" fonksiyonunu çağırmamız gerekmektedir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_mutexattr_init(pthread_mutexattr_t *attr); Fonksiyon argüman olarak üzerinde işlem yapılacak olan "mutex" özellik nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine dönmektedir. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); //... return 0; } //... >>>>>> Bu aşamada "pthread_mutexattr_setXXX" biçiminde olan "set" fonksiyonları ile ilgili "mutex" özelliklerini değiştirebileceğiz. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); /* "mutex" özellik nesnesi bu aşamada "set" edildi. */ //... return 0; } //... >>>>>> Artık "mutex" nesnesi, ilgili "mutex" özellik nesnesi ile oluşturulabilir. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); /* "mutex" özellik nesnesi bu aşamada "set" edildi. */ if((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys("pthread_mutex_init", result); //... return 0; } //... >>>>>> "mutex" nesnesi hayata geldikten sonra ilgili "mutex" özellik nesnesini yok edebiliriz. Bunun için "pthread_mutexattr_destroy" fonksiyonunu çağırmamız gerekmektedir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); Fonksiyon argüman olarak üzerinde işlem yapılacak olan "mutex" özellik nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine dönmektedir. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); /* "mutex" özellik nesnesi bu aşamada "set" edildi. */ if((result = pthread_mutex_init(&g_mutex, mattr)) != 0) exit_sys("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys("pthread_mutexattr_destroy", result); //... return 0; } //... Pekiyi bizler "mutex" nesnesinin hangi özelliklerini değiştirebiliriz? Şöyle bir senaryo düşünelim; bir "thread", bir "mutex" nesnesinin sahipliğini almış olsun. Eğer aynı "thread", aynı "mutex" nesnesinin sahipliğini almak isterse şu üç farklı sonuçtan birisi meydana gelecektir: "thread" kendisini kilitler yani "deadlock" oluşur, "thread" ilgili "mutex" nesnesinin sahipliğini sorunsuz bir şekilde ikinci kez alır, halihazırda sahipliği alınan bir "mutex" nesnesinin sahipliği ikinci defa alınmak istendiğinde ilgili fonksiyon başarısız olur. İşte bu sonuç, "mutex" nesnesinin "type" özelliğine bağlıdır. Bu özellik ise "pthread_mutexattr_settype" isimli fonksiyon ile "set" edilir. >>>>>> "pthread_mutexattr_settype" fonksiyonu aşağıdaki prototipe sahiptir: #include int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); Fonksiyonun birinci parametresi ilgili "mutex" özellik nesnesinin adresini almaktadır. İkinci parametre ise şu değerlerden birisini alır: "PTHREAD_MUTEX_NORMAL", "PTHREAD_MUTEX_ERRORCHECK", "PTHREAD_MUTEX_RECURSIVE", "PTHREAD_MUTEX_DEFAULT". Bu parametrelerden, -> "PTHREAD_MUTEX_NORMAL" : Eğer bir "mutex" nesnesinin sahipliği ikinci kez alınmak istenirse, "deadlock" oluşmasına neden olur. -> "PTHREAD_MUTEX_ERRORCHECK" : Sahipliği alınmamış "mutex" nesnesinin sahipliğinin geri verilmesi ve ikinci kez bir "mutex" nesnesinin sahipliğinin alınmak istenmesi durumunda "pthread_mutex_lock" fonksiyonun başarısız olmasına neden olur. -> "PTHREAD_MUTEX_RECURSIVE" : Sahipliğini aldığı "mutex" nesnesinin tekrardan sahipliğini almasını sağlar fakat sahipliğini aldığı adet kadar sahipliği geri vermelidir. -> "PTHREAD_MUTEX_DEFAULT" : Bu durum varsayılan durumdur ve yukarıdaki üç durumdan birisidir. Linux ve türevi sistemlerde "PTHREAD_MUTEX_NORMAL" durumundadır. Fonksiyonun geri dönüş değeri başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisidir. * Örnek 1, Aşağıda varsayılan durum "mutex" özellik nesnesi kullanılmıştır ve "deadlock" meydana gelmiştir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void* thread_proc2(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void foo(void) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); puts("\n--------------"); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıda ise "PTHREAD_MUTEX_RECURSIVE" tür bilgisi olarak belirtilmiştir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # -------------- -------------- */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); if((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0) exit_sys("pthread_mutexattr_settype", result); if((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys("pthread_mutexattr_destroy", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void* thread_proc2(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void foo(void) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); puts("\n--------------"); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi de şöyle bir senaryo düşünelim; bir "thread", bir "mutex" nesnesinin sahipliğini aldıktan hemen sonra sonlanmaktadır. Bu durumda diğer "thread" ler sahiplik almak isterlerse ise ya "deadlock" oluşacak ya da bu sahiplik alma isteği başarısızlık olacaktır. İşte bu durumda ilgili "mutex" nesnesinin "robust" olma durumuna bakılır. Bu noktada da devreye "pthread_mutexattr_setrobust" fonksiyonu girmektedir. >>>>>> "pthread_mutexattr_setrobust" fonksiyonunun parametrik yapısı aşağıdaki gibidir: #include int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust); Fonksiyonun birinci parametresi ilgili "mutex" özellik nesnesinin adresini almaktadır. İkinci parametre ise şu değerlerden birisini almaktadır; "PTHREAD_MUTEX_STALLED" ve "PTHREAD_MUTEX_ROBUST" Bu parametrelerden, -> "PTHREAD_MUTEX_STALLED" : Bu durum varsayılan durumdur. Eğer başka bir "thread" ilgili "mutex" nesnesini tekrardan kilitlemeye çalışırsa "deadlock" oluşacaktır. -> "PTHREAD_MUTEX_ROBUST" : Bu durumda ise ilgili "mutex" nesnesini kilitlemeye çalışan fonksiyon başarısız olur. Şimdi böylesi bir "mutex" nesnesini kilitlemeye çalışan ilk "thread" için "pthread_mutex_lock" fonksiyonu "EOWNERDEAD" hata kodu ile geri dönmektedir. Diğer "thread" lerin yeniden bu "mutex" nesnesini kilitlemeye çalışması durumunda ise ilgili "pthread_mutex_lock" fonksiyonunun "EOWNERDEAD" hata kodu ile geri dönüp dönmemesi işletim sistemini yazanlara bırakılmıştır. POSIX standartları sadece ilk "thread" için garanti vermektedir. Fonksiyon başarı durumunda "0", başarısızlık durumunda "-1" ile geri dönecektir. * Örnek 1, Aşağıdaki programda "deadlock" gözlemlenmektedir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # */ int result; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void* thread_proc2(void* param) { sleep(1); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ilgili "mutex" nesnesi "robust" olarak oluşturulmuştur. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # pthread_mutex_lock : Owner died */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); if((result = pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST)) != 0) exit_sys("pthread_mutexattr_settype", result); if((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void* thread_proc2(void* param) { sleep(1); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi yukarıdaki senaryodaki "mutex" nesnesini tekrar kilitlenebilir kılmak için de "pthread_mutex_consistent" isimli fonksiyonu çağırmalıyız. >>>>>> "pthread_mutex_consistent" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_mutex_consistent(pthread_mutex_t *mutex); Fonksiyon parametre olarak tekrar kullanılabilir hale getirilecek "mutex" nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda hata kodunun kendisine dönecektir. Bir "mutex" nesnesi "consistent" duruma sokulursa, aynı zamanda kilitlenmiş de olur. Dolayısıyla aşağıdaki gibi örnek bir kullanım gözetilebilir: result = pthread_mutex_lock(&g_mutex); if(result == EOWNERDEAD) pthread_mutex_consistent(&g_mutex); else exit_sys("pthread_mutex_lock", result); /* "g_mutex" is locked now. */ Pekiyi "mutex" nesnelerinin prosesler arasındaki kullanımı nasıl olmaktadır? Anımsayacağımız üzere bu zamana kadar bizler aynı prosese ait "thread" leri senkronize etmiştik. Farklı proseslerdeki "thread" leri senkronize etmemiz için bizlerin "Paylaşılan Bellek Alanları" yöntemini kullanmamız gerekmektedir çünkü bu "mutex" nesnelerine isim verilemediği için "İsimli Boru Haberleşmesi" yöntemini kullanamayız. Yani "Paylaşılan Bellek Alanları" yöntemi bir nevi zorunluluk olmuş oluyor. Fakat bu da yeterli gelmemektedir; ilgili "mutex" nesnesini de "Paylaşılan Bellek Alanlarında" kullanılabilir hale getirmemiz gerekmektedir. Bunu da POSIX standartlarınca "mutex" özellik nesneleri ile gerçekleştiriyoruz. Bu işlemi de "pthread_mutexattr_setpshared" isimli fonksiyon ile gerçekleştirebiliriz. >>>>>> "pthread_mutexattr_setpshared" isimli fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); Fonksiyonun birinci parametresi "mutex" özellik nesnesinin adresini, ikinci parametresi ise ilgili "mutex" nesnesinin paylaşılabilirlik durumunu belirtmektedir. Bu ikinci parametre şu iki değerden birisini almaktadır; "PTHREAD_PROCESS_SHARED" ve "PTHREAD_PROCESS_PRIVATE". -> "PTHREAD_PROCESS_SHARED" : İlgili "mutex" nesnesi paylaşılabilir durumdadır. -> "PTHREAD_PROCESS_PRIVATE" : İlgili "mutex" nesnesi paylaşılamaz durumdadır. Varsayılan durum bu durumdur. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda hata kodunun kendisine geri dönmektedir. Buradan da anlaşılacağı üzere "mutex" nesnesinin prosesler arasında haberleşme için kullanılması biraz zahmetlidir. * Örnek 1, Aşağıdaki örnekte iki proses "mutex" ve "Paylaşılan Bellek Alanları" yöntemi kullanılarak senkronize bir biçimde haberleştirilmiştir. Burada ilk olarak "write" fonksiyonu, daha sonra "read" fonksiyonu çalıştırılmalıdır. Çünkü "mutex" nesnesinin ve "Paylaşılan Bellek Alanı" nesnesinin oluşturulması ve yok edilmesi "write" programı tarafından gerçekleştirilmektedir. Fakat unutmamalıyız ki "Paylaşılan Bellek Alanı" nesnesi "shm_unlink" fonksiyonu ile yok edilse bile gerçek manada yok edilmenin gerçekleşmesi için ilgili nesnenin başka prosesler tarafından kullanılmaması gerekmektedir. Aksi halde gerçek manada silme işlemi gerçekleşmeyecektir. Fakat "mutex" nesnesi için böyle bir şey söz konusu olmadığından, "mutex" nesnesini yok etmeden evvel, başka proseslerin iş bu "mutex" nesnesini kullanmadığından EMİN OLMALIYIZ. Örneğin, "Paylaşılan Bellek Alanı" nesnesi içinde ayrı bir sayaç daha tutulabilir ve bu sayacın değeri "0" olduğunda "mutex" nesnesi yok edilir. Tabii bu sayacın kontrolü de yine "Kritik Kod" alanında yapılmalıdır. Ek olarak "Condition Variables" denilen bir başka senkronizasyon nesnesini de kullanabiliriz. Fakat aşağıdaki programda karadüzen bir sistem kurulmuştur. Çünkü "Paylaşılan Bellek Alanı" nesnesi yok edilirken içerisindekiler de yok edilir. Son olarak aşağıdaki programları derlerken "-lrt -lpthread" seçeneklerini kullanmalıyız. /* sharing.h */ #ifndef SHARING_H_ #define SHARING_H_ #include typedef struct tagSHARED_INFO { int count; //... pthread_mutex_t mutex; }SHARED_INFO; #endif /* write */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_NAME "/shared_memory_for_mutex" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if((result = pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_mutexattr_setpshared", result); if((result = pthread_mutex_init(&shmaddr->mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); shmaddr->count = 0; if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); for(int i = 0; i < 1000000000; ++i) ++shmaddr->count; if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%d\n", shmaddr->count); printf("Press ENTER to exit!...\n"); getchar(); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_NAME "/shared_memory_for_mutex" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int result; if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); for(int i = 0; i < 1000000000; ++i) ++shmaddr->count; if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%d\n", shmaddr->count); printf("Press ENTER to exit!...\n"); getchar(); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde "mutex" nesneleri nispeten hızlı senkronizasyon araçlarıdır. Linux sistemlerinde ise senkronizasyon nesnelerinin bir çoğu "futex" denilen sistem fonksiyonunu kullanmaktadır. Öte yandan "mutex" fonksiyonları duruma göre "kernel mode" a geçmeden "user mode" olarak işlemlerini yapmaktadır. Şöyleki; -> "pthread_mutex_lock" fonksiyonu ile bir "mutex" nesnesini kilitlemek isteyelim. -> "mutex" nesnesi, kendi içerisinde bayrak değişkenleri tutarak, kilitleme işlemini atomik bir biçimde yapmak isteyecektir. Fakat atomikliği sağlamanın bir yolu ise "kernel mode" a geçerek, "CLI" gibi makine kodlarıyla kesme mekanizmasını kapatarak (yani "task-switch" mekanizmasını kapatarak), ilgili bayrak değişkenlerini "set" etmek de olabilir. Fakat "kernel mode" a geçmenin de maliyeti yüksektir. -> İşte yeni modern işlemciler "compare and set" gibi atomik makine kodları eklenmiştir. Bu makine komutları sayesinde bu tür bayrak değişkenleri "kernel mode" a geçmeden "user mode" da atomik bir biçimde "set" edilebilmektedir. Tabii burada bahsedilen durum kilitli olmayan bir "mutex" nesnesinin kilitlenmesi için geçerlidir. Eğer ilgili "mutex" nesnesi halihazırda kilitli ise "pthread_mutex_lock" fonksiyonu ilgili "thread" i bloke edecektir. Fakat burada da şöyle bir nüans vardır; ilgili "mutex" nesnesinin kilitli olduğunun görülmesi üzerine hemen bloke işlemi gerçekleştirilmez ve kısa bir müddet meşgul bir döngü içerisinde beklenir. Umulur ki beklenen bu zaman içerisinde kilit kaldırılır. Eğer bu süre sonunda kilit kalkmamış olursa ilgili "thread" bloke edilir. Buradaki meşgul bekleme durumuna da "spin lock" denilmektedir. İşte bu bloke işlemi için "kernel mode" a geçiş yapılması gerekmektedir... Dolayısıyla hiç "kernel mode" a geçmeden işlem yapmanın bir olanağı yoktur. >>>>> "Senkronizasyon Nesneleri / Conditional Variables" : Tıpkı "mutex" nesneleri gibi bu koşul nesneleri de birer senkronizasyon nesneleridir. Fakat "mutex" nesneleri gibi tek başlarına değil, bir "mutex" nesnesi ile birlikte kullanılmaktadırlar. Bu nesnelerin temel kullanım amacı, belli bir koşul sağlanana kadar, ilgili "thread" i bloke etmektir. Buradaki koşul programcı tarafından oluşturulmaktadır. Örneğin, global isim alanında belirtilen "g_count" değişkeninin değerinin sıfırdan büyük olması. Bu koşul değişkenleri nesnelerini kullanabilmek için belli yöntemlerin izlenmesi gerekmektedir. Şöyleki: >>>>>> "pthread_cond_t" türünden bir nesne hayata getiriyoruz. Bu tür "sys/types.h" içerisinde bir türe karşılık gelmektedir ve tipik olarak bir yapının tür eş ismidir. Tipik olarak bu nesne "global" isim alanı içerisinde tanımlanır ki diğer "thread" ler tarafından görülür olsun. >>>>>> Koşul değişkenleri nesnelerini de tıpkı "mutex" nesnelerinde olduğu gibi iki biçimde ilk değer verebiliriz; "PTHREAD_COND_INITIALIZER" makrosunun kullanılması veya "pthread_cond_init" fonksiyonunun kullanımı. Şimdi de bu kullanım alanlarını inceleyelim: >>>>>>> "PTHREAD_COND_INITIALIZER" makrosunun kullanımı aşağıdaki biçimdedir. * Örnek 1, //... pthread_cond_t g_cound = PTHREAD_COND_INITIALIZER; //... int main(int argc, char** argv) { //... return 0; } //... >>>>>>> "pthread_cond_init" fonksiyonu aşağıdaki prototipe sahiptir: #include int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t * attr); Fonksiyonun ilk parametresi ilk değer verilecek koşul değişkeni nesnesinin adresi, ikinci parametresi ise koşul değişkeni özellik nesnesidir. Bu özellik nesnesine daha sonra değinilecektir. Dolayısıyla bu nesneyi kullanmayacağımız için "NULL" değerini kullanacağız. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönecektir. Yine "mutex" nesnelerinde olduğu gibi bu fonksiyonun ikinci parametresine "NULL" değerini geçmekle yukarıdaki "PTHREAD_COND_INITIALIZER" makrosunun kullanımı arasında bir fark yoktur. >>>>>> Koşul değişkenleri nesnelerinin tek başlarına kullanılamayacağından bahsetmiştik. Şimdi de bu nesne ile birlikte kullanacağımız bir "mutex" nesnesini hayata getirmemiz gerekmektedir. >>>>>> Programcının artık bir koşul oluşturması gerekmektedir. Örneğin, "g_flag" değişkeninin "1" değerinde olması. Buradaki koşuldan kastedilen şey ilgili "thread" in bloke olmaması için gerekli olan durum manasındadır. Yukarıdaki örneği baz alırsak da "g_flag" değişkeninin değeri "1" değil ise ilgili "thread" bloke olacak, "1" olması durumunda bloke kalkacaktır. Bloke olmayı kötü bir şey olarak varsayarsak, koşulun sağlanmaması bir ceza olarak görebiliriz. Tipik olarak "global" isim alanındaki değişkenler koşul olarak kullanılırlar. Aşağıdaki kalıp bu duruma bir örnek gösterilebilir: * Örnek 1, //... pthread_mutex_lock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği alındı, yani kilitlendi. */ while( g_flag != 1 ) /* Buradaki koşul sağlanmadığı sürece aşağıdaki fonksiyon çağrılacaktır. */ pthread_cond_wait(&g_cond, &g_mutex); /* Koşul sağlanmadığı için bu fonksiyon ilgili "thread" i bloke edecektir. */ /* * Bu aşamada kritik kod * bölgesi bulunmaktadır. */ pthread_mutex_unlock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği geri verildi, yani kilit kaldırıldı. */ Pekiyi bu "pthread_cond_wait" fonksiyonu tam olarak ne iş görür? >>>>>>> "pthread_cond_wait" fonksiyonu, koşul değişkeni nesnesini bekleyen fonksiyondur. Prototipi aşağıdaki gibidir: #include int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex); Fonksiyonun birinci parametresi ilgili koşul değişkeni nesnesinin adresi, ikinci parametresi ise ilgili "mutex" nesnesinin adresini almaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. Bu fonksiyon ilgili "thread" nesnesini bloke etmektedir eğer koşul sağlanmamışsa. "thread" in akışı bu fonksiyona girdiğinde artık ilgili "mutex" nesnesinin kilidi kaldırılmakta, sahipliği geri verilmektedir. Buradaki bloke olma ve sahipliğinin geri verilmesi atomik bir olaydı. Şimdi burada iki farklı önemli nokta vardır: İlgili koşulun sağlanması ve bloke edilen ilgili "thread" in uyandırılması ya da blokesinin kaldırılması. Bu iki önemli nokta da diğer "thread" veya "thread" ler tarafından GERÇEKLEŞTİRİLİR. Diğer "thread" (ler) tarafından koşul uygun hale getirildikten sonra blokenin kalkması için iki farklı fonksiyon daha çağrılmalıdır. Bunlar "pthread_cond_broadcast" ve "pthread_cond_signal" isimli fonksiyonlardır. >>>>>>> Bu iki fonksiyonun da prototipi aşağıdaki gibidir: #include int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); Fonksiyon parametre olarak ilgili koşul değişken nesnesinin adresini almaktadır. Başarı durumunda "0" başarısızlık durumunda ise hata kodunun kendisine dönmektedir. Bu fonksiyonlardan "pthread_cond_broadcast" isimli fonksiyon ilgili koşul değişken nesnesini blokede bekleyen bütün "thread" leri uyandırırken, "pthread_cond_signal" fonksiyonu en az bir tanesini uyandırmaktadır. Genel olarak "pthread_cond_signal" fonksiyonu kullanılır. Şimdi yukarıdaki kalıbı tekrar inceleyelim: * Örnek 1, //... pthread_mutex_lock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği alındı, yani kilitlendi. */ while( g_flag != 1 ) /* Buradaki koşul sağlanmadığı sürece aşağıdaki fonksiyon çağrılacaktır. */ pthread_cond_wait(&g_cond, &g_mutex); /* * Koşul sağlanmadığı için bu fonksiyon ilgili "thread" i bloke edecek ve * ilgili "mutex" nesnesinin sahipliği de geri verilmiştir. Fakat bloke kaldırılırsa, * fonksiyondan çıkmadan hemen önce ilgili "mutex" nesnesini tekrar kilitlemeye * çalışmaktadır. Burada ilgili "mutex" nesnesi boş ise kilitleyecektir. Eğer * başka "thread" ler tarafından kilitlenmiş ise, varsayılan durumda, kilit * açılana kadar bizim "thread" yine bloke olacaktır. Bu son bloke işleminin * kalkması için artık ilgili "mutex" nesnesinin kilitlenebilir durumda olması * gerekmektedir. Bu noktada "pthread_cond_signal" veya "pthread_cond_broadcast" * fonksiyonlarının yeniden çağrılması lüzumsuzdur. */ /* * Bu aşamada kritik kod * bölgesi bulunmaktadır. */ pthread_mutex_unlock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği geri verildi, yani kilit kaldırıldı. */ Yukarıdaki kalıba göre bütün bunları özetleyecek olursak; -> İlgili koşul sağlanmamış ise ilgili "thread" in akışı "pthread_cond_wait" fonksiyonuna girecektir. -> İlgili "thread" in akışı "pthread_cond_wait" fonksiyonuna girince bloke olacak ve sahip olduğu ilgili "mutex" nesnesi geri verilecektir. -> Bu aşamada diğer "thread" ler tarafından koşul sağlanmalı ve "pthread_cond_broadcast" veya "pthread_cond_signal" fonksiyonlarına çağrı yapılmalıdır. Böylelikle ilgili "thread" in blokesi kalkacaktır. -> Uyanan/blokesi kalkan "thread", ilgili "mutex" nesnesinin sahipliğini geri almaya çalışacaktır. Bu noktada devreye "pthread_mutex_lock" fonksiyonu girecektir. Eğer ilgili "mutex" nesnesi başka "thread" tarafından kilitlenmişse, varsayılan durumda, kilidin kalkmasını bekleyecektir. Kilit kalktığında da bizim "thread" ilgili "mutex" nesnesini kilitleyecek ve "thread" in akışı "pthread_cond_wait" fonksiyonundan çıkacaktır. Eğer yukarıdaki koşul sağlanmadan "pthread_cond_broadcast" veya "pthread_cond_signal" fonksiyonlarına çağrı yapılırsa bloke yine kalkacak ve akış yine "pthread_mutex_lock" fonksiyonuna girecektir. Burada yine ilgili "mutex" nesnesi başka "thread" tarafından kilitlenmişse, varsayılan durumda, kilidin kalkmasını bekleyecektir. Kilit kalktığında da bizim "thread" ilgili "mutex" nesnesini kilitleyecek ve "thread" in akışı "pthread_cond_wait" fonksiyonundan çıkacaktır. Fakat KOŞUL SAĞLANMADIĞI İÇİN "thread" İN AKIŞI TEKRARDAN "pthread_cond_wait" FONKSİYONUNA GİRECEKTİR. -> Artık "thread" in akışı "Kritik Kod" bölgesine geçecektir. Buradaki en önemli nokta ilgili koşulun sağlanmasından sonra "pthread_cond_broadcast" veya "pthread_cond_signal" fonksiyonlarına çağrının yapılmasıdır. Eğer ilgili "mutex" nesnesi kilitlenebilir durumda ise yeniden bizim "thread" tarafından kilitlenecektir. Bu kilit ise en sonunda da "pthread_mutex_unlock" tarafından açılacaktır. Burada "pthread_mutex_lock" ve "pthread_mutex_unlock" fonksiyonlarına yapılan çağrının kesin sonuçları için ilgili fonksiyonların dökümanına bakmalıyız. Diğer "thread" ler bahsi geçen koşulu sağlamak ve uykudan uyandırmak için aşağıdaki kalıbı izleyebilir: * Örnek 1, //... pthread_mutex_lock(&g_mutex); g_flag = 1; /* Diğer "thread" in koşulu uygun hale gerekmektedir. */ pthread_mutex_unlock(&g_mutex); //... pthread_cond_signal(&g_cond); /* Daha sonra bloke edilen ilgili "thread" i uyandırması. */ //... >>>>>> Koşul nesnesinin kullanımı bittikten sonra "pthread_cond_destroy" fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi şu şekildedir: #include int pthread_cond_destroy(pthread_cond_t *cond); Fonksiyon parametre olarak koşul değişken nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönecektir. Aşağıdaki örnekte kullanıma ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte ilk olarak "Thread-1" çalışmaya başlayacaktır. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; int g_count = 0; int main(int argc, char** argv) { /* # OUTPUT # Thread-1 will handle the condition. Press ENTER to fix it!... Thread-2 may wait at the condition variable... Thread-2 will be locked... Thread-2 is waiting now... The condition is being established!... Thread-1 is unlocked... Thread-2 is unlocked... */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if((result = pthread_cond_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_cond_destroy", result); return 0; } void* thread_proc1(void* param) { puts("Thread-1 will handle the condition. Press ENTER to fix it!..."); getchar(); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); /* * Bu örnek için bu kodun kritik bölgeye alınmasına lüzum yoktur. * Çünkü sadece bir "thread" bu değişkeni değiştirmektedir. */ g_count = 1; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); /* * Aşağıdaki "signal" çağrısından evvel koşulun sağlanmış olması * gerekmektedir. Aksi halde ilgili "thread" tekrar uykuya dalacaktır. */ if((result = pthread_cond_signal(&g_cond)) != 0) exit_sys_errno("pthread_cond_signal", result); if(g_count == 1) puts("The condition is being established!..."); puts("Thread-1 is unlocked..."); } void* thread_proc2(void* param) { puts("Thread-2 may wait at the condition variable..."); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); puts("Thread-2 will be locked..."); while(g_count != 1) { puts("Thread-2 is waiting now..."); if((result = pthread_cond_wait(&g_cond, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); } if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); puts("Thread-2 is unlocked..."); return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Koşul değişkenlerinin kullanımına ilişkin bazı ayrıntılar da yok değildir. Şöyleki: >>>>>> "pthread_cond_signal" veya "pthread_cond_broadcast" fonksiyonlarına hiç çağrı yapılmamasına rağmen, bazı içsel nedenlerden dolayı, uykuya alınan "thread" ler kendiliğinden uyanmaktadır. Bu duruma "Spurious Wakeup" denmektedir. Döngü kullandığımız için ve koşul da sağlanmadığı için uyanan bu "thread" ler tekrar uyutulur. >>>>>> Anımsanacağınız üzere "pthread_cond_signal" fonksiyonu en az bir adet "thread" i uyandırmaktadır. Fakat bazen birden fazla "thread" de uyandırılmaktadır. Varsayalım iki adet "thread" uyandı. Böylesi bir durumda uyanan "thread" lerden birisi "pthread_cond_wait" fonksiyonundan çıkmadan evvel ilgili "mutex" nesnesini kilitleyecektir. Diğer ikinci "thread" ise bu aşamada ilgili "mutex" nesnesini beklemeye başlayacaktır. Sonrasında da ilgili "mutex" nesnesini kilitleyen "thread", "pthread_cond_wait" fonksiyonundan çıkıp ilgili Kritik Kod bölgesine geçecektir. En sonunda da ilgili "mutex" nesnesinin kilini tekrar kaldıracaktır. Bu noktada da "pthread_cond_wait" içerisinde bekleyen ikinci "thread" ilgili "mutex" nesnesini kilitleyip "pthread_cond_wait" fonksiyonundan çıkacak ve ilgili Kritik Kod bölgesine geçecektir. En sonunda da o da ilgili "mutex" nesnesinin kilidi kaldıracaktır. Bu durumunu engellemek ve ilgili "thread" leri teker teker uyandırıp "pthread_cond_wait" fonksiyonundan çıkmasını istiyorsak, ilgili Kritik Kod bölgesine koşulu eski haline getirecek bir kod parçacığı eklemeliyiz. Böylelikle koşul tekrar bozulacağı için, ekstradan uyanan diğer "thread" ler tekrar uykuya dalacaktır. >>>>>> Öte yandan birden fazla "thread", "pthread_cond_signal" ve "pthread_cond_broadcast" fonksiyonlarına çağrı yapar olsun. Koşul değişkenini bekleyen birden fazla "thread" uyanacaktır. Fakat bunlardan bir tanesi ilgili "mutex" nesnesinin sahipliğini alacak ve "pthread_cond_wait" fonksiyonundan çıkacaktır. Uyanan diğer "thread" ler ise "pthread_cond_wait" içerisinde ilgili "mutex" nesnesinin kilidinin açılmasını bekleyecektir. Eğer döngü kullanmazsak, ilgili "mutex" nesnesi boşa çıktıktan sonra, bekleyen bu "thread" lerden birisi de "pthread_cond_wait" fonksiyonundan çıkıp Kritip Kod bölgesine girecektir. Eğer Kritik Kod bölgesinde tüketilecek bir kaynak var ise ve bu kaynaklar da "pthread_cond_wait" fonksiyonundan çıkan ilk "thread" tarafından tüketilmişse, bir sorun meydana gelebilir. Bu duruma ise "Üretici-Tüketici Problemleri" denmektedir. >>>>>>> "Üretici-Tüketici Problemi" : En çok karşılaşılan senkronizasyon problemidir. Aynı prosesin "thread" lerin arasında karşımıza çıkabileceği gibi farklı proseslerin "thread" leri arasında da karşımıza çıkabilmektedir. Bu problemde en az bir üretici "thread" ve en az bir tüketici "thread" vardır. Üretici "thread", bir takım işlemlerden sonra bir değer üretir ve bu değerin işlenmesi için de bu değeri tüketici "thread" e havale eder. Buradaki havale edilme işlemi paylaşılan bir alan vesilesi ile gerçekleştirilir. Örneğin, prosesler arasında kullanılacak ise Paylaşılan Bellek Alanları, aynı proses içerisinde kullanılacak ise "global" isim alanı kullanılır. Yani üretici "thread" bir değer üretip bunu paylaşılan alana yazmak, tüketici "thread" ise bu alana yazılan değeri alarak işlemektir. Buradaki önemli husus üretici "thread" in ilgili paylaşılan alandaki değeri ezmemesi, yani üzerine yazmaması, gerekirken tüketici "thread" in aynı değeri iki defa alıp işlememesi gerekmektedir. Eğer bu iki "thread" arasında bir hız farkı olursa ya aynı değer iki defa işlenecek ya da önceki değerin üzerine yeni değer yazılacağı için değer kaçırılacaktır. Bu problemin amacı ise hız kazanmaktır. Koordineli bir biçimde, bir yandan üretim yapılırken bir yandan da tüketim yapılmasıdır. Eğer eş zamanlı olarak bu işlemler yapılmasaydı, yani seri bir biçimde yapılsaydı, ortada bir gecikme oluşacaktır. Eğer tek bir CPU da olsa birden fazla "thread" kullanılması yine hız kazandıracaktır. Öte yandan bu problemde iki "thread" arasında kullanılan paylaşılan alan bir "FIFO" kuyruk sistemi olursa, üretici ve tüketici "thread" ler birbirlerini daha az bekleyecektir. Kuyruk tam dolduğunda tüketici beklerken, kuyruk tam boşaldığında tüketici bekleyecektir. Bir diğer yandan bu problemde kullanılan "thread" ler birden fazla da olabilmektedir. Yani çok üretici ve çok tüketici aynı paylaşılan alanı kullanmaktadır. Bu problem ile gerçek hayatta çok yerde karşılaşılmaktadır. Örneğin, "Client-Server" uygulamalar. Birden fazla "Client" üretim yaparken, birden fazla "Server" tüketim yapmaktadır. * Örnek 1, Aşağıdaki örnekte iki proses arasında koordinasyon sağlanmadığı için bazı değerler kaçırılmış, bazı değerler iki defa işlenmiştşir. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int g_shared; int main(int argc, char** argv) { /* # OUTPUT # 0 0 1 2 3 4 7 8 8 9 10 10 12 13 14 15 16 19 20 21 22 24 24 25 */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ g_shared = val; if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; for(;;) { val = g_shared; usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } "Üretici-Tüketici Problemleri" tipik olarak semaforlar ve koşul değişkenleri kullanılarak çözülmektedir. Şimdilik koşul değişkenleri ile kullanımı üzerinde durup, semaforlar ile birlikte kullanımına ileride değineceğiz. Koşul değişkenleri ile bu problemin çözümüne ilişkin kodlar kabaca aşağıdaki gibidir: //... int g_flag = 0; pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER; pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER; //... /* "Producer thread" */ for(;;) { /* Bir değer üretilir. */ pthread_mutex_lock(&g_mutex); while(g_flag == 1) pthread_cond_wait(&cond_producer, &g_mutex); /* Üretilen değer paylaşılan alana yerleştirilir. */ g_flag = 1; pthread_mutex_unlock(&g_mutex); pthread_cond_signal(cond_consumer); } /* "Consumer thread" */ for(;;) { pthread_mutex_lock(&g_mutex); while(g_flag == 0) pthread_cond_wait(&cond_consumer, &g_mutex); /* Paylaşılan alandaki değer alınır. */ g_flag = 0; pthread_mutex_unlock(&g_mutex); pthread_cond_signal(cond_producer); } Aşağıda bu konuya ilişkin bir örnek verilmiştir; Aşağıdaki örnekte iki proses arasında koordinasyon sağlandığı için artık hiç değer kaçırılmamıştır. Buradaki koordinasyon koşul değişkenleri ile sağlanmıştır. Kritik Kod bölgesinin kısa tutulması, sadece gerekli kısımların içeri alınması iyi bir tekniktir. * Örnek 1, Aşağıdaki örnekte iki "thread" için oluşturulan ortak alan bir nesnedir. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); pthread_cond_t g_cond_producer; pthread_cond_t g_cond_consumer; pthread_mutex_t g_mutex; int g_flag = 0; int g_shared; int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 */ int result; if((result = pthread_cond_init(&g_cond_producer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_cond_init(&g_cond_consumer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; int result; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_flag == 1) if((result = pthread_cond_wait(&g_cond_producer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); g_shared = val; g_flag = 1; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; int result; for(;;) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_flag == 0) if((result = pthread_cond_wait(&g_cond_consumer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = g_shared; g_flag = 0; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte iki "thread" için oluşturulan ortak alan bir kuyruk sisetmidir. Böylelikle üretici "thread" kuyruk tam doluyken, tüketici "thread" ise kuyruk tam boşken bekleyecektir. Bu da bekleme olasılıklarını düşürecektir. Burada kullanılan kuyruk sistemi yine çeşitli biçimlerde gerçekleştirilebilir. En çok kullanılan yöntem, "circual queue" de denilen, Döngüsel Kuyruk Sistemidir. Böylesi bir sistemde bir dizi oluşturulur. Daha sonra "head" ve "tail" diyeceğimiz iki adet indeks ya da gösterici ilgili dizinin sırasıyla başını ve sonunu tutar. Kuyruğa eleman eklenirken "tail", kuyruktan eleman alınırken "head" isimli indeks/gösterici ötelenir. Eğer "head" ve "tail" ilgili dizinin sonuna gelirse, dizinin tekrar başına alınır. Böylelikle sanki bir "ring" sistemi varmış gibi gözükür. Son olarak dizideki eleman sayısını da yine bir sayaçta tutarak dizideki toplam eleman sayısının bilgisi saklanır. #include #include #include #include #include #include #define QUEUE_SIZE 25 void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); pthread_cond_t g_cond_producer; pthread_cond_t g_cond_consumer; pthread_mutex_t g_mutex; int g_queue[QUEUE_SIZE]; int g_head = 0; int g_tail = 0; int g_count = 0; int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 */ int result; if((result = pthread_cond_init(&g_cond_producer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_cond_init(&g_cond_consumer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; int result; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_count == QUEUE_SIZE) /* Kuyruk tam dolduğunda üretici bekleyecektir. */ if((result = pthread_cond_wait(&g_cond_producer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); g_queue[g_tail++] = val; /* * Eğer "g_tail" in değeri 'g_tail % QUEUE_SIZE' işleminin sonucudur. * Böylelikle dizinin sonuna geldikten sonra tekrardan dizinin başına geçiyoruz. */ g_tail %= QUEUE_SIZE; ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; int result; for(;;) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_count == 0) /* Kuyruk tam boşken tüketici bekleyecektir. */ if((result = pthread_cond_wait(&g_cond_consumer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = g_queue[g_head++]; /* * Eğer "g_head" in değeri 'g_head % QUEUE_SIZE' işleminin sonucudur. * Böylelikle dizinin sonuna geldikten sonra tekrardan dizinin başına geçiyoruz. */ g_head %= QUEUE_SIZE; --g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>> "pthread_cond_signal" ve "pthread_cond_broadcast" isimli fonksiyonlar, o an uykuda olan "thread" leri uyandırırlar. Yani bu fonksiyonlar çağrıldıdığında uyuyan "thread" yok ise boş yere çağrılmış olacaktır. >>>>>> Öte yandan "pthread_cond_signal" ve "pthread_cond_broadcast" isimli fonksiyonlar ile bir "thread" uyansa fakat kilitlenecek "mutex" nesnesi halihazırda kilitliyse, uyanan bu "thread" ilgili "mutex" nesnesinin kilidi açılana kadar bloke edilir. Kilit kaldırılınca uyanan bu "thread" ilgili "mutex" nesnesinin sahipliğini alır ve "pthread_cond_wait" fonksiyonundan çıkar. Dolayısıyla "pthread_cond_signal" ve "pthread_cond_broadcast" fonksiyonlarını ilgili "mutex" nesnesi kilitlenebilir durumdayken çağrılması gerekmektedir. Koşul değişkenlerinin zaman aşımlı beklemeye yol açan bir başka biçimi daha vardır. Bu, "pthread_cond_timedwait" isimli fonksiyon ile gerçekleştirilmektedir. Belli bir zaman aşımı dolunca ilgili koşul değişkeni otomatik olarak açılır. İş bu fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_cond_timedwait(pthread_cond_t * cond, pthread_mutex_t * mutex, const struct timespec * abstime); Yine burada üçüncü parametre ile belirtilen zaman aşımı değeri MUTLAKTIR, GÖRELİ DEĞİLDİR. Yine bu fonksiyon da atomik bir biçimde ilgili "mutex" nesnesinin sahipliğini bırakır ve çıkışta yine "mutex" in sahipliğini almaya çalışır. Yani buradaki bekleme süresi, belirtilen zaman kadar olacaktır. Dolayısıyla bu fonksiyon zaman aşımı gerçekleştiğinde "ETIMEDOUT" değerine geri dönmektedir. Aksi halde diğer hata kodlarına geri dönmektedir. Bu da demektir ki başarısızlık durumunda hata kodunun "ETIMEDOUT" olup olmadığı yine kontrol edilmelidir. Öte yandan koşul değişkenleri de özellik parametresine sahiptir, tıpkı "mutex" nesneleri gibi. Fakat koşul değişkenlerinin değiştirilebilir iki adet özelliği vardır. Yine bu özellikleri "set" etmek, "mutex" nesnelerinin özelliklerini "set" etmek gibidir. Yani aynı yöntem kullanılır. Şöyleki; -> İlk olarak "pthread_condattr_t" türünden bir özellik nesnesi bildirir. -> Daha sonra "pthread_condattr_init" ile bu nesneye ilk değerini verir. İlgili fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_condattr_init(pthread_condattr_t *attr); -> Daha sonra "pthread_condattr_setXXX" fonksiyonları ile ilgili özellik nesnesinin bazı özelliklerini "set" eder. -> Sonrasında da "pthread_cond_init" fonksiyonuna bu özellik nesnesinin adresi geçilir ki ilgili özelliklere bahsi geçen koşul değişkeni haiz olsun. -> Son olarak ilgili özellik nesnesinin "pthread_cond_init" fonksiyonundan sonra korunmasına gerek yoktur. Bu nedenle "pthread_condattr_destroy" fonksiyonu ile boşaltılabilir. Bu fonksiyonun da prototipi aşağıdaki gibidir: #include int pthread_condattr_destroy(pthread_condattr_t *attr); Pekiyi bizler hangi özellikleri "set" edebiliriz? İki adet özelliği "set" edebiliriz. >>>>>> Bunlardan bir tanesi ilgili koşul nesnesinin prosesler arasında kullanımını mümkün kılan özelliktir. Tabii ilgili koşul nesnesinin paylaşılan bellek alanında oluşturulması gerekmtekdir. Tabii bu durumda "mutex" nesnesinin de yine paylaşılan bellek alanında oluşturulması gerekmtekdir. Bu işlemler için "pthread_condattr_setpshared" fonksiyonu ile mümkündür. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared); Fonksiyonun ilk parametresi ilgili koşul nesnesine ait olan özellik nesnesinin adresini, ikinci parametre ise şu değerlerden birisini almaktadır; "PTHREAD_PROCESS_SHARED" ve "PTHREAD_PROCESS_PRIVATE". -> "PTHREAD_PROCESS_SHARED" : -> "PTHREAD_PROCESS_PRIVATE" : Varsayılan değer budur. Aşağıda örnek bir kullanım verilmiştir: * Örnek 1, Aşağıdaki örnekte "üretici-tüketici" problemi prosesler arasında işlenmiştir. Paylaşılan Bellek Alanı kullanılmıştır. Programlardan ilk önce "Producer", daha sonra "Consumer" isimli program çalıştırılmalıdır. /* sharing.h */ #ifndef PRODCONS_H_ #define PRODCONS_H_ #include #define QUEUE_SIZE 25 #define SHM_NAME "/producer-consumer" /* * Paylaşılan bellek alanına yazılacak nesne aşağıdaki nesnedir. */ typedef struct tagSHARED_INFO{ pthread_cond_t cond_producer; pthread_cond_t cond_consumer; pthread_mutex_t mutex; int head; int tail; int queue[QUEUE_SIZE]; int count; }SHARED_INFO; #endif /* Producer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if((result = pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_mutexattr_setpshared", result); if((result = pthread_mutex_init(&shmaddr->mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); pthread_condattr_t cattr; if((result = pthread_condattr_init(&cattr)) != 0) exit_sys_errno("pthread_condattr_init", result); if((result = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_condattr_setpshared", result); if((result = pthread_cond_init(&shmaddr->cond_producer, &cattr)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_cond_init(&shmaddr->cond_consumer, &cattr)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_condattr_destroy(&cattr)) != 0) exit_sys_errno("pthread_condattr_destroy", result); shmaddr->count = 0; shmaddr->head = 0; shmaddr->tail = 0; int val = 0; for(;;) { usleep(rand() % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(shmaddr->count == QUEUE_SIZE) /* Kuyruk tam dolduğunda üretici bekleyecektir. */ if((result = pthread_cond_wait(&shmaddr->cond_producer, &shmaddr->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); shmaddr->queue[shmaddr->tail++] = val; /* * Eğer "g_tail" in değeri 'g_tail % QUEUE_SIZE' işleminin sonucudur. * Böylelikle dizinin sonuna geldikten sonra tekrardan dizinin başına geçiyoruz. */ shmaddr->tail %= QUEUE_SIZE; ++shmaddr->count; if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&shmaddr->cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == QUEUE_SIZE) break; ++val; } /* Alternatif - I */ // puts("Press ENTER to finish the communication!..."); getchar(); /* Alternatif - I */ /* Alternatif - II */ if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(shmaddr->count != 0) if((result = pthread_cond_wait(&shmaddr->cond_producer, &shmaddr->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); /* Alternatif - II */ if((result = pthread_cond_destroy(&shmaddr->cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&shmaddr->cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_mutex_destroy(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int val; int result; for(;;) { usleep(rand() % 300000); if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(shmaddr->count == 0) if((result = pthread_cond_wait(&shmaddr->cond_consumer, &shmaddr->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = shmaddr->queue[shmaddr->head++]; shmaddr->head %= QUEUE_SIZE; --shmaddr->count; printf("%d ", val); fflush(stdout); if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&shmaddr->cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == QUEUE_SIZE) break; } puts(""); /* Alternatif - II */ if((result = pthread_cond_signal(&shmaddr->cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); /* Alternatif - II */ if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>> Bir diğer "set" edilebilir özellik ise "pthread_cond_timdwait" fonksiyonunda zaman aşımında kullanılan saatin cinsidir. Bunun için "pthread_condattr_setclock" isimli fonksiyon kullanılır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_condattr_setclock(pthread_condattr_t *attr, clockid_t clock_id); Fonksiyonun ilk parametresi ilgili koşul nesnesine ait olan özellik nesnesinin adresini, ikinci parametre ise ilgili saat nesnesinin ID'sidir. Varsayılan değer "system clock" değeridir. >>>>> "Senkronizasyon Nesneleri / Semafor" Nesneleri: Sayaçlı senkronizasyon nesneleridir. Bu nesnelerin amacı bir Kritik Kod bölgesine en fazla "n" tane "thread" in aynı anda girmesini sağlamaktır. İşte bünyelerindeki sayaç, bu "thread" lerin adedini tutmaktadır. Bu nesnelerin en önemli kullanım sebebi "n" tane kaynağı "k" adet kullanıcıya dağıtarak, kaynak paylaştırmak için kullanılır. "n" taneden daha fazla "thread" Kritik Kod bölgesine girmek isterse, bu "thread" ler blokede bekletilecektir. Burada "n" değerinin "1" olması durumunda ilgili semafor nesneleri "Binary Semaphore" olarak adlandırılır ve "mutex" nesneleri gibi bir etkiye sahiptir. Fakat "mutex" nesnelerinden farkı, sahiplik ile ilgilidir. Anımsanacağınız üzere "mutex" nesnesinin sahipliğini alan "thread" bu sahipliğini bırakabilir. Fakat semafor nesnelerinde böyle bir durum yoktur. Diğer "thread" ler içerideki iş bu sayaç değerini değiştirebilir. Bundan dolayıdır ki hataya açık nesneler olarak da görülebilir. Semafor nesneleri de bünyesinde iki farklı arayüz bulundurur. Bunlar "POSIX" ve "System 5" semafor nesneleridir. Tıpkı Paylaşılan Bellek Alanları ve Mesaj Kuyrukları gibi. Bundan dolayıdır ki semafor nesneleri de "IPC" konusu ile ilişkilidir. Buradaki arayüzlerden "System 5" arayüz oldukça kötü tasarlanmıştır. Fakat "POSIX" arayüzü ile bu durum düzeltilmiştir. Dolayısıyla ilk olarak "POSIX", daha sonra "System 5" arayüzü işlenecektir. Tavsiye edilen arayüz ise "POSIX" arayüzüdür. >>>>>> "POSIX" semafor nesneleri: İsimli ve isimsiz olarak iki farklı biçimdedir. İsimli olanlar prosesler arasında kullanıma daha uygunken, isimsiz olanlar aynı prosesin "thread" ler arasında kullanıma uygundur. Fakat yine farklı prosesler arasında da kullanılabilmektedir. >>>>>>> İsimsiz "POSIX" nesneleri: "semt_t" türü ile temsil edilmektedir. Herhangi bir türe karşılık gelebilir. Bu türden nesneleri kullanabilmek için aşağıdaki prosedürü takip etmeliyiz: -> "sem_t" türünden bir nesne bildiririz. -> "sem_init" isimli fonksiyon ile bu nesneye ilk değer veririz. Fonksiyonun prototipi aşağıdaki gibidir: #include int sem_init(sem_t *sem, int pshared, unsigned value); Fonksiyonun birinci parametresi "sem_t" türünden nesnenin adresini, ikinci parametre ise ilgili Semafor nesnesinin prosesler arasında paylaşılıp paylaşılamayacağı bilgisini alır. "0" değeri geçilmesi durumunda prosesler arasında PAYLAŞIMI OLMAYACAĞI, "non-zero" bir değer ise prosesler arasında PAYLAŞILACAĞINI belirtir. Üçüncü parametre ise semafor nesnesi içindeki sayacın değerini belirtir. Bu değer Kritik Kod bölgesine aynı anda en fazla kaç "thread" in gireceğini belirtir. Fonksiyon, başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. Son olarak her ne kadar isimsiz semafor nesneleri de prosesler arasında kullanılabilir olsalarda, isimli semafor nesneleri bu iş için genellikle daha uygundur. -> Daha sonra Kritik Kod bölgesi oluşturulur. Bu sefer "sem_wait" ve "sem_post" isimli fonksiyonlar kullanılır ve ilgili kod parçacıkları bu iki fonksiyon çağrısı arasında yazılır. Bu iki fonksiyon şu şekilde çalışmaktadır; diyelim ki semafor nesnesinin içerisindeki sayaç "3" olsun. Bir "thread" Kritik Kod alanına girerken akışı "sem_wait" fonksiyonundan da geçecektir. Bu durumda semafor nesnesinin sayacı bir azaltılacaktır. İkinci bir "thread" de aynı kritik kod bölgesine girmek istediğinde, onun da akışı yine "sem_wait" fonksiyonundan geçecektir ve semafor nesnesinin içindeki sayaç bir azaltılacaktır. Üçüncü bir "thread" de yine aynı kritik kod bölgesinden geçerken benzer durum yaşanacaktır. Fakat semafor nesnesinin sayacı "0" olduğundan dolayı dördüncü ve sonrasındaki "thread" ler blokede bekletilecektir. Benzer şekilde kritik kod bölgesinde işi biten "thread" lerin akışı da "sem_post" fonksiyonundan geçecektir ve semafor nesnesinin sayacı "1" olacaktır. İşte tam da bu anda dışarıdaki blokede bekleyen "thread" lerden birisi kritik kod bölgesine girecektir. Onun da akışı "sem_wait" içerisinden geçtiği için semafor nesnesi içindeki sayacın değeri tekrar "0" olacaktır. Bir nevi berber koltuğu bekleyen müşterileri bu duruma benzetebiliriz. Dolayısıyla belli bir kaynağın belli "thread" lere paylaştırılması mottosu güdülmüştür. İlgili fonksiyonların prototipi aşağıdaki gibidir: #include int sem_wait(sem_t *sem); int sem_post(sem_t *sem); Fonksiyonlar argüman olarak ilgili semafor nesnesinin adresini alırlar. Başarı durumunda "0", hata durumunda ise "-1" değerine geri dönerler ve "errno" değişkeni uygun değere çekilir. Son olarak tekrar hatırlatmakta fayda vardır ki "sem_wait" fonksiyonuna girmek için blokede bekleyen "thread" lerden hangisinin ilk önce gireceğine dair bir garanti POSIX standartlarınca verilmemiştir. Burada "sem_post" yapmak için "sem_wait" yapmaya GEREK YOKTUR. "sem_wait" fonksiyonunun birde "sem_timedwait" isimli zaman aşımlı bir versiyonu daha vardır. Bu versionda, semafor nesnesinde bloke olunmuşsa, zaman aşımı dolduğu vakit bloke kaldırılmaktadır. Fonksiyonun prototipi şöyledir; #include #include int sem_timedwait(sem_t * sem, const struct timespec * abstime); Ancak buradaki zaman aşımı göreli değil, mutlak zamanı belirtmektedir. Fonksiyon eğer zaman aşımından dolayı başarısız olmuşsa "-1" ile geri döner ve "errno" değişkeni "ETIMEDOUT" değerini alır. -> Semafor nesnesinin kullanımı bittikten sonra "sem_destroy" ile boşa çıkartılmalıdır. Fonksiyonun prototipi şu şekildedir: #include int sem_destroy(sem_t *sem); Fonksiyonlar argüman olarak ilgili semafor nesnesinin adresini alırlar. Başarı durumunda "0", hata durumunda ise "-1" değerine geri dönerler ve "errno" değişkeni uygun değere çekilir. Aşağıda örnek bir kullanım verilmiştir: * Örnek 1, Aşağıdaki semafor nesnesinin kullanımına bir örnek verilmiştir. #include #include #include #include #include #include #include void* thread_1(void* param); void* thread_2(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); #define SIZE 10 sem_t g_sem; int g_count; int main(int argc, char** argv) { /* # OUTPUT # 20 */ int result; pthread_t tid1, tid2; if((sem_init(&g_sem, 0, 1)) == -1) exit_sys("sem_init"); if((result = pthread_create(&tid1, NULL, thread_1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); printf("%d\n", g_count); if(sem_destroy(&g_sem) == -1) exit_sys("sem_destroy"); return 0; } void* thread_1(void* param) { for(int i = 0; i < SIZE; ++i) { if(sem_wait(&g_sem) == -1) exit_sys("sem_wait"); ++g_count; if(sem_post(&g_sem) == -1) exit_sys("sem_post"); } return NULL; } void* thread_2(void* param) { for(int i = 0; i < SIZE; ++i) { if(sem_wait(&g_sem) == -1) exit_sys("sem_wait"); ++g_count; if(sem_post(&g_sem) == -1) exit_sys("sem_post"); } return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte birden fazla "thread", "3" adet kaynağa atanmıştır. Böylelikle aynı anda en fazla üç adet "thread" in çalıştığı, diğer "thread" lerin ise uykuda beklediği gözlemlenmektedir. #include #include #include #include #include #include #include #define EPOCH 10 #define NTHREADS 10 #define NRESOURCES 3 typedef struct tagTHREAD_INFO{ unsigned int seed; pthread_t tid; char name[32]; }THREAD_INFO; typedef struct tagRESOURCES{ int useflags[NRESOURCES]; pthread_mutex_t mutex; sem_t sem; }RESOURCES; void assign_resource(THREAD_INFO* ti); void do_with_resources(THREAD_INFO* ti, int nresource); void* thread_1(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); RESOURCES g_resources; int main(int argc, char** argv) { /* # OUTPUT # */ THREAD_INFO* threads_info[NTHREADS]; srand(time(NULL)); for(int i = 0; i < NRESOURCES; ++i) g_resources.useflags[i] = 0; if((sem_init(&g_resources.sem, 0, NRESOURCES)) == -1) exit_sys("sem_init"); int result; if((result = pthread_mutex_init(&g_resources.mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); for(int i = 0; i < NTHREADS; ++i) { if((threads_info[i] = (THREAD_INFO*)malloc(sizeof(THREAD_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } snprintf(threads_info[i]->name, 32, "Thread-%2d", i + 1); threads_info[i]->seed = rand(); if((result = pthread_create(&threads_info[i]->tid, NULL, thread_1, threads_info[i])) != 0) exit_sys_errno("pthread_create", result); } for(int i = 0; i < NTHREADS; ++i){ if((result = pthread_join(threads_info[i]->tid, NULL)) != 0) exit_sys_errno("pthread_join", result); free(threads_info[i]); } if((result = pthread_mutex_destroy(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if(sem_destroy(&g_resources.sem) == -1) exit_sys("sem_destroy"); return 0; } void assign_resource(THREAD_INFO* ti) { /* En fazla 'n' tane "thread" in Kritik Kod bölgesine girmesine olanak sağlıyor. */ if(sem_wait(&g_resources.sem) == -1) exit_sys("sem_wait"); /* * Buradaki "mutex" nesnesi ile "semafor" nesnesinin bir arada kullanılmasının * yegane amacı kaynak bittiğinde diğer "thread" lerin uykuda bekletilmesidir. */ int result; if((result = pthread_mutex_lock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); int i; for(i = 0; i < NRESOURCES; ++i) { if(!g_resources.useflags[i]) { g_resources.useflags[i] = 1; break; } } if((result = pthread_mutex_unlock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("[%s] is acquired resources: [%d]...\n", ti->name, i + 1); do_with_resources(ti, i + 1); usleep(rand_r(&ti->seed) % 50000); if((result = pthread_mutex_lock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); g_resources.useflags[i] = 0; if((result = pthread_mutex_unlock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("[%s] is released resources: [%d]...\n", ti->name, i + 1); if(sem_post(&g_resources.sem) == -1) exit_sys("sem_post"); usleep(rand_r(&ti->seed) % 1000); } void do_with_resources(THREAD_INFO* ti, int nresource) { printf("[%s] is doing something with resource: [%d]...\n", ti->name, nresource); } void* thread_1(void* param) { THREAD_INFO* ti = (THREAD_INFO*) param; for(int i = 0; i < EPOCH; ++i) assign_resource(ti); return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>>>> "Üretici-Tületici" Problemlerinin semafor nesneleri ile çözümü: Anımsanacağınız üzere bu problemi Koşul Değişkenleri ile çözmüştük. Fakat semafor nesneleri ile daha kolay bir şekilde çözebiliriz. Yine aynı mantık ile hareket edilmektedir; yani iki "thread" birbirini uyandırmaktadır. Aşağıda bu problemin semafor nesneleri kullanılarak çözümü mevcuttur. Bu çözümde aynı prosesin "thread" leri kullanılmıştır. * Örnek 1, Aşağıdaki yaklaşımda kuyruk sistemi yerine bir nesne kullanılmıştır. #include #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_shared; int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 */ if((sem_init(&g_sem_producer, 0, 1)) == -1) exit_sys("sem_init"); if((sem_init(&g_sem_consumer, 0, 0)) == -1) exit_sys("sem_init"); int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if(sem_destroy(&g_sem_consumer) == -1) exit_sys("sem_destroy"); if(sem_destroy(&g_sem_producer) == -1) exit_sys("sem_destroy"); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if(sem_wait(&g_sem_producer) == -1) exit_sys("sem_wait"); g_shared = val; if(sem_post(&g_sem_consumer) == -1) exit_sys("sem_post"); if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; for(;;) { if(sem_wait(&g_sem_consumer) == -1) exit_sys("sem_wait"); val = g_shared; if(sem_post(&g_sem_producer) == -1) exit_sys("sem_post"); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Şimdi de aynı problemin kuyruk sistemi kullanılarak çözümü gerçekleştirilmiştir. Burada dikkat edilmesi gereken üreteci için kullanılacak semafor nesnesinin sayacı, kuyruk uzunluğunda olması gerekmekte. Yine tüketici çalışmadığı zaman üretici kuyruk uzunluğu kadar çalışacaktır. Benzer biçimde üretici çalışmadığı zaman ise tüketici kuyruktakilerin hepsini aldıktan sonra beklemeye geçecektir. İlgili programı derlerken "gcc sample.c syncqueue.c -o sample -lpthread" şeklinde komut çalıştırmalıyız. /* syncqueue.h */ #ifndef SYNCQUEUE_H_ #define SYNCQUEUE_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagSYNC_QUEUE{ DATATYPE* queue; size_t size; size_t head; size_t tail; sem_t sem_producer; sem_t sem_consumer; }SYNC_QUEUE; /* Function Prototypes */ SYNC_QUEUE* init_syncqueue(size_t size); DATATYPE populate_syncqueue(SYNC_QUEUE* sq, DATATYPE value); DATATYPE depopulate_syncqueue(SYNC_QUEUE* sq, DATATYPE* value); DATATYPE destroy_syncqueue(SYNC_QUEUE* sq); #endif /* syncqueue.c */ #include #include "syncqueue.h" /* Function Definitions */ SYNC_QUEUE* init_syncqueue(size_t size) { SYNC_QUEUE* sq; if((sq = (SYNC_QUEUE*)malloc(sizeof(SYNC_QUEUE))) == NULL) goto FAILED_I; if((sq->queue = (DATATYPE*)malloc(sizeof(DATATYPE) * size)) == NULL) goto FAILED_II; sq->size = size; sq->head = sq->tail = 0; if(sem_init(&sq->sem_producer, 0, size) == -1) goto FAILED_III; if(sem_init(&sq->sem_consumer, 0, 0) == -1) goto FAILED_III; return sq; FAILED_III: free(sq->queue); FAILED_II: free(sq); FAILED_I: return NULL; } DATATYPE populate_syncqueue(SYNC_QUEUE* sq, DATATYPE value) { if(sem_wait(&sq->sem_producer) == -1) return -1; sq->queue[sq->tail++] = value; sq->tail %= sq->size; if(sem_post(&sq->sem_consumer) == -1) return -1; return 0; } DATATYPE depopulate_syncqueue(SYNC_QUEUE* sq, DATATYPE* value) { if(sem_wait(&sq->sem_consumer) == -1) return -1; *value = sq->queue[sq->head++]; sq->head %= sq->size; if(sem_post(&sq->sem_producer) == -1) return -1; return 0; } DATATYPE destroy_syncqueue(SYNC_QUEUE* sq) { if(sem_destroy(&sq->sem_producer) == -1) return -1; if(sem_destroy(&sq->sem_consumer) == -1) return -1; free(sq->queue); free(sq); return 0; } /* sample.c */ #include #include #include #include #include #include #include "syncqueue.h" #define EPOCH 100 void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 */ SYNC_QUEUE* sq; if((sq = init_syncqueue(EPOCH)) == NULL) exit_sys("init_syncqueue"); pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, sq)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, sq)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if(destroy_syncqueue(sq) == -1) exit_sys("destroy_syncqueue"); return 0; } void* thread_producer(void* param) { SYNC_QUEUE* sq = (SYNC_QUEUE*)param; unsigned int seed = time(NULL) + 123; int val = 0; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((populate_syncqueue(sq, val)) == -1) exit_sys("populate_syncqueue"); if(val == EPOCH) break; ++val; } return NULL; } void* thread_consumer(void* param) { SYNC_QUEUE* sq = (SYNC_QUEUE*)param; unsigned int seed = time(NULL) + 456; int val; for(;;) { if((depopulate_syncqueue(sq, &val)) == -1) exit_sys("depopulate_syncqueue"); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == EPOCH) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, Şimdi de "Üretici-Tüketici" problemini semaforlar kullanarak farklı prosesler arasındaki çözümünü gerçekleştirelim. Fakat çalıştırırken ilk önce "Producer", sonra "Consumer" programını çalıştırmalıyız. /* sharing.h */ #ifndef SHARING_H #define SHARING_H #include #include #define QUEUE_SIZE 100 #define SHM_NAME "/producer-consumer" /* * Paylaşılan bellek alanına yazılacak nesne aşağıdaki nesnedir. */ typedef struct tagSHARED_INFO{ sem_t sem_producer; sem_t sem_consumer; int head; int tail; int queue[QUEUE_SIZE]; }SHARED_INFO; #endif /* Producer */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); if(sem_init(&shmaddr->sem_producer, 1, QUEUE_SIZE) == -1) exit_sys("sem_init"); if(sem_init(&shmaddr->sem_consumer, 0, QUEUE_SIZE) == -1) exit_sys("sem_init"); shmaddr->head = 0; shmaddr->tail = 0; int val = 0; for(;;) { usleep(rand() % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if(sem_wait(&shmaddr->sem_producer) == -1) exit_sys("sem_wait"); shmaddr->queue[shmaddr->tail++] = val; shmaddr->tail %= QUEUE_SIZE; if(sem_post(&shmaddr->sem_consumer) == -1) exit_sys("sem_wait"); if(val == QUEUE_SIZE) break; ++val; } puts("Press ENTER to continue..."); getchar(); if(sem_destroy(&shmaddr->sem_consumer) == -1) exit_sys("sem_destroy"); if(sem_destroy(&shmaddr->sem_producer) == -1) exit_sys("sem_destroy"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 */ srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int val = 0; for(;;) { if(sem_wait(&shmaddr->sem_consumer) == -1) exit_sys("sem_wait"); val = shmaddr->queue[shmaddr->head++]; shmaddr->head %= QUEUE_SIZE; if(sem_post(&shmaddr->sem_producer) == -1) exit_sys("sem_post"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == QUEUE_SIZE) break; } puts(""); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>>> İsimli Semafor Nesneleri: Daha önce işlenilen "POSIX" Paylaşılan Bellek Alanları ve "POSIX" Mesaj Kuyruklarına benzer biçimde kullanılmaktadır. Kullanım adımları ise şu şekildedir; -> İlk önce "sem_open" fonksiyonu ile isimli bir semafor nesnesi oluşturulur. Eğer aynı isimde bir semafor nesnesi var ise o açılır. Fonksiyonun prototipi şu şekildedir: #include sem_t *sem_open(const char *name, int oflag, ...); Fonksiyonun birinci parametresi prosesler arasında kullanım için gereken bir isimdir. Bu isim yine diğer "POSIX IPC" nesnelerinde olduğu gibi kök dizinde bir isim olmalıdır. İkinci parametre ise açık modlarını belirtmektedir. Bu parametre şu bayraklardan birisini almaktadır: "O_CREAT" ve "O_EXCL". Burada "O_CREAT" bayrağının kullanılması durumunda ilgili isimde bir nesne yok ise yeni bir tane oluşturulur. Aksi halde var olan açılır. "O_EXCL" bayrağı ise "O_CREAT" ile birlikte kullanılır ve ilgili isimde bir nesne varsa fonksiyon başarısız olacaktır. Aksi halde sıfırdan bir nesne oluşturulur. Öte yandan semafor nesnelerine yazma veya okuma yapmak gibi bir kavram POSIX standartlarında mevcut değildir. Dolayısıyla açış modu olarak "O_RDONLY", "O_WRONLY" veya "O_RDWR" gibi bayraklar kullanılmaz. Bu durumda halihazırdaki bir semafor nesnesi açılacaksa, bu ikinci parametreye "0" geçilebilir. Eğer yeni bir tane oluşturulacaksa, iki parametre daha bu fonksiyona geçilmelidir. Üçüncü parametre erişim hakları içinken, dördüncü parametre ise semafor nesnesi içerisindeki sayacı belirtmektedir. Burada dikkat etmemiz gereken şey, bu üçüncü parametreye geçeceğimiz "access" bilgilerinin prosesin "umask" değerinden etkilenir olmasıdır. Son olarak üçüncü parametreye geçilen erişim haklarında hem "read" hem de "write" hakkının bulunuyor olması gerekmektedir. Her ne kadar POSIX bu konuda bir şey söylememiş olsa bile Linux standartlarınca bu şekildedir. Buradan hareketre diyebiliriz ki, eğer yeni bir semafor nesnesi oluşturulacaksa, fonksiyonun prototipi aşağıdaki gibidir: #include sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); Fonksiyonun geri dönüş değer ise başarı durumunda yeni oluşturulan semafor nesnesinin adresine, başarısızlık durumunda ise hata kodunun("SEM_FAILED") kendisine dönmektedir. Yine "errno" değişkeni de uygun değere çekilir. -> Artık Kritik Kod bölgesi tıpkı isimsiz semafor nesnelerinde olduğu gibi "sem_wait" ve "sem_post" fonksiyonlarının arasına yazılması gerekmektedir. -> Semafor nesnesinin kullanımı bittikten sonra "sem_close" fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi de şu şekildedir: #include int sem_close(sem_t *sem); Fonksiyon ilgili semafor nesnesinin adresini alır. Başarı durumunda "0", hata durumudna "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. -> Son olarak isimli bu semafor nesnesi "sem_unlink" fonksiyonu ile yok edilmelidir. Eğer nesne yok edilmezse "kernel Persistance" bir biçimde ilk "reboot" edilene kadar sistemde kalmaya devam edecetir. Fonksiyonun prototipi aşağıdaki gibidir: #include int sem_unlink(const char *name); Fonksiyon semafor nesnesinin ismini parametre olarak alır. Başarı durumunda "0", hata durumunda "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. İsimli POSIX semafor nesneleri de tıpkı diğer "POSIX" IPC nesnelerinde olduğu gibi "/dev/shm" dizini içerisinden görüntülenmektedir. Fakat Linux sistemlerinde semafor nesnelerinin isimlerinin başlarına "sem." yazısı eklenerek saklanmaktadır. * Örnek 1, Aşağıda tüketici-üretici probleminin isimli semafor nesneleri kullanılarak çözümü belirtilmiştir. Yine ilk önce "Producer", daha sonra "Consumer" programı çalıştırılmalıdır. /* sharing.h */ #ifndef SHARING_H #define SHARING_H #include #include #define QUEUE_SIZE 100 #define SHM_NAME "/producer-consumer" #define SEM_PRODUCER_NAME "/producer-semaphore" #define SEM_CONSUMER_NAME "/consumer-semaphore" typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; }SHARED_INFO; #endif /* Producer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); sem_t* sem_producer; if((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR | S_IWUSR, QUEUE_SIZE)) == SEM_FAILED) exit_sys("sem_open"); sem_t* sem_consumer; if((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR | S_IWUSR, 0)) == SEM_FAILED) exit_sys("sem_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); shmaddr->head = 0; shmaddr->tail = 0; int val = 0; for(;;) { usleep(rand() % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if(sem_wait(sem_producer) == -1) exit_sys("sem_wait"); shmaddr->queue[shmaddr->tail++] = val; shmaddr->tail %= QUEUE_SIZE; if(sem_post(sem_consumer) == -1) exit_sys("sem_wait"); if(val == QUEUE_SIZE) break; ++val; } puts("Press ENTER to continue..."); getchar(); if(sem_close(sem_producer) == -1) exit_sys("sem_close"); if(sem_close(sem_consumer) == -1) exit_sys("sem_close"); if(sem_unlink(SEM_PRODUCER_NAME) == -1) exit_sys("shm_unlink"); if(sem_unlink(SEM_CONSUMER_NAME) == -1) exit_sys("shm_unlink"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 */ srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); sem_t* sem_producer; if((sem_producer = sem_open(SEM_PRODUCER_NAME, 0)) == SEM_FAILED) exit_sys("sem_open"); sem_t* sem_consumer; if((sem_consumer = sem_open(SEM_CONSUMER_NAME, 0)) == SEM_FAILED) exit_sys("sem_open"); int val = 0; for(;;) { if(sem_wait(sem_consumer) == -1) exit_sys("sem_wait"); val = shmaddr->queue[shmaddr->head++]; shmaddr->head %= QUEUE_SIZE; if(sem_post(sem_producer) == -1) exit_sys("sem_post"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == QUEUE_SIZE) break; } puts(""); if(sem_close(sem_consumer) == -1) exit_sys("sem_close"); if(sem_close(sem_producer) == -1) exit_sys("sem_close"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>> "System 5" semafor nesneleri: Yine burada izlenecek yol da diğer "System 5 IPC" nesnelerinde izlenen yol gibidir. Benzer arayüze sahiptirler. Fakat "System 5" semafor nesneleri "POSIX" semafor nesnelerine göre oldukça karışık bir arayüze sahiptir. Bu nedenle "POSIX" semafor nesnelerinin kullanılması tavsiye edilmektedir eğer özel nedenler yoksa. Öte yandan "System 5" semafor nesneleri aynı prosesin "thread" leri arasında değil, farklı prosesler arasında kullanım için düşünülmüştür. Zaten bunların tasarlandığı yıllarda henüz "thread" ler uygulamaya girmemiştir. Pekiyi "System 5" semafor nesnelerinin arayüzünü karışık yapan unsurlar nelerdir? Bu unsurlardan bir tanesi tek hamlede birden fazla semafor üzerinde işlem yapılmasıdır. Ayrıca sayaç mekanizması da biraz karışık ve çok işlevli olarak tasarlanmıştır. Bu tip semafor nesneleri karmaşık tasarımından dolayı gün geçtikte, "POSIX" versiyonlarına nazaran, daha az kullanılmaktadır. Şimdi bu tipin tipik kullanımına bakalım; -> Semafor nesnesi "semget" fonksiyonu ile (anımsarsanız diğer "System 5 IPC" nesneleri "shmget" ve "msgget" fonksiyonu kullanılır) oluşturulur yada var olan açılır. Hatırlayacağınız üzere "System 5 IPC" nesnelerinde "key-value" çifti kullanılmaktadır. "semget" fonksiyonunun prototipi aşağıdaki gibidir: #include int semget(key_t key, int nsems, int semflg); Fonksiyonun birinci parametresi, prosesler arasında kullanım için gereken, "key" değeridir. Bu "key" değeri kullanılarak bir "ID" değeri elde edilecektir. Bu "ID" değeri ise sistem genelinde "unique" haldedir. Yine bu "key" değerini bir isimden elde etmek için "ftok" fonksiyonunu kullanabiliriz. Tabii fonksiyonun bu birinci parametresi, tıpkı diğer "System 5 IPC" nesnelerinde olduğu gibi "IPC_PRIVATE" olarak da girilebilir. Böylesi bir durumda sistem, olmayan bir "key" kullanarak bizlere "ID" değeri vermektedir. "semget" fonksiyonunun ikinci parametresi ise semafor sayısını belirtmektedir. Yani bu parametre toplam kaç tane semaforun oluşturulacağını belirtmektedir. Örneğin, "Üretici-Tüketici" problemi için iki adet semafor nesnesi oluşturmalıyız. Fonksiyonun üçüncü parametresi ise oluşturulacak "IPC" nesnesinin erişim haklarını belirtmektedir. Bu parametre yine "IPC_CREAT" ve "IPC_EXCL" bayrakları da "bitwise OR" işlemi ile eklenebilir. Anımsanacağız üzere sadece "IPC_CREAT" kullanılması durumunda ilgili "key" değerine sahip semafor nesnesi yok ise yeniden oluşturulacak, bu bayrakla "IPC_EXCL" ile birlikte kullanılması durumunda ise yoksa oluşturulacak fakat varsa hata ile geri dönülecektir. Tabii "IPC_EXCL" bayrağının da yalnız başına kullanılamayacağını unutmamalıyız. Fonksiyon başarı durumunda "IPC" nesnesinin "ID" değerine, hata durumunda ise "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. * Örnek 1, Aşağıdaki örnekte semafor nesneleri oluşturulmuştur. #include #include #include #include #include #define KEY_NAME "." #define KEY_ID 123 void exit_sys(const char* msg); int main(void) { key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); int semid; if((semid = semget(key, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Şimdi bizler "semget" fonksiyonu ile bir nevi semafor kümesi oluşturduk. Bu küme içerisindeki semaforlar, birer indis numarasına sahiptir. Tıpkı normal dizilerde olduğu gibi. Yukarıdaki örneğin baz alırsak, küme içerisindeki semaforların indis numarası sırasıyla "0" ve "1" biçimindedir. İşte "semctl" isimli fonksiyon ile bu küme içerisindeki semafor nesneleri ile işlem yapacağız. Fonksiyonun prototipi aşağıdaki gibidir: #include int semctl(int semid, int semnum, int cmd, ...); Fonksiyonun birinci parametresi, "semget" ile elde edilen "ID" değerini belirtmektedir. İkinci parametre ise semafor kümesi içerisinde üzerinde işlem yapılacak olan semaforun indis numarasıdır. Fonksiyonun üçüncü parametresi ise bu indis numarasına sahip olan semafor nesnesine uygulanacak işlemleri belirtmektedir. Bu işlemlere göre fonksiyona dördüncü parametre geçilmesi gerekebilir. Öylesi bir durumda, bu parametreye geçilmesi gereken argüman "union semun" türünden olmalıdır. İlgili tür POSIX standartlarınca aşağıdaki biçimde tanımlanmıştır: union semun { int val; struct semid_ds *buf; unsigned short *array; }arg; /* Burada "arg" isminin kullanılması gerekmemektedir. */ Anımsayacağımız üzere birlik elemanları çakışık yerleştirilmektedir. Dolayısıyla bu dördüncü parametre, üçüncü parametredeki işleme göre, ya "int" ya "struct semid_ds*" ya da "unsigned short*" türünden bir nesne olmalıdır. Fakat bu birlik bildirimi herhangi bir başlık dosyasında BULUNMAMAKTADIR. Dolayısıyla programcının kendisi bu bildirimi YAPMALIDIR. Pekiyi bizler bu fonksiyonun üçüncü parametresine hangi argümanları geçebiliriz? Bu argümanlar şunlardır: "GETVAL", "SETVAL", "GETALL", "SETALL", "GETPID", "GETNCNT", "GETZCNT" vs. Bu argümanlardan, -> "GETVAL", eğer o semafor üzerinde okuma hakkımız varsa iş bu semafor nesnes içerisindeki sayacın değerini alabiliriz. Bu durumda "semctl" fonksiyonu, o semaforun sayaç değeri ile geri dönecektir. -> "SETVAL", eğer o semafor üzerinde yazma hakkımız varsa iş bu semafor nesnes içerisindeki sayaca değer vermek için kullanılır. Bu durumda "semctl" fonksiyonunun dördüncü parametresine "semun" birliğini geçmeliyiz. Fakat öncesinde bu birliğin "val" isimli elemanına bir değer atamalıyız. -> "GETALL", semafor kümesindeki bütün semaforların içindeki sayaç değerleri elde edilir. Bu durumda "semctl" fonksiyonunun dördüncü parametresine "semun" birliğini geçmeliyiz. Fakat öncesinde bu birliğin "array" isimli elemanına bir değer atamalıyız. Bunu da semafor kümesindeki semafor kadar içi boş bir dizi oluşturup, bu diziyi birliğin "array" isimli elemanına atayarak gerçekleştirebiliriz. Unutmamalıyız ki bu dizinin elemanları "unsigned short" türünden olmalıdır. Artık fonksiyonun ikinci parametresine geçilen değerin bir önemi kalmamıştır. Tabii bu işlemi yapabilmek için prosesin ilgili semafor nesnesi üzerinde okuma hakkının olması gerekmektedir. -> "SETALL", semafor kümesindeki bütün semaforların sayaçlarını "set" etmek için kullanılır. Bu durumda "semctl" fonksiyonunun dördüncü parametresine "semun" birliğini geçmeliyiz. Fakat birliğin "array" isimli elemanına, biz programcıların oluşturup içini doldurduğu "unsigned short" türünden diziyi atamalıyız. Yine bizim oluşturacağımız dizi ile semafor kümesindeki semaforların adedi aynı olmalıdır. Artık fonksiyonun ikinci parametresine geçilen değerin bir önemi kalmamıştır. Tabii bu işlemi yapabilmek için prosesin ilgili semafor nesnesi üzerinde yazma hakkının olması gerekmektedir. -> "GETPID", o semafor nesnesi üzerinde en son "semop" işlemi uygulayan prosesin "PID" değerini verir. Çok seyrek kullanılmaktadır. Yine prosesin "read" hakkına sahip olması gerekmektedir. -> "GETNCNT" ve "GETZCNT", sırasıyla semafor sayacının arttırılmasını ve sıfır olmasını bekleyen diğer proseslerin adedini elde etmek için kullanılır. Yine prosesin "read" hakkına sahip olması gerekmektedir. -> ... Fonksiyon başarısızlık durumunda "-1" ile geri dönmekte ve "errno" değerini uygun değere çekmektedir. -> Kritik Kod bölgesi "semop" fonksiyonu ile oluşturulur. İşte işlerin karıştığı nokta da burasıdır. Fonksiyonun prototipi şöyledir: #include int semop(int semid, struct sembuf *sops, size_t nsops); Fonksiyonun birinci parametresi "semget" fonksiyonu ile elde ettiğimiz "ID" değeridir. İkinci parametre ise "sembuf" isimli bir yapının adresini almaktadır. Bu yapı "sys/sem.h" içerisinde bildirilmiştir. Fakat bu nesnenin içi, bu fonksiyonu çağıran kişi tarafından doldurulur. "sembuf" yapısı aşağıdaki biçimdedir: struct sembuf { unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ }; Yapının, -> "sem_num" isimli eleman, semafor kümesinde işlem yapılacak semaforun indis bilgisidir. -> "sem_op" isimli eleman, semafor sayacı üzerinde yapılacak işlemi belirtmektedir. Bu veri elemanı şu değerlerden birisini almaktadır: -> ">0" bir değer geçilirse, geçilen bu değer ile semafor nesnesi içerisindeki sayaç değeri toplanır. -> "<0" bir değer geçilirse, o andaki semafor sayacına bakılır. Eğer semafor sayacından buradaki değerin mutlak değeri çıkartıldığında sonuç negatif ise ilgili "thread" bloke edilir. Ancak burada çıkartma işlemi YAPILMAZ. Eğer bu çıkartma işleminden elde edilen sonuç sıfır ya da sıfırdan büyük olacaksa ÇIKARTMA İŞLEMİ YAPILIR VE BLOKEYE YOL AÇMAZ. * Örnek 1, semafor sayacının değerinin "5" olduğunu varsayalım. Biz de "sem_op" değerine "-4" geçmiş olalım. Bu durumda "(5) - (|-4|)" işleminin sonucu "1" olacağı için çıkartma işlemi gerçekleştirilir. Artık semafor sayacının değeri "1" olmuştur ve herhangi bir bloke oluşmamıştır. * Örnek 2, semafor sayacının değerinin "5" olduğunu varsayalım. Eğer "sem_op" değerine "5" girmiş olsaydık, "5 - 5" işleminin sonucu sıfır olacağı için çıkartma işlemi yapılır ve yine bloke oluşmaz. Artık semafor sayacının değeri "0" olmuştur. * Örnek 3, semafor sayacının değerinin "5" olduğunu varsayalım. Eğer "sem_op" değerine "-10" girmiş olsaydık, "(5) - (|-10|)" işleminin sonucu negatif olacağı için ÇIKARTMA İŞLEMİ GERÇEKLEŞTİRİLMEZ VE İLGİLİ THREAD BLOKE EDİLİR. Artık bu blokeden kurtulmadan tek yolu, semafor sayacının değerini "10" ya da daha büyük değere çekmekle mümkündür. Yani başka bir proses "sem_op" değerine "+10" değerini girmeli ki "10 - 10" işleminin sonucu sıfır olsun, bloke kalksın ve sayacın yeni değeri artık "0" olsun. -> "==0" olursa, özel bir durumdur. Başka bir anlama gelmektedir. Bu durumda ilgili semafor nesnesi içerisindeki sayaç "0" olana kadar ilgili "process" blokede bekletilir. Blokeden çıkmanın tek yolu ise bu sayacın değerini "0" dan yukarı çıkartmaktır. Yani "sem_op" değeri sıfır ise bu durum semafor sayacı "0" olmadığı sürece blokede bekle, anlamına gelmektedir. Bir nevi tam tersi bir durumdur. -> "sem_flg" isimli eleman "IPC_NOWAIT" veya "SEM_UNDO" bayraklarından birisini veya her ikisini içerebilir. Eğer bu bayrakları kullanmak istemiyorsak da "0" değerini geçebiliriz. Bu iki bayrak ise şu anlamlara gelmektedir: -> "IPC_NOWAIT" : Blokesiz işlem yapmak için kullanılmaktadır. Artık bloke oluşturabilecek durumlarda bloke oluşmaz ve "semop" fonksiyonu "-1" değeri ile geri döner ve "errno" değişkeni "EAGAIN" değerini alır. -> "SEM_UNDO" : İşlem sonrasında "Semaphore Adjusment" değerini işleme sokarak ters işlem yapmaktadır. Bu bayrağı şimdilik es geçiyoruz. "semop" fonksiyonunun üçüncü parametresi ise ikinci parametredeki "sembuf" dizisinin eleman sayısını belirtmektedir. Yani aslında ikinci parametreye tek bir "sembuf" nesnesinin adresini değil, elemanları "struct sembuf" olan bir dizinin başlangıç adresini de geçebiliriz. Bu durumda "semop" fonksiyonu, birden fazla semafor nesnesi üzerine işlem yapar. Fakat böyle bir kullanım çok seyrektir. Dolayısıyla bu parametre "1" geçilir. Eğer "semop" fonksiyonunda birden fazla semafor nesnesi üzerinde işlem yapılacaksa, bu işlemler atomik yapılmaktadır. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", hata durumunda ise "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. -> "System 5" semafor nesneleri de diğer "System 5 IPC" nesneleri gibi "Kernel Persistance" biçimindedir. Ya silinene kadar ya da "reboot" işlemine kadar sistemde kalıcıdır. Bu tip semafor nesnelerini silmek için de "semctl" fonksiyonunda "IPC_RMID" bayrağını kullanmak gerekmektedir. Bu durumda "semctl" fonksiyonunun ikinci parametresi dikkate alınmayacaktır. Tıpkı diğer "System 5 IPC" nesnelerinde olduğu gibi, "System 5" semafor nesnelerinin bazı değerleri "semctl" fonksiyonunda "IPC_GET" ve "IPC_SET" bayrakları ile "get" ve "set" edilebilmektedir. "semid_ds" türünden bir yapı işletim sistemi tarafından oluşturulur ki bu yapı aşağıdaki gibi bildirilmiştir: struct semid_ds { struct ipc_perm sem_perm; /* Ownership and permissions */ time_t sem_otime; /* Last semop time */ time_t sem_ctime; /* Creation time/time of last modification via semctl() */ unsigned long sem_nsems; /* No. of semaphores in set */ }; Yapının, -> "sem_perm" : İlgili "IPC" nesnesinin erişim hakkını belirtir. -> "sem_otime" ve "sem_ctime" : İlgili semafor nesnesi üzerinde yapılan son işlemlerin zamanları hakkında bilgi verir. -> "sem_nsems" : Semafor kümesindeki toplam semafor adedini belirtmektedir. "semctl" ile ilk değer verilmemiş semafor nesneleri için yapının "sem_ctime" ve "sem_nsems" isimli elemanları çöp değerdedir. Standartlar, henüz "semop" fonksiyonu çağrılmadan evvel yapının "sem_otime" isimli elemanının "0" değerinde olacağını garanti etmektedir. Bu garanti sayesinde iki proses aynı semafor nesnesine ilişkin "init." işlemi yapmak istediğinde oluşabilecek sorun çözülebilmektedir. Şimdi de "System 5" semafor nesneleri ile kabaca Kritik Kod oluşturulmasına bakalım: * Örnek 1, #include #include #include #include #include #define KEY_NAME "." #define KEY_ID 123 union semun { int val; struct semid_ds *buf; unsigned short *array; }; void exit_sys(const char* msg); int main(void) { //... key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); int semid; if((semid = semget(key, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); union semun arg; unsigned short semvals[2] = {1, 0}; arg.array = semvals; if(semctl(semid, 1, SETALL, arg) == -1) exit_sys("semctl"); //... { struct sembuf sbuf; sbuf.sem_num = 0; /* "0" indisli semafor nesnesini ele alacağız. */ sbuf.sem_op = -1; /* Bu semafor nesnesinin sayacını "1" azaltacağız. */ sbuf.sem_flags = 0; semop(semid, &sbuf, 1); /* Kritik Kod */ sbuf.sem_num = 0; /* "0" indisli semafor nesnesini ele alacağız. */ sbuf.sem_op = 1; /* Bu semafor nesnesinin sayacını "1" arttıracağız. */ sbuf.sem_flags = 0; semop(semid, &sbuf, 1); } if(semctl(semid, 0, IPC_RMID) == -1) exit_sys("semctl"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Daha önce de belirtildiği gibi "System 5" semafor nesneleri prosesler arasında kullanım için düşünülmüştür. Özellikle "Üretici-Tüketici" problemleri gibi tipik senkronizasyon problemleri, bu nesneler ile çözülmekteydi. Mademki bu "System 5" semafor nesneleri prosesler arasında kullanılmaktadır, o halde bu nesneler ağırlıklı olarak Paylaşılan Bellek Alanları ile birlikte kullanılmaktadır. * Örnek 1, Aşağıdaki örnekte semafor nesnesinin silinmesini geciktirmek için kara düzen bir sistem uygulanmıştır. /* Producer */ #include #include #include #include #include #include #include #include #define SHM_KEY 0x12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; int semid; /*0: producer; 1: consumer*/ }SHARED_INFO; union semun{ int val; struct semid_ds* buf; unsigned short* array; struct seminfo* __buf; }; void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, sizeof(SHARED_INFO), IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); si->head = 0; si->tail = 0; /* * Semafor nesnesi "IPC_PRIVATE" ile hayata getirilmiştir. */ if((si->semid = semget(IPC_PRIVATE, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); union semun arg; unsigned short semvals[2] = { QUEUE_SIZE, 0 }; arg.array = semvals; if(semctl(si->semid, 0, SETALL, arg) == -1) exit_sys("semctl"); struct sembuf sbuf; int val = 0; for(;;) { usleep(rand() % 300000); sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); si->queue[si->tail++] = val; si->tail %= QUEUE_SIZE; sbuf.sem_num = 1; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); if(val == 100) break; ++val; } printf("Press ENTER to finish...\n"); getchar(); if(semctl(si->semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if(shmdt(si) == -1) exit_sys("shmdt"); if(shmctl(shmid, IPC_RMID, 0) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #define SHM_KEY 0x12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; int semid; /*0: producer; 1: consumer*/ }SHARED_INFO; void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, 0, 0)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); struct sembuf sbuf; int val = 0; for(;;) { sbuf.sem_num = 1; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); val = si->queue[si->head++]; si->head %= QUEUE_SIZE; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == 100) break; } printf("\n"); if(shmdt(si) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise, yukarıdaki kara düzeni kullanmak yerine, yeni bir semafor nesnesi kullanılarak iki prosesin haberleşmesinin sonlandırılması sağlanmıştır. Fakat Paylaşılan Bellek Alanı'nı "Producer" oluşturduğu için, ilk olarak onun çalıştırılması gerekmektedir. /* sharing.h */ #ifndef SHARING_H #define SHARING_H #define SHM_KEY 0x12345678 #define SEM_KEY 0X12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; int semid; /*0: producer; 1: consumer*/ }SHARED_INFO; union semun{ int val; struct semid_ds* buf; unsigned short* array; struct seminfo* __buf; }; #endif /* Producer */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, sizeof(SHARED_INFO), IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); si->head = 0; si->tail = 0; if((si->semid = semget(IPC_PRIVATE, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); int semid; if((semid = semget(SEM_KEY, 1, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); union semun arg; unsigned short semvals[2] = { QUEUE_SIZE, 0 }; arg.array = semvals; if(semctl(si->semid, 0, SETALL, arg) == -1) exit_sys("semctl"); arg.val = 0; if(semctl(semid, 0, SETVAL, arg) == -1) exit_sys("semctl"); struct sembuf sbuf; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); int val = 0; for(;;) { usleep(rand() % 300000); sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); si->queue[si->tail++] = val; si->tail %= QUEUE_SIZE; sbuf.sem_num = 1; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); if(val == 100) break; ++val; } sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); if(semctl(si->semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if(semctl(semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if(shmdt(si) == -1) exit_sys("shmdt"); if(shmctl(shmid, IPC_RMID, 0) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, 0, 0)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); int semid; if((semid = semget(SEM_KEY, 1, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); struct semid_ds semds; if(semctl(semid, 0, IPC_STAT, &semds) == -1) exit_sys("semctl"); union semun arg; if(semds.sem_otime == 0) { arg.val = 0; if(semctl(semid, 0, SETVAL, arg) == -1) exit_sys("semctl"); } struct sembuf sbuf; sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); int val = 0; for(;;) { sbuf.sem_num = 1; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); val = si->queue[si->head++]; si->head %= QUEUE_SIZE; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == 100) break; } printf("\n"); sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); if(shmdt(si) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Görüldüğü üzere "System 5" semafor nesneleri, "POSIX" Semafor nesnelerine nazaran kullanımı daha zahmetlidir. Fakat bir takım "wrapper" fonksiyonlar yazarak "POSIX" semafor nesnelerini taklit edebilir, yukarıdaki karmaşıklığı minimize edebiliriz. Şöyleki: /* Anahtar ve erişim haklarını alarak bir semafor nesnesi oluşturur. */ int sem_create(int key, int mode) { return semget(key, 1, IPC_CREAT | mode); } /* Oluşturulan bu semafor nesnesi açılır. */ int sem_open(int key) { return semget(key, 1, 0); } /* Açılan bu semafor nesnesinin sayacına ilk değeri atanır. */ int sem_init(int semid, int val) { union semun{ int val; struct semid_ds* buf; unsigned short* array; struct seminfo *__buf; }su; su.val = val; return semctl(semid, 0, su); } /* Kritik Kod oluşturmak için kullanılır. */ int sem_wait(int semid) { struct sembuf sb; sb.sem_num = 0; sb.sem_op = -1; sb.sem_flg = 0; return semop(semid, &sb, 1); } /* Kritik Kod oluşturmak için kullanılır. */ int sem_post(int semid) { struct sembuf sb; sb.sem_num = 0; sb.sem_op = 1; sb.sem_flg = 0; return semop(semid, &sb, 1); } /* İlgili semafor nesnesi yok edilir. */ int sem_destroy(int semid) { return semctl(semid, 0, IPC_RMID); } Son olarak tekrar hatırlatmakta fayda vardır ki "System 5" semafor nesneleri prosesler arasında haberleşme için tasarlanmışlardır. Bu nesneler tasarlandığı yıllarda henüz "thread" ler kullanımda değildi. Her ne kadar günümüzde bu semafor nesneleri aynı prosesin "thread" leri arasında da kullanım olanağı olsada, böyle bir kullanım verimsiz ve gereksizdir. "thread" ler arasında haberleşmede semafor nesnesi kullanacaksak, tavsiye edilen isimsiz POSIX semaforlarını kullanmalıyız. Çünkü POSIX senkronizasyon nesneleri UNIX ve türevi sistemlere "thread" ler ile birlikte eklenmiştir. >>>>> "Senkronizasyon Nesneleri / Bariyer Senkronizasyon" Nesnesi" : Bir duvar düşünün; bir kişi bu duvarı yıkamıyor ve ikinci bir kişiyi çağırıyor. İki kişi birlikte yıkamadığı için üçüncü bir kişi çağırıyorlar. En sonunda bu üç kişi duvarı yıkabiliyor. İşte bariyer senkronizasyon nesneleri de bu mottodadır. Nispeten az kullanılan senkronizasyon nesneleridir. Windows sistemlerinde bu tip nesnelerin tam bir karşılığı yoktur. Bu tip senkronizasyon nesneleri, bir işin parçalarının bir takım "thread" lere yaptırılması fakat nihai işin bu "thread" lerin işlerini bitirtikten sonra yaptırılması için kullanılır. Örneğin, yeteri kadar büyük bir diziyi sıralamak isteyelim. Bu işi de 10 farklı "thread" ile gerçekleştirmek isteyelim. Her "thread" kendisine düşen kısmı sıraladıktan sonra, en son da birleştirme işlemi gerçekleştirelim. İşte bu birleştirme işlemi için kullanılmaktadır. Bu senkronizasyon nesnesini aşağıdaki adımları takip ederek kullanabiliriz: -> İlk önce "pthread_barrier_t" türünden "global" bir nesne tanımlanır. POSIX standartlarınca bu tür herhangi bir türe karşılık gelebilir. "sys/types.h" ve "pthread.h" içerisinde "define" edilmişlerdir. Genel olarak "struct" türüne karşılık gelmektedir. -> Tanımlanan bu nesneye ilk değer verilmesi gerekmektedir. Bunun için "pthread_barrier_init" isimli POSIX fonksiyonu kullanılmaktadır. "barrier" nesnelerini makrolar ile "init" EDEMEYİZ. Fonksiyonun parametrik yapısı aşağıdaki gibidir: #include int pthread_barrier_init(pthread_barrier_t * barrier, const pthread_barrierattr_t * attr, unsigned count); Fonksiyonun birinci parametresi, ilgili "barrier" nesnesinin adresidir. İkinci parametre ise bir "attribute" nesnesidir. Bu parametreye "NULL" değeri geçilebilir. Bu durumda varsayılan özellikler kullanılacaktır. Eğer varsayılan özellikler kullanılmayacaksa ki bir adet özellik nesnesi vardır ve o da ilgili nesnenin prosesler arasında kullanılıp kullanılmayacağını belirtmektedir. Varsayılan durumda nesne, prosesler arasında kullanılamamaktadır. Eğer bu özelliği değiştirmek istiyorsak; -> İlk önce "pthread_barrierattr_t" türünden bir nesne oluşturulur. -> Daha sonra bu nesneye "pthread_barrierattr_init" fonksiyonu ile ilk değer verilir. -> Daha sonra ilgili özelliğin "set" ve "get" edilmesi için sırasıyla "pthread_barrierattr_setpshared" ve "pthread_barrierattr_getpshared" fonksiyonları çağrılabilir. Burada tek bir "attribute" olduğu için sadece "PTHREAD_PROCESS_SHARED" sembolik sabiti kullanımaktadır. Bu özellik ise ilgili nesnenin prosesler arasında kullanılıp kullanılamayacağını belirtmektedir. -> En sonunda ise bu "attribute" nesnesini "destroy" etmeliyiz. Bunu da "pthread_barrierattr_destroy" ile gerçekleştirebiliriz. Tabii yine prosesler arasında kullanabilmek için, ilgili bariyer nesnesini de Paylaşılan Bellek Alanında oluşturmamız gerekmektedir. Son parametre ise kaç tane nesne geldiğinde bariyerin açılacağı bilgisidir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Burada dikkat etmemiz gereken şey bariyer kırıldıktan sonra tekrar otomatik olarak YENİLENMESİDİR. -> "n" tane "thread" in belli bir noktada bekletilmesi için "pthread_barrier_wait" fonksiyonu kullanılmaktadır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_barrier_wait(pthread_barrier_t *barrier); Fonksiyon argüman olarak ilgili "barrier" nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Bu fonksiyon ilgili "thread" lerin "barrier" nesnesi tarafından bekletilmesine yol açar. "n" tane "thread" bekletildiğinde artık bloke kaldırılır. Dolayısıyla bu fonksiyon çağrıldığında ilgili "thread" in kendi işini bitirmiş olması gerekmektedir. Yukarıdaki diziyi sıralama örneğini baz alırsak, kendi içerisinde sıralanmış dizileri hangi "thread" birleştirecektir? POSIX standartları bu iş için yine "pthread_barrier_wait" fonksiyonunu görevli kılmıştır. Şöyleki; Bu fonksiyon aynı zamanda "PTHREAD_BARRIER_SERIAL_THREAD" sembolik sabiti ile de geri dönmektedir. İşte hangi "thread" in çağırdığı "pthread_barrier_wait" fonksiyonu bu sembolik sabit ile geri dönerse, nihai birleştirme işi de o "thread" tarafından yapılacaktır. Fakat hangi şartlar altında fonksiyonun bu sembolik sabit ile döneceği konusunda POSIX standartları bir şey söylememiştir. -> Artık ilgili "barrier" nesnesini "destroy" etmeliyiz. Bunun için "pthread_barrier_destroy" fonksiyonunu çağırmalıyız. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_barrier_destroy(pthread_barrier_t *barrier); Yine argüman olarak ilgili bariyer nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Aşağıda kullanıma ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıda 3 adet "thread" kullanılmıştır. #include #include #include #include #include void* pthread_proc1(void* param); void* pthread_proc2(void* param); void* pthread_proc3(void* param); void exit_sys_errno(const char* msg, int eno); pthread_barrier_t g_barrier; int main(void) { pthread_t tid1, tid2, tid3; int result; if((result = pthread_barrier_init(&g_barrier, NULL, 3)) != 0) exit_sys_errno("pthread_barrier_init", result); if((result = pthread_create(&tid1, NULL, pthread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid3, NULL, pthread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_barrier_destroy(&g_barrier)) != 0) exit_sys_errno("pthread_barrier_destroy", result); return 0; } void* pthread_proc1(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc1 started...\n"); seed = time(NULL) + 123; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc1 will merge >>>\n"); } printf("pthread_proc1 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc2(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc2 started...\n"); seed = time(NULL) + 456; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc2 will merge >>>\n"); } printf("pthread_proc2 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc3(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc3 started...\n"); seed = time(NULL) + 789; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc3 will merge >>>\n"); } printf("pthread_proc3 returned after %d seconds...\n", sleep_time); return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } * Örnek 2, Aşağıda 3 adet "thread" kullanılmıştır. Ek olarak ilgili bariyer nesnesi prosesler arasında kullanıma açılmıştır. #include #include #include #include #include void* pthread_proc1(void* param); void* pthread_proc2(void* param); void* pthread_proc3(void* param); void exit_sys_errno(const char* msg, int eno); pthread_barrier_t g_barrier; int main(void) { int result; pthread_barrierattr_t battr; if((result = pthread_barrierattr_init(&battr)) != 0) exit_sys_errno("pthread_barrierattr_init", result); if((result = pthread_barrierattr_setpshared(&battr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_barrierattr_setpshared", result); if((result = pthread_barrierattr_destroy(&battr)) != 0) exit_sys_errno("pthread_barrierattr_destroy", result); pthread_t tid1, tid2, tid3; if((result = pthread_barrier_init(&g_barrier, &battr, 3)) != 0) exit_sys_errno("pthread_barrier_init", result); if((result = pthread_create(&tid1, NULL, pthread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid3, NULL, pthread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_barrier_destroy(&g_barrier)) != 0) exit_sys_errno("pthread_barrier_destroy", result); return 0; } void* pthread_proc1(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc1 started...\n"); seed = time(NULL) + 123; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc1 will merge >>>\n"); } printf("pthread_proc1 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc2(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc2 started...\n"); seed = time(NULL) + 456; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc2 will merge >>>\n"); } printf("pthread_proc2 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc3(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc3 started...\n"); seed = time(NULL) + 789; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc3 will merge >>>\n"); } printf("pthread_proc3 returned after %d seconds...\n", sleep_time); return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } * Örnek 3.0, Aşağıda ise bir dizi parçalara bölünerek sıralanmıştır. #include #include #include #include #include #include /* Thread'siz 50000000 için 11.30 saniye (100000000 için 43.1)*/ #define SIZE 50000000 #define NTHREADS 10 void sort_with_single_thread(void); void sort_with_multiple_thread(void); void exit_sys_thread(const char *msg, int err); void *thread_proc(void *param); int comp(const void *pv1, const void *pv2); void merge(void); int check(void); pthread_barrier_t g_barrier; int g_nums[SIZE]; int g_snums[SIZE]; clock_t g_start, g_stop; double g_telaped; int main(void) { puts("\n--------------------------------"); sort_with_single_thread(); // Total Duration: 5.080036 puts("\n--------------------------------"); sort_with_multiple_thread(); // Total Duration: 8.873042 puts("\n--------------------------------"); return 0; } void sort_with_single_thread(void) { g_start = clock(); qsort(g_nums, SIZE, sizeof(int), comp); merge(); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void sort_with_multiple_thread(void) { int result; int i; pthread_t tids[NTHREADS]; srand(time(NULL)); for (i = 0; i < SIZE; ++i) g_nums[i] = rand(); g_start = clock(); if ((result = pthread_barrier_init(&g_barrier, NULL, NTHREADS)) != 0) exit_sys_thread("pthread_barrier_init", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_create(&tids[i], NULL, thread_proc, (void *)i)) != 0) exit_sys_thread("pthread_create", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_join(tids[i], NULL)) != 0) exit_sys_thread("pthread_join", result); pthread_barrier_destroy(&g_barrier); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void exit_sys_thread(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc(void *param) { int part = (int)param; int result; qsort(g_snums + part * (SIZE / NTHREADS) , SIZE / NTHREADS, sizeof(int), comp); result = pthread_barrier_wait(&g_barrier); if (result != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_thread("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) merge(); /* other jobs... */ return NULL; } int comp(const void *pv1, const void *pv2) { const int *pi1 = (const int *)pv1; const int *pi2 = (const int *)pv2; return *pi1 - *pi2; } void merge(void) { int indexes[NTHREADS]; int min, min_index; int i, k; int partsize; int size = NTHREADS; partsize = SIZE / NTHREADS; for (i = 0; i < NTHREADS; ++i) indexes[i] = i * partsize; while(size > 0) { min = g_nums[indexes[0]]; min_index = 0; for (k = 1; k < size; ++k) if (g_nums[indexes[k]] < min) { min = g_nums[indexes[k]]; min_index = k; } g_snums[i] = min; ++indexes[min_index]; if(indexes[min_index] >= (i+1) * partsize){ indexes[min_index] = indexes[size - 1]; --size; } } } int check(void) { int i; for (i = 0; i< SIZE - 1; ++i) if (g_snums[i] > g_snums[i + 1]) return 0; return 1; } * Örnek 3.1, Aşağıda ise bir dizi parçalara bölünerek sıralanmıştır. #include #include #include #include #include #include /* Thread'siz 50000000 için 11.30 saniye (100000000 için 43.1)*/ #define SIZE 50000000 #define NTHREADS 10 struct INDEX{ int index; int part; }; void sort_with_single_thread(void); void sort_with_multiple_thread(void); void exit_sys_thread(const char *msg, int err); void *thread_proc(void *param); int comp(const void *pv1, const void *pv2); void merge(void); int check(void); pthread_barrier_t g_barrier; int g_nums[SIZE]; int g_snums[SIZE]; clock_t g_start, g_stop; double g_telaped; int main(void) { puts("\n--------------------------------"); sort_with_single_thread(); // Total Duration: 5.080036 puts("\n--------------------------------"); sort_with_multiple_thread(); // Total Duration: 8.873042 puts("\n--------------------------------"); return 0; } void sort_with_single_thread(void) { g_start = clock(); qsort(g_snums, SIZE, sizeof(int), comp); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void sort_with_multiple_thread(void) { int result; int i; pthread_t tids[NTHREADS]; srand(time(NULL)); for (i = 0; i < SIZE; ++i) g_nums[i] = rand(); g_start = clock(); if ((result = pthread_barrier_init(&g_barrier, NULL, NTHREADS)) != 0) exit_sys_thread("pthread_barrier_init", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_create(&tids[i], NULL, thread_proc, (void *)i)) != 0) exit_sys_thread("pthread_create", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_join(tids[i], NULL)) != 0) exit_sys_thread("pthread_join", result); pthread_barrier_destroy(&g_barrier); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void exit_sys_thread(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc(void *param) { int part = (int)param; int result; qsort(g_snums + part * (SIZE / NTHREADS) , SIZE / NTHREADS, sizeof(int), comp); result = pthread_barrier_wait(&g_barrier); if (result != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_thread("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) merge(); /* other jobs... */ return NULL; } int comp(const void *pv1, const void *pv2) { const int *pi1 = (const int *)pv1; const int *pi2 = (const int *)pv2; return *pi1 - *pi2; } void merge(void) { struct INDEX indexes[NTHREADS]; int min, min_index; int i, k; int partsize; int size = NTHREADS; partsize = SIZE / NTHREADS; for (i = 0; i < NTHREADS; ++i){ indexes[i].index = i * partsize; indexes[i].part = i; } while(size > 0) { min = g_nums[indexes[0].index]; min_index = 0; for (k = 1; k < size; ++k) if (g_nums[indexes[k].index] < min) { min = g_nums[indexes[k].index]; min_index = k; } g_snums[i] = min; ++indexes[min_index].index; if(indexes[min_index].index >= (i+1) * indexes[min_index].part){ indexes[min_index] = indexes[size - 1]; --size; } } } int check(void) { int i; for (i = 0; i< SIZE - 1; ++i) if (g_snums[i] > g_snums[i + 1]) return 0; return 1; } >>>>> "Senkronizasyon Nesneleri / Spinlock" senkronizasyon nesnesi: Tek bir "thread" akışının Kritik Kod bölgesine girmesini sağlamaktadır. Diğer senkronizasyon nesnelerine nazaran meşgul bir döngü içerisinde bekleme yapmakta, blokeye yol açmamaktadır. Anımsayacağınız üzere "thread" ler bloke edilirken, yani "wait" kuyruğuna alınırken", ve blokeleri kaldırılırken, yani "run" kuyruklarına alınırken, bir zaman harcarlar. Öte yandan meşgul bir döngü içerisinde beklemek de "quanta" süresini boşa harcamaya neden olmaktadır. İşte "quanta" süresinin boşa harcanması, bir "thread" in önce "wait" daha sonra "run" kuyruklarına alınmasından daha önemsizse, bu durumlarda "Spinlock" senkronizasyon nesnesi kullanılabilir. Bir diğer deyişle Kritik Kod bölgesi çok kısa olan ve o bölgenin çağrılma olasılığı düşük olan senaryolarda "Spinlock" kullanabiliriz. Yani meşgul bir döngü içerisinde beklemeye değecekse, "Spinlock"; aksi halde diğer senkronizasyon nesneleri. Kaldı ki diğer senkronizasyon nesneleri az da olsa "spin" yapmaktadır. Diğer yandan tek "core" lu işlemcilerde bu senkronizasyon nesnesinin kullanımı hiç tavsiye edilmez. Birden fazla "core" içeren işlemcilerde kullanırken de dikkatli olmalıyız. Burada tavsiye edilen şey aynı prosesin "thread" leri arasında senkronizasyon için akla ilk gelmesi gereken "mutex" nesneleridir. "Spinlock" nesneleri bazı uç senaryolarda performansı arttırmak için dikkatli kullanılmalıdır. Pekiyi "Spinlock" nesneleri nasıl kullanılır? -> "pthread_spinlock_t" türünden global bir nesne oluşturulur. Bu tür yine "pthread.h" ve "sys/types.h" içerisinde tanımlanmıştır. Herhangi bir türe karşılık gelebilir. Tipik olarak "struct" türüne karşılık gelmektedir. -> Daha sonra bu nesneye "pthread_spin_init" fonksiyonu ile ilk değer verilir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_spin_init(pthread_spinlock_t *lock, int pshared); Fonksiyonun birinci parametresi, yukarıdaki nesnenin adresini almaktadır. İkinci parametre ise prosesler arasında kullanılıp kullanılamayacağını belirtmektedir. Eğer kullanılacaksa "PTHREAD_PROCESS_SHARED" değerini, aksi halde "0" değerini almaktadır. Eğer prosesler arasında kullanılacaksa, Paylaşılan Bellek Alanları kullanılmalıdır. Diğer taraftan "Spinlock" nesnesinin, "barrier" nesnelerine nazaran, ayrıca bir "attribute" nesnesine sahip olmadığına dikkat ediniz. Yine "Spinlock" nesnesine ilk değer vermek için bir makro bulunmamaktadır. -> Kritik Kod bölgesi "pthread_spin_lock" ve "pthread_spin_unlock" fonksiyonları ile oluşturulur. Fonksiyonların prototipleri şu şekildedir: #include int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock); Fonksiyonlar argüman olarak ilgili "Spinlock" nesnesinin adresini alırlar. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönerler. "thread" in akışı "pthread_spin_lock" fonksiyonuna girdiğinde meşgul bir döngü içerisinde beklemektedir. "pthread_spin_lock" fonksiyonunun "pthread_spin_trylock" biçimi de vardır. Bu fonksiyon ise "spin" yapmak yerine başarısızlıkla geri döner. Bu tür durumlarda ise hata kodu genellikle "EBUSY" biçimindedir. -> Son aşamada ise ilgili "Spinlock" nesnesinin "destroy" edilmesi gerekmektedir. Bunun için de "pthread_spin_destroy" fonksiyonu çağrılmalıdır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_spin_destroy(pthread_spinlock_t *lock); Fonksiyon ilgili "Spinlock" nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Aşağıda ise "Spinlock" nesnesinin kullanımına dair örnekler verilmiştir. "Spinlock" nesnesinin kullanımı CPU yoğun bir kullanımdır. Dolayısıyla birden fazla çekirdekli işlemcilerde daha iyi sonuç verebilmektedir. Fakat işletim sistemi aşağıdaki "thread" leri aynı çekirdeklere atarsa, tam tersine daha kötü sonuçlar da elde edebiliriz. * Örnek 1, #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_proc1(void* param); void* pthread_proc2(void* param); void exit_sys_errno(const char* msg, int eno); pthread_spinlock_t g_spinlock; int g_count; int main(void) { int result; if((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); printf("Ok... %d\n", g_count); return 0; } void* pthread_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } * Örnek 2, Aşağıdaki örnekte ise "Spinlock" ile "mutex" nesnesi hız bakımından karşılaştırılmıştır. #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_spin_proc1(void* param); void* pthread_spin_proc2(void* param); void* pthread_mutex_proc1(void* param); void* pthread_mutex_proc2(void* param); void spin_lock(void); void mutex_lock(void); void exit_sys_errno(const char* msg, int eno); pthread_spinlock_t g_spinlock; pthread_mutex_t g_mutex; int g_count; int main(void) { /* # OUTPUT # Ok... 20000000, in 1.886349 seconds Ok... 40000000, in 1.951004 seconds */ spin_lock(); mutex_lock(); return 0; } void* pthread_spin_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_spin_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void spin_lock(void) { double start, end; int result; start = clock(); if((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_spin_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_spin_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void mutex_lock(void) { double start, end; int result; start = clock(); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_mutex_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_mutex_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } >>>>> "Senkronizasyon Nesneleri / Read-Write Lock" Nesneleri: Bir diğer sık karşılaşılan senkronizasyon nesnesi de "Reader-Writer Lock" nesneleridir. Öyle bir senkronizasyon nesnesidir ki birden fazla "thread" in okuma yapmasına izin vermekte, fakat bir "thread" yazma işlemi yapıyorsa diğer "thread" lerin okuma veya yazma yapmasına izin vermemektedir. Yani şöyle de diyebiliriz: elimizde bir adet bağlı liste ve bu bağlı listeye "insert", "delete" ve "search" işlemleri yapan bir grup "thread" ler söz konusu olsun. Burada "thread" lerden birisi bağlı listeye "insert" ya da "delete" işlemi yaparken diğerlerinin beklemesi gerekmektedir. Benzer biçimde "thread" lerden birisi "search" işlemi yaparken diğer "thread" lerin "insert" ya da "delete" işlemi yapamaması gerekmektedir. Sadece birden fazla "thread" in aynı anda "search" işlemi yapmasına müsaade edilmelidir. Buradaki "insert" ve "delete" işlemleri "write", "search" işlemi de "read" işlemi olarak ele alınabilir. Burada açıklanan durumu daha önce işlenen senkronizasyon nesnelerini direkt olarak kullanarak basitçe bir yolu yoktur. Örneğin, "mutex" nesnelerini ele alalım: "write" işlemi yaparken diğerlerinin beklemesini sağlayabiliriz fakat "read" yaparken diğerlerinin "read" yapmasını da engelleriz. Dolayısıyla iki tane "read" işlemi yapılamaz. Pekiyi "Reader-Write Lock" nesneleri nasıl kullanılır? -> İlk önce "pthread_rwlock_t" türünden nesne oluşturulur. Bu tür yine "pthread.h" ve "sys/types.h" dosyalarında herhangi bir tür olabilecek şekilde "typedef" edilmiştir. Genellikle "struct" türüne karşılık gelmektedir. -> Daha sonra bu nesneye ilk değerini vereceğiz. Bunun için ya "PTHREAD_RWLOCK_INITIALIZER" makrosu ile statik bir biçimde ya da "pthread_rwlock_init" fonksiyonu ile ilk değer verebiliriz. İlgili fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_rwlock_init(pthread_rwlock_t * rwlock, const pthread_rwlockattr_t * attr); Fonksiyonun ilk parametresi ilgili "rwlock" nesnesinin adresini almaktadır. İkinci parametre ise iş bu "rwlock" nesnesine ait bir "attribute" nesnesinin adresidir. Bu özellik nesnesini kullanabilmek için; -> İlk önce "pthread_rwlockattr_t" türünden bir nesne oluşturulur. -> Daha sonra bu nesneye "pthread_rwlockattr_init" fonksiyonu ile ilk değer verilir. -> Daha sonra "pthread_rwlockattr_setXXX" fonksiyonları ile yeni özellikler iliştirilir. Fakat şimdilik sadece bir adet özellik bulunmaktadır. O da prosesler arasında kullanılabilirliğine ilişkindir. Varsayılan durumda (yani "PTHREAD_RWLOCK_INITIALIZER" makrosu kullanıldığında ya da "pthread_rwlock_init" fonksiyonunda "attribute" argümanı için "NULL" değeri geçildiğinde), ilgili "rwlock" nesnesi prosesler arasında kullanımı YOKTUR. -> Daha sonra bu "attribute" nesnesi "pthread_rwlock_init" fonksiyonunda kullanılır ve böylelikle ilgili özelliklerde bir "rwlock" nesnesi ilk değerini alır. -> En sonunda da "pthread_rwlockattr_destroy" ile iş bu "attribute" nesnesini yok etmeliyiz. Bu yok etme işlemini, ilgili "rwlock" nesnesine ilk değer verdikten hemen sonra da gerçekleştirebiliriz. Bu parametreye "NULL" adres geçilmesi durumunda varsayılan değerler kullanılacaktır. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. -> Kritik Kod bölgesi oluşturmak için elimizde iki adet fonksiyon bulunmaktadır. Bunlar "pthread_rwlock_rdlock" ve "pthread_rwlock_wrlock" isimlerinde olup, sırasıyla "read" ve "write" işlemleri için kullanılacaktır. Burada niyetimize göre iki farklı "lock" fonksiyonu mevcuttur. Fakat kilitlenen "rwlock" nesnesinin kilidini açmak için sadece "pthread_rwlock_unlock" fonksiyonu kullanılmaktadır. Fonksiyonların prototipi ise aşağıdaki gibidir: #include int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); Fonksiyonlar argüman olarak ilgili "rwlock" nesnesinin adresini almaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Tabii buradaki "rwlock" nesnesini kilitleyen fonksiyonların "try" lı versiyonları da vardır ki o fonksiyonlar kilitlenmiş nesne karşısında başarısızla geri dönmektedir. Bunlar "pthread_rwlock_tryrdlock" ve "pthread_rwlock_trywrlock" isimli fonksiyonlardır. -> İlgili "rwlock" nesnesi ile işimiz bittikten sonra "destroy" işlemi gerçekleştirilmelidir. Bunun için "pthread_rwlock_destroy" fonksiyonu çağrılmalıdır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); Fonksiyon argüman olarak ilgili "rwlock" nesnesinin adresini almaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Aşağıda kullanıma ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıda "rwlock" nesnesinin kullanımına ilişkin bir örnek verilmiştir: #include #include #include #include #include #include void exit_sys_errno(const char *msg, int err); void *thread_proc1(void *param); void *thread_proc2(void *param); void *thread_proc3(void *param); void *thread_proc4(void *param); pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; int main(void) { int result; pthread_t tid1, tid2, tid3, tid4; if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid3, NULL, thread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid4, NULL, thread_proc4, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid4, NULL)) != 0) exit_sys_errno("pthread_join", result); pthread_rwlock_destroy(&g_rwlock); return 0; } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc1(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 12345; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread1 ENTERS to critical section for READING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread1 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } void *thread_proc2(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 23456; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread2 ENTERS to critical section for READING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread2 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } void *thread_proc3(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 35678; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_wrlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread3 ENTERS to critical section for WRITING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread3 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } void *thread_proc4(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 356123; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_wrlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread4 ENTERS to critical section for WRITING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread4 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } >> "threads" ve "fork" / "exec" işlemi: Anımsayacağımız üzere birden fazla "thread" e sahip bir proseste "fork" işlemi yapıldığında hayata gelen alt proses her zaman tek bir "thread" e sahip olur. Bu "thread" ise "fork" çağrısını yapan "thread" dir. Örneğin, 10 adet "thread" oluşturalım ve bunlardan sadece bir tanesi "fork" çağrısı yapmış olsun. Artık bu çağrıyı yapan üst proses olurken, çağrı sonucunda hayata gelen ise alt proses olacaktır. Her ne kadar bu süreç sırasında üst prosesin bellek alanı alt prosese kopyalansada, alt proseste sadece bir adet "thread" akışı olacaktır. Bu da üst prosesin akışıdır. Fakat şunu da belirtmek gerekirki "fork" işlemi ile "thread" lerin bir arada kullanmaktan kaçınmalıyız. * Örnek 1, Aşağıdaki örnekte de görüleceği üzere "main thread" içerisinde iki adet "thread" meydana gelmesine rağmen, tek bir akış yeniden oluşmuştur. Toplamda üç adet akış vardır. #include #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # thread - 1: 0 Child Process terminated. thread - 2: 0 Parent Process thread - 1: 1 thread - 2: 1 thread - 1: 2 thread - 2: 2 */ int result; pthread_t tid1, tid2; pid_t pid; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((pid = fork()) == 1) exit_sys("fork"); if(pid != 0){ printf("Parent Process\n"); } else{ printf("Child Process terminated.\n"); pthread_exit(NULL); } if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; for(i = 0; i < 3; ++i){ printf("thread - 1: %d\n", i); sleep(1); } return NULL; } void *thread_proc2(void *param) { int i; for(i = 0; i < 3; ++i){ printf("thread - 2: %d\n", i); sleep(1); } return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise "thread" lerden sadece bir tanesi "fork" yapmaktadır. Yine toplamda üç adet "thread" akışı vardır. #include #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # thread - 1: 0 thread - 2: 0 thread - 1: 1 thread - 2: 1 thread - 1: 2 thread - 2: 2 thread - 1: 3 thread - 2: 3 thread - 2: 4 thread - 1: 4 thread - 1: 4 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; pid_t pid; for(i = 0; i < 5; ++i){ printf("thread - 1: %d\n", i); if(i == 3) if((pid = fork()) == -1) exit_sys("fork"); sleep(1); } if(pid != 0 && waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return NULL; } void *thread_proc2(void *param) { int i; for(i = 0; i < 5; ++i){ printf("thread - 2: %d\n", i); sleep(1); } return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } Bir diğer yandan şunu da söylemek gerekir ki bir prosesin son "thread" i sonlandığı vakit, işletim sistemi tarafından sonlandırılır. Öte yandan birden çok "thread" in bulunduğu ortamlarda ve bir "thread" in "fork" işlemi yapması durumunda, üst ve alt proseslerde bazı işlerin yapılması gerekmektedir. Bunu gerçekleştirebilmek için "pthread_atfork" fonksiyonunu kullanmamız gerekmektedir. >>> "pthread_atfork" : Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); Fonksiyonun üç parametresi de bir fonksiyon göstericisidir. "prepare" isimli parametreye geçilen fonksiyon "fork" işlemi yapılmadan evvel üst proses tarafından, "parent" ise "fork" işleminden sonra üst proses tarafından ve "child" ise "fork" işleminden sonra alt proses tarafından çağrılmaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Pek tabii bu fonksiyon da birden fazla kez çağrılabilir. Bu durumda en sonki çağrıya bağlı olan fonksiyonlar ilk olarak çağrılacaktır. * Örnek 1, Aşağıdaki örnekte ID değerlerinin aynı olmasının sebebi, "fork" işlemi sırasında "ID" bilgisinin de üst proses olan "thread" den kopyalanmasıdır. #include #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void prepare(void); void parent(void); void child(void); void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # thread - 1: 0 thread - 2: 0 thread - 1: 1 thread - 2: 1 thread - 1: 2 thread - 2: 2 thread - 1: 3 void prepare(void) : 140110887732800 thread - 2: 3 void parent(void) : 140110887732800 void child(void) : 140110887732800 thread - 2: 4 thread - 1: 4 thread - 1: 4 */ int result; pthread_t tid1, tid2; if((result = pthread_atfork(prepare, parent, child)) != 0) exit_sys_errno("pthread_atfork", result); if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; pid_t pid; for(i = 0; i < 5; ++i){ printf("thread - 1: %d\n", i); if(i == 3) if((pid = fork()) == -1) exit_sys("fork"); sleep(1); } if(pid != 0 && waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return NULL; } void *thread_proc2(void *param) { int i; for(i = 0; i < 5; ++i){ printf("thread - 2: %d\n", i); sleep(1); } return NULL; } void prepare(void) { printf("void prepare(void) : %llu\n", (unsigned long long)pthread_self()); } void parent(void) { printf("void parent(void) : %llu\n", (unsigned long long)pthread_self()); } void child(void) { printf("void child(void) : %llu\n", (unsigned long long)pthread_self()); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } Bütün bunlara ek olarak birden çok "thread" in kullanıldığı uygulamalarda "fork" yaparken senkronizasyon nesnelerine de dikkat etmeliyiz. Çünkü "fork" sırasında başka bir "thread" ilgili senkronizasyon nesnesini kilitleyebilir. Bu durumda hayata gelen alt prosesin senkronizasyon nesnesi kilitli olacaktır. Eğer alt proseste bu senkronizasyon nesnesi kullanılmak istenirse, zaten kilitli olduğu için, "deadlock" oluşacaktır. Çünkü kiliti ancak kilitleyen açacaktır. Örneğin, bir "thread" aşağıdaki gibi bir Kritik Kod bölgesi oluştursun: void thread_proc1(){ //... pthread_mutex_lock(&g_mutex); // => Bir başka "thread", bu "thread" in akışı tam da buradayken, "fork" çağrısı yapsın. pthread_mutex_unlock(&g_mutex); //... } "fork" çağrısı yapan "thread" ilgili "mutex" nesnesini kilitlememiştir fakat "fork" çağrısından dolayı bellek alanı kopyalancağı için ilgili "mutex" nesnesi de kopyalanacaktır. Fakat kilitli bir biçimde kopyalanacaktır. Artık alt prosesteki bu "mutex" nesnesinin açılmasının bir olanağı yoktur. İşte böylesi bir senaryoda "pthread_atfork" fonksiyonu yararlı olabilir. Şöyleki; -> "fork" işleminden evvel, "prepare" argümanına geçilen fonksiyon içerisinde, kullanılan "mutex" nesnesinin kilidi açılabilir. Böylelikle bu "mutex" nesnesi alt prosese kilidi açık bir biçimde geçecektir. -> Üst proses olarak nitelenen diğer "thread" ler ilgili "mutex" nesnesini tekrardan kilitleyebilir. Yani, yukarıda belirtilen noktaya dikkat etmeliyiz. Eğer "fork" işleminden hemen sonra "exec" işlemi yapmak, sadece "fork" yapmaya nazaran, daha az problemlidir. Benzer şekilde "fork" yapmadan sadece "exec" yapmamız halinde, üst prosesteki halihazırdaki diğer "thread" ler sonlanır ve yeni proses tek bir "thread" ile hayata gelir. Yine bu durumda da prosesin bellek alanının gittiğini unutmayınız. Özetle birden fazla "thread" in olduğu ortamlarda şu noktalara dikkat etmeliyiz: -> Eğer amacımız "exec" ise "fork" ve "exec" yapmakta bir problem yoktur. Fakat "exec" yapmayacakak, sadece "fork" yapmak kötü bir tekniktir. -> "fork" işleminden sonra alt proseste sadece bir "thread" olur. -> "fork" yapmadan direkt olarak "exec" yaparsak, o ana kadarki bütün "thread" ler sonlandırılı. Sadece "exec" çağrısı yapan hayatta kalır ki bu da çağrıyı yapabilsin. >> "thread" lerin çizelgelenmesi: İşletim sistemlerinde proseslerin ve "thread" lerin CPU'ya atanıp zaman paylaşımlı olarak çalıştırılmasına proseslerin çizelgelenmesi denmektedir. "thread" ler olmadan evvel çizelgelenen şeyler proseslerin kendisiydi fakat "thread" lerin gelmesi ile birlikte artık "thread" ler çizelgelenmektedir. İşletim sisteminin "thread" leri çizelgelemekte kullandığı algoritmalara ise adı üzerinde Çizelgeleme Algoritmaları denmektedir. Tabii tarih boyunca çeşitli algoritmalar çeşitli işletim sistemlerinde geliştirilmeye çalışılmıştır. Böylelikle "interactivity" ve "throughput" arasında bir denge sağlanmaya çalışılmıştır. Günümüzde en yaygın kullanılan çizelgeleme algoritması ise "Round Robin Scheduling" denilen Döngüsel Çizelgeleme algoritmasıdır. >>> "Round Robing Scheduling" : Bu çizelgeleme algoritmasında "thread" ler bir Çalışma Kuyruğunda tutulur ve sırası gelen CPU'ya atanır. "quanta" süresi bittikten sonra zorla CPU'dan kopartılır ve yerine çalışma kuyruğunda bulunan sıradaki "thread" atanır. Bir döngü içerisinde bu mekanizma işletilir. CPU'da çalıştırılan "thread" bloke olursa ilgili "thread" çalışma kuyruğundan çıkartılır ve Bekleme Kuyruğuna alınır. Blokenin kalkması durumunda ilgili "thread" tekrardan çalışma kuyruğuna alınır ki bu kuyruklar arasındaki işlem genellikle Kesme Mekanizması ile uygulanır. * Örnek 1, "sleep" fonksiyonunu ele alalım. Bu çağrıyı yapan "thread" ilk başta CPU'ya atanır. Programın akışı "sleep" çağrısına geldiğinde CPU'dan kopartılır ve "sleep" için oluşturulan bekleme kuyruğuna alınır. Pekiyi işletim sistemi sürenin tamamlandığını nasıl anlamaktadır? Bu noktada devreye yine Zaman Kesmeleri ("Timer Interrupts") girmektedir. Linux sistemlerinde bir bir milisaniyede bir zaman kesmesi oluşur ve işletim sisteminin kodu devreye girer. Buna "jiffy" de denir. Devreye giren bu kod "sleep" için oluşturulan bekleme kuyruklarını da kontrol etmekte, süresi bitenleri tekrardan çalışma kuyruklarına aktarmaktır. * Örnek 2, bir "thread" bir soketten okuma yapmak istesin ancak ilgili sokete henüz bilgi gelmemiş olsun. İşte işletim sistemi bu "thread" i bloke edip, çalışma kuyruğundan çıkarmakta ve ilgili bekleme kuyruğuna eklemektedir. Eğer sokete bilgi gelirse, yine bir kesme oluşur. Bu da gelen bilginin işletim sistemi tarafından alınmasına neden olur. Böylece bilginin gelmesini bekleyen "thread" uyandırılıp çalışma kuyruğuna yerleştirilir. Örneklerden de görüleceği üzere bir "t" anında çalışma kuyruklarında belli bir adet "thread" bulunurken, bekleme kuyruklarında da belli bir adet "thread" blokeli haldedir. Tabii buradan da diyebiliriz ki her bir olay için özel bir bekleme kuyruğu oluşturulmaktadır. Şunu da belirtmekte fayda vardır ki bu çizelgeleme algoritması da birden çok varyasyona sahiptir. Örneğin, Windows sistemleri Öncelik Sınıfı Temelinde Döngüsel Çizelgeleme ismindeki halini kullanırken Linux sistemleri ilk başlarda özel bir isim verilmeyen fakat yine döngüsel çizelgeleme algoritması kullanmaktadır. 2.6 nolu sürümden sonra O(1) Çizelgelemesi, 2.6.23 nolu sürüm ile birlikte "Completely Fair Scheduling" ismindeki çizelgelemeyi kullanmaktadır. UNIX türevi sistemlerde ise proseslerin çizelgeleme politikaları ("Process Scheduling Policy") bulunmaktadır. Bu politikalardan bazıları "SCHED_OTHER" ve "SCHED_NORMAL" ismindeki politikalardır. Her proses bir çizelgeleme politikasına sahiptir. POSIX standartlarınca proseslerin çizelgeleme politikaları şunlardan birisi olabilir: "SCHED_FIFO", "SCHED_RR", "SCHED_OTHER" ve "SCHED_SPORADIC. Bu çizelgelerden "SCHED_SPORADIC" olan opsiyonel olarak bırakılmıştır ve Linux sistemlerinde desteklenmemektedir. Yine POSIX standartlarınca "SCHED_FIFO" ve "SCHED_RR" olanlarına Gerçek Zamanlı Çizelgeleme Politikaları da denmektedir ve açıkça POSIX standarlarında tanımlanmışlardır. Fakat bunlar tam manası ile gerçek zamanlı değillerdir. Yani bu politikaları takip eden prosesler, gerçek zamanlı işletim sistemlerindeki gibi davranmayacaktır. Öte yandan "SCHED_OTHER" ise "implementation defined" olarak bırakılmıştır ve POSIX standartlarında varsayılan politika budur. Ayrıca o işletim sisteminde "SCHED_OTHER" politikası "SCHED_FIFO" veya "SCHED_RR" olarak da implemente edilebilir, POSIX standartları buna izin vermektedir. Diğer yandan prosesin çizelgeleme politikası "fork" işlemi sırasında üst prosesten alınmaktadır. Linux sistemlerinde varsayılan politika "SCHED_OTHER" biçimindedir (Linux dünyasında bu politikaya "SCHED_NORMAL" da denmektedir fakat "SCHED_NORMAL" politikası POSIX standartlarında geçmemektedir). Yine POSIX standartlarınca bir prosesin çizelgeleme politikası, onun bütün "thread" lerinin de politikasıdır. Dolayısıyla bir prosesin çizelgeleme politikasını değiştirisek, o prosesin bütün "thread" lerinin çizelgeleme politikası da değişecektir. FAKAT Linux SİSTEMLERİ BUNA UYMAMAKTADIR. Linux SİSTEMLERİNDE SADECE "main thread" ÇİZELGELEME POLİTİKASI DEĞİŞMEKTEDİR. Bir prosesin çizelgeleme politikasının ne olduğu bilgisi, Proses Kontrol Bloğu içerisinde saklanmaktadır. Şimdi de bu politikaları inceleyelim: >>> "SCHED_OTHER" : POSIX standartlarınca o işletim sistemini yazanlarına bırakılmıştır. Fakat POSIX standartları, bu politikayı kullananlar proseslerin CPU kullanma miktarları konusunda etkili olabilecek bir kavram da tanımlamıştır ki bu kavram "nice" değeridir. Bu "nice" değerini açıklamadan evvel "SCHED_OTHER" politikasının ne anlam ifade ettiğine değinelim; bu politika kabaca şu yönergeleri içermektedir: -> İşletim sistemi, çalışma kuyruğundaki her bir "thread" için bir "quanta" sayaç değeri tutar. Linux sistemlerinde bu değer "task_struct" yapısı içerisinde saklanır (Anımsayacağımız üzere Linux sistemlerinde "thread" ler de tıpkı prosesler gibi "task_struct" yapısı ile temsil edilirler). Örneğin, bu sayaç değerinin 200 olduğunu varsayalım. -> Bir "thread" CPU'ya atandıktan sonra her bir Zaman Kesmesi olduğunda, ona ait "quanta" sayaç değeri bir eksiltilir. Günümüzdeki bu zaman kesmesi bir milisaniye olduğundan, her bir milisaniyede bu değer bir eksilecektir. Tıpkı 199, 198... -> Bu "quanta" sayaç değeri sıfıra düştüğünde, "thread" ler arası geçiş meydana gelir ve ilgili "thread" CPU'dan kopartılır. Daha sonra çalışma kuyruğundaki diğer "thread" CPU'ya atanır. Genel olarak "quanta" sayaç değeri daha büyük olan "thread" CPU'ya önce atanır. Örneğin, çalışma kuyruğunda toplam beş adet "thread" olsun. Bunların "quanta" sayaç değerleri de sırasıyla 18, 89, 0, 120, 5 biçiminde olsun. Bu durumda, "thread" ler arası geçiş meydana geldiğinde, "quanta" sayaç değeri 120 olan "thread" CPU'ya atanacaktır. -> Çalışma kuyruğundaki bütün "thread" lerin "quanta" sayaç değerleri sıfıra düştükten sonra sayaç değerleri tekrardan doldurulmaktadır. Dolayısıyla "quanta" sayacı sıfır olan "thread" ler, diğer "thread" lerin de sayaçları sıfır olana dek CPU'ya atanmamaktadır. -> Bütün bu işleyiş sırasında, "quanta" sayaç değerinin ne olduğuna bakılmaksızın, bir "thread" bloke olursa çalışma kuyruğundan çıkartılır ve ilgili bekleme kuyruğuna alınır. Artık işletim sistemi bu "thread" i tekrardan CPU'ya atamak için çizelgelemez. Ancak işletim sistemi, çalışma kuyruklarındaki bütün "thread" lerin "quanta" sayaçları bittikten sonra ilgili sayaçlara doldurma yaparken, bekleme kuyruklarındakileri de göz önüne alır. Fakat bekleme kuyruklarındakilere doldurma yaparken daha fazla doldurma yapmaktadır. Böylelikle ilgili "thread" in uyanması durumunda, çalışma kuyruğundaki diğer "thread" lerin "quanta" sayaçlarından daha fazla değere sahip olacaktır. Böylesi bir yaklaşımın daha adil olacağı düşünülmektedir. Pekiyi bütün bunlar göz önüne alındığında, bir "thread" in "quanta" sayacının doldurma işlemi sırasında alacağı değer ne olacaktır? İşte burada "thread" in "nice" değeri devreye girmektedir. Bu değer ile orantılı olacak bir biçimde "quanta" sayaç değerleri doldurulmaktadır. Fakat POSIX standartları bu "nice" değerinin etkisinin tam olarak nasıl olacağını belirtmemiştir. Bu değerin detayları sistemden sisteme değişiklik gösterebilir. Son olarak şunu da belirtmekte fayda vardır ki yukarıda anlatılanlar ilgili politikanın kaba halidir, ayrıntılarına değinilmemiştir. Şimdi de bu "nice" değerini inceleyelim: >>>> "nice" değeri : POSIX standartlarınca bu değer sıfır ile "2*NZERO-1" değeri arasında bir değer almaktadır. inux sistemlerinde "NZERO" değeri "20" olduğu için "nice" değeri [0, 39] değerleri arasında bir değer alacaktır. Bu değerin rakamsal olarak büyük olması daha düşük önceliğe sahip olunacağını belirtmektedir. Bu "NZERO" değeri ise UNIX türevi sistemler için orta noktayı temsil etmektedir. Pekiyi bizler bu "nice" değeri ile nasıl oynama yapabilir miyiz? Burada "setpriority" / "getpriority" ve "nice" / "renice" isimli POSIX fonksiyonları devreye girmektedir. >>>>> "setpriority" / "getpriority" : Fonksiyonların prototipi aşağıdaki gibidir: #include int getpriority(int which, id_t who); int setpriority(int which, id_t who, int value); Fonksiyonların birinci parametresi olan "which" parametresi, şu değerlerden birisi olabilir: "PRIO_PROCESS", "PRIO_PGRP" ve "PRIO_USER". Bu bayraklardan, -> "PRIO_PROCESS" : Yalnızca tek bir prosesin "nice" değerinin temin etmek / değiştirmek için. -> "PRIO_PGRP" : Bir proses grubundaki bütün proseslerin "nice" değerini temin etmek / değiştirmek için. -> "PRIO_USER" : Belli bir Etkin Kullanıcı ID değerine ilişkin bütün proseslerin "nice" değerini temin etmek / değiştirmek için. Fonksiyonların ikinci parametresi olan "who" parametresi, "which" isimli parametreye göre, sırasıyla ilgili prosesin Proses ID, Process Grup ID ve Ekin Kullanıcı ID değeri olmaktadır. Bu nedenden dolayı "id_t" isimli bir tür "typedef" edilmiştir ki bu ise arka planda "pid_t" ve "uid_t" türlerinin her ikisini de ifade edebilecek bir türdür. Bu parametreye "0" değerini geçmemiz halindeyse, "which" parametresi yerine çağrıyı yapan prosesinkiler baz alınacaktır. Fonksiyonun üçüncü parametresi olan "value" ise aslında "nice" değeri olup, "NZERO" değeri ile toplama anlamındadır. Dolayısıyla bu parametreye "10" geçmemiz, "NZERO" değeri de "20" ise, "nice" değeri "30" olacaktır. Bu durumda ilgili "thread" in önceliği düşecektir. Bu parametreye "-10" geçmemiz halinde ise yeni "nice" değeri "10" olacaktır, eğer "NZERO" da "20" ise. Bu durumda ilgili "thread" in önceliği yükselmiş olacaktır. Ancak POSIX standartlarına göre, bu üçüncü parametreye geçeceğimiz değerler sonucunda "nice" değeri "[0,39]" aralığından çıkıyorsa fonksiyon başarısız olmaz. Bu aralıktaki minimum ya da maksimum değerini alır. "setpriority" fonksiyonunun geri dönüş değeri ise başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmektedir. "getpriority" fonksiyonu ise başarı durumunda "NZERO" değerine, hata durumunda "-1" değerine geri dönmektedir. Fakat bu fonksiyonunun "-1" değeri döndürmesi iki anlamlıdır; ya gerçekten de başarısızlık olundu ya da "NZERO" değeri olarak "-1" geri döndürüldü. Bunu ayırt etmek için de aşağıdaki gibi bir yöntem uygulanabilir: errno = 0; if((result = getpriority(...)) == -1 && errno != 0) exit_sys("getpriority"); Öte yandan bizler herhangi bir prosesin "nice" değerini elde edebiliriz, bu konuda bir erişim kontrolü uygulanmamaktadır. Dİğer yandan biz "getpriority" fonksiyonu ile bir proses grubunun ya da belli bir etkin kullanıcı ID değerine ilişkin proseslerin "nice" değerlerini elde ederken hangi prosesinkini elde etmiş olacağız? POSIX standartlarınca en düşük "nice" değerini bize vermektedir, yani önceliği en yüksek olanınki. Ancak "setpriority" fonksiyonu ile bir prosesin "nice" değerini düşürmek istiyorsak, prosesimiz "priviledged" olması gerekmektedir. Eğer yükseltmek istiyorsak, prosesimizin Kullanıcı ID değerleri ile hedef prosesin Kullanıcı ID değerleri aynı olmalıdır. Özetle; -> Bizler herhangi bir prosesin "nice" değerini yükselterek onun daha az CPU zamanı harcamasını sağlayabilmemiz için hedef prosesin Etkin Kullanıcı ID değeri ile bizim Etkin ya da Gerçek Kullanıcı ID değeri ile aynı olması gerekmektedir. Başka bir deyişle sadece kendi proseslerimizin "nice" değerini yükseltebiliriz. -> Herhangi bir prosesin "nice" değerini düşürmek için prosesimizin "priviledged" olması gerekmektedir. Burada prosesimizin "root" ya da "CAP_SYS_NICE" yeterliliğine sahip olması gerekmektedir. Yine "setpriority" fonksiyonu ile bir prosesin "nice" değerini değiştirirsek, POSIX standartlarınca, o prosesin bütün "thread" lerinin "nice" değeri değişecektir. Fakat Linux sistemlerinde ise sadece "main-thread" inki değişecektir. Bu fonksiyonu çağıran "thread" başka bir "thread" oluşturursa, yine bu "nice" değeri de aktarılacaktır. Aşağıda bu fonksiyonların kullanımına ilişkin örnekler verilmiştir: * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # Priority : 0 Priority : 10 */ int result; errno = 0; if((result = getpriority(PRIO_PROCESS, 0)) == -1 && errno != 0) exit_sys("getpriority"); printf("Priority : %d\n", result); if(setpriority(PRIO_PROCESS, 0, 10)) exit_sys("setpriority"); result = getpriority(PRIO_PROCESS, 0); printf("Priority : %d\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # Priority : 0 setpriority: Permission denied */ int result; errno = 0; if((result = getpriority(PRIO_PROCESS, 0)) == -1 && errno != 0) exit_sys("getpriority"); printf("Priority : %d\n", result); if(setpriority(PRIO_PROCESS, 0, -1)) exit_sys("setpriority"); errno = 0; if((result = getpriority(PRIO_PROCESS, 0)) == -1 && errno != 0) exit_sys("getpriority"); printf("Priority : %d\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte kendi prosesimizin "nice" değerini önce yükseltmek, sonra da düşürmek istedik. #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char** argv) { /* # Command Line Arguments # ./sample 0 1 ./sample 0 -1 */ /* # OUTPUT # Success.. setpriority: Permission denied */ if(argc != 3){ fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } pid_t pid = (pid_t)atol(argv[1]); int prio = atoi(argv[2]); if(setpriority(PRIO_PROCESS, pid, prio) == -1) exit_sys("setpriority"); printf("Success...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>>> "nice" fonksiyonu: Sadece kendi prosesinin "nice" değerini değiştirmektedir. Prototipi aşağıdaki gibidir. #include int nice(int incr); Bu fonksiyon parametre olarak aldığı değeri, o anki "nice" değeri üzerine eklemektedir. POSIX standartlarına göre, prosesin bütün "thread" leri üzerinde etkili olmaktadır. Ancak Linux sistemlerinde ise sadece bu fonksiyonu çağıran "thread" inkini değiştirmektedir. "setpriority" fonksiyonunda olduğu gibi, bir "thread" başka bir "thread" oluştururken, "nice" değeri de aktarılmaktadır. Yine başarı durumunda yeni "nice" değerini döndürmekte, hata durumunda ise "-1" ile geri dönmektedir. Fakat yeni "nice" değerinin "-1" olması durumunda yine "-1" ile döneceği için yukarıdaki gibi "errno" değişkenini kullanarak hata kontrolü yapmalıyız. * Örnek 1, Bu program, notların devamında işlenecek olan "nice" kabuk komutunda kullanılmıştır. #include #include #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # Priority : 0 Priority : -2 Priority : -4 */ int result; errno = 0; if((result = nice(0)) == -1 && errno != 0) exit_sys("nice"); printf("Priority : %d\n", result); errno = 0; if((result = nice(-2)) == -1 && errno != 0) exit_sys("nice"); printf("Priority : %d\n", result); errno = 0; if((result = nice(-2)) == -1 && errno != 0) exit_sys("nice"); printf("Priority : %d\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki program ise "nice" komutu ile birlikte ve komut olmadan çalıştırılmıştır. #include #include #include #include #include #include int set_nice_value(int new_value); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # "shell" # sudo ./sample sudo nice --5 ./sample */ /* # OUTPUT # New Nice Value: 5 Old Nice Value: 0 New Nice Value: 0 Old Nice Value: -5 */ int old_value = set_nice_value(5); printf("Old Nice Value: %d\n", old_value); return 0; } int set_nice_value(int new_value) { int old_value; errno = 0; if((old_value = nice(0)) == -1 && errno != 0) exit_sys("nice"); errno = 0; int result; if((result = nice(new_value)) == -1 && errno != 0) exit_sys("nice"); printf("New Nice Value: %d\n", result); return old_value; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } Şimdi de "gettid()" fonksiyonunu inceleyelim: >>>>> "gettid" fonksiyonu: Bir Linux fonksiyonudur. Prototipi aşağıdaki gibidir. #define _GNU_SOURCE #include pid_t gettid(void); Fonksiyon başarısız olamamaktadır. Başarı durumunda ise kendisini çağıran "thread" in ID değerine geri dönmektedir. Bu fonksiyonu kullanırken kaynak kodun en üst kısmına "_GNU_SOURCE" makrosunu bildirmeliyiz. Buna alternatif olarak "-D _GNU_SOURCE" biçimindeki komut satırını da kullanabiliriz: * Örnek 1, #define _GNU_SOURCE #include #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Process ID : 14715 Main-Thread ID : 14715 Thread-I ID : 14719 */ printf("Process ID : %jd\n", (intmax_t)getpid()); printf("Main-Thread ID : %jd\n", (intmax_t)gettid()); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pid_t tid; tid = gettid(); printf("Thread-I ID : %jd\n", (intmax_t)tid); return NULL; } void exit_sys(const char* msg){ perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda ise spesifik bir "thread" in ID değeri "setpriority" fonksiyonu ile değiştirilmiştir. Fakat bu davranışın Linux sistemlere özgü olduğunu unutmayınız. #include #include #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Main-Thread Priority(old): 0 Thread Priority(old): 0 Thread Priority(new): 10 Main-Thread Priority(old): 0 */ int prio; errno = 0; if((prio = getpriority(PRIO_PROCESS, getpid())) == -1 && errno != 0) exit_sys("getpriority"); printf("Main-Thread Priority(old): %d\n", prio); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); errno = 0; if((prio = getpriority(PRIO_PROCESS, getpid())) == -1 && errno != 0) exit_sys("getpriority"); printf("Main-Thread Priority(old): %d\n", prio); return 0; } void* thread_proc(void* param){ pid_t tid; tid = gettid(); int prio; errno = 0; if((prio = getpriority(PRIO_PROCESS, tid)) == -1 && errno != 0) exit_sys("getpriority"); printf("Thread Priority(old): %d\n", prio); if(setpriority(PRIO_PROCESS, tid, 10) == -1) exit_sys("setpriotity"); errno = 0; if((prio = getpriority(PRIO_PROCESS, tid)) == -1 && errno != 0) exit_sys("getpriority"); printf("Thread Priority(new): %d\n", prio); return NULL; } void exit_sys(const char* msg){ perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Yine burada unutmamalıyız ki POSIX standardına göre sadece bir adet "thread" ID değeri bulunmaktadır ki bu da aslında "main-thrad" e ilişkin olup, prosesin ID değeri olarak da geçmektedir. Dolayısıyla bu ID değerini kullanarak "nice" değerini değiştirdiğimizde POSIX standartlarına göre ilgili prosesteki bütün "thread" ler, Linux sistemlerinde ise o ID değerine sahip spesifik "thread" in "nice" değeri değişmiş olacaktır. Diğer yandan akla şu gelmektedir; nadem "nice" ve "setpriority" fonksiyonları bir prosesin "main-thread" i üzerinde etkili olmaktadır, belli bir "thread" in "nice" değerini POSIX standartlarına uygun biçimde nasıl değiştirebiliriz? Anımsayacağınız üzere Linux sistemlerinde her bir "thread" bir "task_struct" yapısına sahiptir. Dolayısıyla prosesin ID değeri "task_struct" yapısı içerisinde saklandığından, "thread" e özgüdür. Diğer yandan yine "task_struct" yapısı içerisindeki "thread_group" isimli bağlı liste, "main-thread" e ilişkin bütün "thread" lerin "task_struct" yapılarını tutmaktadır. Pekiyi bir "thread" e ilişkin olan proses ID değerine nasıl ulaşacağız? İşte bunun için "Linux-Specific" olan "gettid()" isimli fonksiyonu çağırmalıyız. Hangi "thread" içerisinde çağrılmışsa, onun ID değerini döndürecektir. "getpid" gibi POSIX fonksiyonu ise "main-thread" e ilişkin ID değerini döndürecektir. Dolayısıyla bir "thread" in "nice" değerini değiştirmek için ilk önce "gettid()" ile onun ID değerini almalı, bu değeri "setpriority()" fonksiyonuna geçmeliyiz. Özetle; -> Linux sistemlerinde her bir "thread" için ayrı bir "task_struct" yapısı bulunmaktadır. Dolayısıyla her bir "thread" in ayrı bir ID değeri vardır. -> "main-thread" e ilişkin "task_struct" içerisindeki ID değeri, prosese ilişkin ID değeri olarak kullanılmaktadır. POSIX fonksiyonu olan "getpid" fonksiyonunu çağırırsak, prosese ilişkin ID değerini elde ederiz. -> Prosesin "main-thread" i içerisinde, o prosese ilişkin bütün "thread" lerin "task_struct" yapıları da tutulmaktadır. Dolayısıyla bu yapı kullanılarak bütün "task_struct" yapılarına erişilebilir. -> Her "task_struct" içerisinde bir ID varsa, belli bir "thread" in ID değerini ise bir Linux fonksiyonu olan "gettid" fonksiyonunu çağırmalıyız. Hangi "thread" içerisinde çağrılmışsa, ona ait olan ID değeri elde edilecektir. Şimdi de "shell" komutlarını kullanarak bir prosesin "nice" değerini değiştirmeyi, okumayı öğrenelim. >>>>> "ps" komutu: Proseslerin "nice" değerini görmek için kullanılır. Bu komut "proc" dosya sisteminden bilgileri almaktadır. Bu komutun kullanımı ise şöyledir; -> Sadece "ps" olarak çalıştırılırsa, içinde bulunulan terminalde ve kullanıcıya ilişkin prosesler kısa biçimde listelenir. Şöyleki: $ ps PID TTY TIME CMD 16 pts/0 00:00:00 bash 249 pts/0 00:00:00 ps -> "-l" seçeneği ile birlikte "ps -l" biçiminde kullanırsak, içinde bulunulan ve kullanıcıya ilişkin prosesler hakkında daha fazla bilgiler elde edeceğiz. Şöyleki: $ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 1000 16 15 0 80 0 - 1551 do_wai pts/0 00:00:00 bash 0 R 1000 250 16 0 80 0 - 1869 - pts/0 00:00:00 ps -> "-u" seçeneği ile birlikte "ps -u ahmopasa" biçiminde kullanırsak, "ahmopasa" ya ait bütün terminallerde çalışan prosesler hakkında bilgi verir. Şöyleki: $ ps -u ahmopasa PID TTY TIME CMD 16 pts/0 00:00:00 bash 108 pts/1 00:00:00 sh 109 pts/1 00:00:00 sh 114 pts/1 00:00:00 sh 118 pts/1 00:00:12 node 129 pts/1 00:00:00 node 142 pts/2 00:00:03 node 151 pts/3 00:00:06 node 175 pts/1 00:00:34 node 190 pts/1 00:00:10 node 219 pts/1 00:00:00 node 251 pts/0 00:00:00 ps Eğer bu seçeneği "-l" ile birlikte kullanırsak, detaylı bir şekilde sonuç elde etmiş olacağız. Şöyleki: $ ps -lu ahmopasa F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 1000 16 15 0 80 0 - 1551 do_wai pts/0 00:00:00 bash 4 S 1000 108 107 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 109 108 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 114 109 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 118 114 0 80 0 - 233954 - pts/1 00:00:12 node 0 S 1000 129 118 0 80 0 - 157259 - pts/1 00:00:00 node 4 S 1000 142 141 0 80 0 - 147383 do_epo pts/2 00:00:03 node 4 S 1000 151 150 0 80 0 - 146765 do_epo pts/3 00:00:06 node 0 S 1000 175 118 0 80 0 - 239904 - pts/1 00:00:34 node 0 S 1000 190 118 0 80 0 - 209607 - pts/1 00:00:10 node 0 S 1000 219 175 0 80 0 - 148137 - pts/1 00:00:00 node 0 R 1000 252 16 0 80 0 - 1869 - pts/0 00:00:00 ps -> "-t" veya "-tty" seçeneğini "ps -t pts/1" biçiminde kullanırsak, sadece o terminalde çalışan prosesler hakkında bilgi ediniriz. Şöyleki: $ ps -t pts/1 PID TTY TIME CMD 108 pts/1 00:00:00 sh 109 pts/1 00:00:00 sh 114 pts/1 00:00:00 sh 118 pts/1 00:00:12 node 129 pts/1 00:00:00 node 175 pts/1 00:00:34 node 190 pts/1 00:00:10 node 219 pts/1 00:00:00 node Yine bu seçeneği de "-l" ile birleştirip kullanabiliriz. Şöyleki: $ ps -l -t pts/1 F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 1000 108 107 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 109 108 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 114 109 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 118 114 0 80 0 - 233954 - pts/1 00:00:12 node 0 S 1000 129 118 0 80 0 - 157259 - pts/1 00:00:00 node 0 S 1000 175 118 0 80 0 - 239904 - pts/1 00:00:35 node 0 S 1000 190 118 0 80 0 - 209607 - pts/1 00:00:10 node 0 S 1000 219 175 0 80 0 - 148137 - pts/1 00:00:00 node -> "-o" seçeneğini kullanarak, ekrana yazdırılan sütunların sıralamasını değiştirebiliriz. -> "-T" seçeneğini kullanırsak, o prosese ait bütün "thread" leri ekrana yazdırmış olacağız. >>>>> "top" komutu: Bu komutunu çalıştırarak, proseslerin kullandığı kaynakları anlık bir şekilde görüntüleyebiliriz. Şöyleki: $ top top - 01:31:05 up 4:01, 0 users, load average: 0.00, 0.00, 0.00 Tasks: 21 total, 1 running, 20 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.0 us, 0.1 sy, 0.0 ni, 99.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st MiB Mem : 12664.5 total, 12095.1 free, 278.7 used, 290.7 buff/cache MiB Swap: 4096.0 total, 4096.0 free, 0.0 used. 12142.8 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 175 ahmopasa 20 0 962052 95960 35924 S 0.7 0.7 0:58.73 node 190 ahmopasa 20 0 838428 50348 32844 S 0.3 0.4 0:17.67 node 1 root 20 0 928 532 476 S 0.0 0.0 0:00.06 init 14 root 20 0 1276 376 20 S 0.0 0.0 0:00.00 init 15 root 20 0 1276 376 20 S 0.0 0.0 0:00.42 init 16 ahmopasa 20 0 6204 5036 3288 S 0.0 0.0 0:00.30 bash 106 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 107 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 108 ahmopasa 20 0 2884 944 856 S 0.0 0.0 0:00.00 sh 109 ahmopasa 20 0 2884 948 856 S 0.0 0.0 0:00.00 sh 114 ahmopasa 20 0 2884 928 836 S 0.0 0.0 0:00.00 sh 118 ahmopasa 20 0 936072 72460 36160 S 0.0 0.6 0:19.91 node 129 ahmopasa 20 0 629036 50052 33072 S 0.0 0.4 0:01.16 node 140 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 141 root 20 0 1016 116 20 S 0.0 0.0 0:03.44 init 142 ahmopasa 20 0 589532 44760 30684 S 0.0 0.3 0:06.83 node 149 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 150 root 20 0 1016 116 20 S 0.0 0.0 0:06.51 init 151 ahmopasa 20 0 588512 43360 30604 S 0.0 0.3 0:12.92 node 219 ahmopasa 20 0 592548 47572 32448 S 0.0 0.4 0:00.79 node 448 ahmopasa 20 0 7788 3664 3068 R 0.0 0.0 0:00.00 top "q" tuşuna basarak da bu komutu sonlandırabiliriz. >>>>> "nice" komutu : Eğer bir programı belli bir "nice" değeri ile çalıştırmak istiyorsak, bu komutunu kullanabiliriz. Örneğin, $ sudo nice -5 ./sample şeklinde bir komut çalıştırdığımız zaman, Priority : 5 Priority : 3 Priority : 1 şeklinde bir sonuç elde etmekteyiz. Eğer, $ sudo ./sample şeklinde bir komut çalıştırırsak da , Priority : 0 Priority : -2 Priority : -4 şeklinde bir sonuç elde edeceğiz. Görüleceği üzere ilgili program "5" "nice" değeri ile çalıştırılmıştır. Eğer negatif bir değer ile çalıştırılmasını istiyorsak aşağıdaki biçimde kullanmalıyız: $ sudo nice --5 ./sample Bu durumda aşağıdaki biçimde bir çıktı elde edeceğiz: Priority : -5 Priority : -7 Priority : -9 Burada kullanılan "sample" programı, "nice" POSIX fonksiyonunun açıklandığı kısımdaki örnek programdır. >>>>> "renice" komutu : Arka planda "setpriority" fonksiyonunu kullanarak, belli bir prosesin "nice" değerini değiştirmektedir. Bunun için ilgili prosesin halihazırda çalışıyor olması gereklidir. Örneğin, $ renice -1 -p 6827 komutunu çalıştırdığımız zaman "PID" değeri 6827 olan prosesin "nice" değeri "1" azaltılacaktır. Tabii biz burada tek çekirdekli işletim sistemlerini baz alarak değerlendirme yaptık. Birden çok çekirdeğin bulunduğu sistemlerde çoğu işletim sistemi, her bir çekirdeğin çalışma kuyruğunu birbirinden ayırmaktadır. Tabii bir çekirdeğin çalışma kuyruğundaki "thread" ler azalırsa, diğer çalışma kuyruğundaki "thread" lerin bu kuyruğa aktarılabilir. Örneğin, Linux'un "O(1)" çizelgeleme algoritmasında tek bir çalışma kuyruğu bulunmakta, işi biten CPU'lara atamalar bu tek kuyruktan yapılmaktaydı. Günümüzde bu yaklaşımı LCW mağazalarındaki kasa kuyrukları örnek gösterilebilir. Fakat Linux'taki "CFS" algoritması ile birlikte her çekirdek için ayrı bir çalışma kuyruğu oluşturulmakta, gerektiği durumlarda kuyruk arasında "thread" aktarımı gerçekleşmektedir. >>> "SCHED_FIFO" ve "SCHED_RR" : Varsayılan çizelgeleme algoritmaları değildir fakat ikisi de prosese özgüdür. Tabii POSIX standartlarına göre bir prosesin çizelgeleme algoritması bunlardan birisi seçildiğinde, yine bütün "thread" lerinin de çizelgeleme algoritması etkilenmektedir. Ancak POSIX, belli bir "thread" in çizelgeleme algoritmasının bunlardan birisi olarak seçilmesini mümkün kılmaktadır. Diğer yandan bu iki çizelgeleme politikası, "SCHED_OTHER" politikasından daha baskındır. Yani o an sistemde bu politika ile çalışabilecek bir "thread" varsa, bloke olmadıkça veya sonlanmadıkça, "SCHED_OTHER" olanlar CPU'ya atanmazlar. Öte yandan "SCHED_FIFO" veya "SCHED_RR" çizelgeleme algoritmasına sahip "thread" ler, "Statik Öncelik (Static Priority)" kavramına sahiptir. Bu kavram, "SCHED_OTHER" da bulunan "nice" değerinden farklıdır. POSIX standartları bu "static priority" kavramının alt ve üst limitlerinin ne olması gerektiği konusunda bir belirlemede bulunmamış fakat bu değerlerin programın çalışma zamanında elde edilebilmesi için şu iki fonksiyonu bulundurmaktadır: "sched_get_priority_max" ve "sched_get_priority_min". Bu fonksiyonlardan, >>>> "sched_get_priority_min" ve "sched_get_priority_max" : Fonksiyonların prototipi aşağıdaki gibidir: #include int sched_get_priority_max(int policy); int sched_get_priority_min(int policy); Fonksiyon parametre olarak çizelgeleme politikasının ismini, geri dönüş değeri olarak da "static priority" değerinin azami ve asgari değerlerini döndürmektedir. Başarısızlık durumunda ise "-1" değeri ile geri dönmektedirler. Eğer fonksiyona parametre olarak "SCHED_OTHER" değil, "SCHED_FIFO" veya "SCHED_RR" olarak girilmelidir. Aksi halde sonuç "implementation defined" olmaktadır. Diğer yandan "static priority" değeri sıfırdan küçük OLAMAMAKTADIR. * Örnek 1, Aşağıdaki örnekten de görüleceği üzere "SCHED_FIFO" ve "SCHED_RR" algoritmaları aynı "Static Priority" seviyesindedir. #include #include #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # SCHED_FIFO : [1,99] SCHED_RR : [1,99] */ int prio_min, prio_max; if((prio_min = sched_get_priority_min(SCHED_FIFO)) == -1) exit_sys("sched_get_priority_min"); if((prio_max = sched_get_priority_max(SCHED_FIFO)) == -1) exit_sys("sched_get_priority_max"); printf("SCHED_FIFO : [%d,%d]\n", prio_min, prio_max); if((prio_min = sched_get_priority_min(SCHED_RR)) == -1) exit_sys("sched_get_priority_min"); if((prio_max = sched_get_priority_max(SCHED_RR)) == -1) exit_sys("sched_get_priority_max"); printf("SCHED_RR : [%d,%d]\n", prio_min, prio_max); return 0; } void exit_sys(const char* msg){ perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Burada bahsi geçen "Static Priority" değerinden küçük değere sahip olan düşük öncelikli, büyük değere sahip olan ise yüksek önceliklidir. Bütün bunlara ek olarak, bu iki çizelgeleme algoritmasında "Öncelik Sınıfı(Priority Class)" dayalı bir çizelgeleme kuyruğu oluşturulmaktadır. Her bir "Static Priority" değerine sahip "thread" ler kendi içerisinde sıralanmaktadır. Örneğin, bu değeri 10, 10, 15, 20, 30 olan beş farklı "thread" için toplamda dört adet kuyruk bulundurulmaktadır. Dolayısıyla bir sistemde bu "Static Priority" değerler [1,99] arasında ise 99 farklı kuyruk bulundurulmaktadır. İşte değeri yüksek olan kuyruktakiler ilk olarak CPU'ya atanmaktadır. Dolayısıyla yüksek öncelikli varken düşük öncelikli CPU'ya atanmayacaktır. >>>> "SCHED_FIFO" : Bu çizelgelemeye sahip, aynı "Static Priority" değerine sahip bir grup "thread" olsun. Buradaki çizelgeleme şunlara göre yapılmaktadır: -> Kuyruğun en başındaki "thread" CPU'ya atanır ve "quanta" süresine bakılmaksızın sürekli olarak çalıştırılır. Ancak biterse veya bloke olursa CPU'dan kopartılır. -> Bloke olan iş bu "thread" in blokesi kalktığında, kendi kuyruğunun en sonuna yerleştirilir. Örneğin, elimizde aşağıdaki "Static Priority" değerlerine sahip 6 adet "thread" imiz olsun: T1(10), T2(10), T3(10), T4(10), T5(12), T6(12) Daha sonra bu "thread" ler aşağıdaki gibi kuyruk haline getirilir: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Burada dikkat etmemiz gereken husus "Static Priority" değeri yüksek olanlar daha öncelikli bir kuyrukta toplanırken, düşük olan ise daha düşük öncelikli kuyrukta toplanmıştır. İşte bu notkada CPU'ya ilk olarak "T5" isimli "thread" atanacaktır çünkü hem "Static Priority" değeri yüksek kuyrukta hem de kuyruğun en başında. Artık "quanta" süresine bakılmaksızın bu "thread" sürekli çalışacaktır. Ancak bu "thread" bloke olursa veya tamamlanırsa, "T6" isimli "thread" atanacaktır ve aynı şeyler bunun için de geçerlidir. "T6" da tamamlandıktan sonra veya bloke olursa, sıra "T1" e gelecektir çünkü kuyruğun başında o vardır. İşte bu mekanizma içerisinde bloke olan ve blokesi çözülen olursa, kendi kuyruğunun en sonuna eklenecektir. Dolayısıyla düşük öncelik kuyruğundaki "thread" ler yine bu kuyruğun sonuna atanan "thread" i bekleyecektir. >>>> "SCHED_RR" : Bu çizelgeleme politikası "SCHED_FIFO" ile çok benzerdir. Aralarındaki tek fark, "SCHED_RR" politikasına sahip olan "thread" ler CPU'da bir "quanta" süresince çalıştırılmasıdır. Halbuki "SCHED_FIFO" olanlar "quanta" süresinden bağımsız bir şekilde çalıştırılmaktaydı. Örneğin, elimizde aşağıdaki "Static Priority" değerlerine sahip 6 adet "thread" imiz olsun: T1(10), T2(10), T3(10), T4(10), T5(12), T6(12) Daha sonra bu "thread" ler aşağıdaki gibi kuyruk haline getirilir: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Yalnız buradaki "quanta" süresi, "SCHED_OTHER" politikasındaki "quanta" süresinden bağımsızdır. O politikadaki süreler, onların "nice" değerine göre değişkenlik gösterirken, buradakilerin süreleri sistem genelinde sabittir. Linux sistemlerinde bu politikaya sahip "thread" lerin "quanta" süreleri 100ms kadardır. Pekiyi bu politikayı benimseyen "thread" ler ilgili "quanta" süresini nasıl temin edebilirler? Bunun için POSIX standartları "sched_rr_get_interval" isminde bir fonksiyon bulundurmaktadır. >>>> "sched_rr_get_interval" : Fonksiyonun prototipi aşağıdaki gibidir. #include int sched_rr_get_interval(pid_t pid, struct timespec *interval); Bizler "SCHED_FIFO" ile "SCHED_RR" politikasına sahip "thread" leri ayrı ayrı inceledik fakat bu "thread" ler bir arada bulunabilirler. Örneğin, aşağıdaki gibi "thread" kümemiz olsun: T1(10 / SCHED_FIFO), T2(10 / SCHED_RR), T3(10 / SCHED_FIFO), T4(10 / SCHED_RR), T5(12 / SCHED_FIFO), T6(12 / SCHED_RR) Bunlar aslında aşağıdaki gibi bir kuyruk oluşturacaklardır: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Burada ilk "T5" isimli "thread" CPU'ya atanacaktır. Kendisi "SCHED_FIFO" politikasına sahip olduğu için ya bloke olana dek ya tamamlanana dek ya da daha yüksek öncelikli "SCHED_FIFO" / "SCHED_RR" bir başka "thread" uyanana kadar CPU'dan kopartılmayacaktır. Örneğimizde bu "thread" in bloke olduğunu ve tekrar uyandırıldığını varsayalım. Dolayısıyla kuyruğumuz aşağıdaki biçimde olacaktır: Q1 ---> T6(12), T5(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Şimdi ise "T6" isimli "thread" CPU'ya atanır ve "quanta" süresince çalıştırılır. Bu süre dolduğunda ise kendi kuyruğunun sonuna alınır. Şöyleki: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Ve çalışma bu şekilde devam eder. Tabii "Q1" dekilerin hepsinin bloke olduğunda artık "T1" isimli "thread" atanacaktır. Eğer "T1" çalıştırılıken "Q1" dekilerden birisi uyanırsa, "T1" direkt CPU'dan kopartılacaktır da. Bunun sebebi ise daha yüksek öncelikli bir "thread" in uyanmış olmasıdır. Şimdi burada dikkat etmemiz gereken, "SCHED_FIFO" veya "SCHED_RR" politikasına sahip olan "thread" varsa "SCHED_OTHER" politikasını benimseyenler hiç bir zaman çizelgelenmezler. Linux çekirdeğinde ise gerçekleştirimi kolaylaştırmak için "SCHED_OTHER" politikasını benimseyen "thread" ler için, sanki "Static Priority" değerleri varmış ve sıfırmış gibi, bir kuyruk oluşturulur. Çünkü "SCHED_OTHER" politikasını benimseyenlerde "Static Priority" kavramı yoktur. Böylelikle "SCHED_FIFO" / "SCHED_RR" politikasına sahip olanlar "[1,99]" arasında bir "Static Priority" değerine sahip olurken, "SCHED_OTHER" ise "0" değerinde bir "Static Priority" değerine sahip olmuş olur. Pekiyi çok işlemcili ya da çekirdekli sistemlerde yukarıda açıklanan mekanizma nasıl işletilmektedir? POSIX standartları bu konuda bir şey söylememektedir ancak her bir CPU ya da çekirdek diğerlerinden bağımsız bir birim olarak ele alınmaktadır. Örneğin aşağıdaki gibi bir "thread" kümemiz olsun: T1(SCHED_OTHER), T2(10 / SCHED_RR), T3(SCHED_OTHER), T4(12 / SCHED_FIFO) Burada ilk olarak "T2" isimli "thread" boş bir CPU'ya atanacaktır. Eğer diğer CPU'lar da boş ise önce "T1" atanacaktır. Eğer hala boş CPU kalmışsa, geri kalanları da ona atar. Fakat işletim sistemi isterse "T2" ve "T4" isimli "thread" leri bir CPU'ya, "T1" ve "T3" isimli "thread" leri başka bir CPU'ya atayabilir. Özetle birden fazla CPU ya da çekirdek varsa ilk olarak "thread" ler çekirdeklere atanır. Daha sonra her bir CPU ya da çekirdek diğerlerinden bağımsızmış gibi çizelgeleme yapılır. Windows sistemlerindeki çizelgeleme politikası ise buradaki "SCHED_RR" politikasına benzemektedir. Pekiyi bizler hangi durumlarda hangi çizelgeleme politikasını kullanmalıyız? Buradaki "SCHED_FIFO" ve "SCHED_RR" politikaları nispeten "soft real-time" uygulamalarda tercih edilmelidir. Dolayısıyla bir olay gerçekleştiğinde o olayın hemen ele alınmasını gerekiyorsa, bu politikaları tercih edebiliriz. Örneğin, bir ısı sensöründen okuma yapalım ve değer eşik değerini geçtiğinde de bir müdahale gereksin. İşte böylesi durumlar için "SCHED_FIFO" politikasını benimseyen "thread" ler kullanabiliriz. Tabii "SCHED_FIFO" olanların yoğun bir şekilde kullanılması, diğer "thread" lerin çalışamamasına da neden olabilmektedir. Bu nedenle "SCHED_FIFO" olanların bloke olabilmesi arzu edilmektedir. Tabii bir "thread" in yoğun bir işlemi izlemesi de gerekebilir. Bu durumda bu "thread" için "SCHED_FIFO" politikası da benimsenebilir. Böylelike o "thread" sürekli bir biçimde çalışması sağlanabilir. "SCHED_RR" ise bir grup "thread" in kendi aralarında zaman paylaşımlı bir biçimde çalıştırıldığı durumlarda kullanılabilir. Bütün bunlardan sonra şimdi de proses ve "thread" lerin çizelgeleme politikaları ile "SCHED_FIFO" ve "SCHED_RR" politikasını benimseyenlerin "Static Priority" değerleri nasıl değiştirildiğini inceleyelim: "sched_getparam" ve "sched_setparam", "sched_setscheduler" ve "sched_getscheduler", "sched_yield. Şimdi de bunları inceleyelim: >>> "sched_getparam" ve "sched_setparam" : Bu fonksiyonlar "SCHED_FIFO" ve "SCHED_RR" proseslerin "Static Priority" değerlerini sırasıyla "get" ve "set" etmek için kullanılır. Bu fonksiyonlar birer POSIX fonksiyonudur. Bu fonksiyonları "SCHED_OTHER" politikasına ilişkin "thread" ler üzerinde kullanmaya ÇALIŞMAMALIYIZ. Yine bir prosesin "Static Priority" değeri değiştirildiğinde, o proses ait bütün "thread" lerin bu değeri değiştirilecektir, tabii "SCHED_FIFO" ve "SCHED_RR" politikasını benimsemişse. Fakat Linux sistemlerde ise sadece "main-thread" bundan etkilenmektedir. Tabii Linux sistemlerinde o "thread" in ID değerini elde edip bu fonksiyonlarda kullanılırsa, o ID değerine sahip olanınki de değiştirilecektir. Fonksiyonların prototipleri aşağıdaki gibidir: #include int sched_getparam(pid_t pid, struct sched_param *param); int sched_setparam(pid_t pid, const struct sched_param *param); Fonksiyonun birinci parametresi, işlemin yapılacağı proses ilişkin ID değeridir. Bu parametreye "0" değerinin geçilmesi durumunda bu çağrıyı yapan "thread" ele alınacaktır. İkinci parametre ise "sched_param" isimli bir yapı türündendir ki bu yapı türü şimdilik tek bir elemana sahiptir. Yapının tanımı ise aşağıdaki gibidir: struct sched_param { ... int sched_priority; ... }; Fonksiyonlar başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmekte ve "errno" uygun değere çekilmektedir. Linux'ta proseslerin "Static Priority" değerlerini "get" etmek için herhangi bir ön koşul gerekmemektedir. Fakat diğer işletim sistemlerinde bu durum farklılık gösterebilir. "set" etmek için yine o prosesin uygun önceliklere sahip olması gerekmektedir. * Örnek 1, Bu fonksiyonu "SCHED_OTHER" prosesler için kullanmamalıyız. Linux, "SCHED_OTHER" prosesler için, "sched_getparam" her zaman "0" değerini döndürmektedir. #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # Static Priority: 0 */ struct sched_param sparam; if(sched_getparam(0, &sparam) == -1) exit_sys("sched_getparam"); printf("Static Priority: %d\n", sparam.sched_priority); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "SCHED_OTHER" politikasını benimseyen prosesler için "sched_setparam" fonksiyonu başarılı olamayacaktır. #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # ched_setparam: Invalid argument */ struct sched_param sparam; sparam.sched_priority = 1; if(sched_setparam(0, &sparam) == -1) exit_sys("sched_setparam"); printf("Static Priority: %d\n", sparam.sched_priority); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "sched_setscheduler" ve "sched_getscheduler" : Bir prosesin çizelgeleme politikasını sırasıyla değiştirmek ve almak için kullanılan POSIX fonksiyonlarıdır. Buradaki "sched_getscheduler" yalnızda prosesin çizelgeleme politikasını alırken, "sched_setscheduler" ise hem değiştirmekte hem de "SCHED_FIFO" ve "SCHED_RR" politikasını benimseyenlerin "Static Priority" değerlerini de değiştirmektedir. Fonksiyonların prototipleri şöyledir: #include int sched_getscheduler(pid_t pid); int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param); Fonksiyonların birinci parametresi ilgili prosesin ID değeridir. Bu parametreye sıfır geçilmesi durumunda, fonksiyonu çağıran proses ele alınacaktır. Fonksiyonun üçüncü parametresi ise "SCHED_FIFO" ve "SCHED_RR" olanların "Static Priority" değerini belirtmektedir. Fonksiyonun ikinci parametresi ise çizelgeleme politikasına ait değerdir. Fonksiyonlar başarı durumunda "0", hata durumunda ise "-1" ile geri dönmektedir. Tabii prosesin çizelgeleme politikasını değiştirmek için o prosesin uygun önceliğe sahip olması gerekmektedir. Yine POSIX standartlarına göre bu iki fonksiyon prosesin bütün "thread" leri üzerinde, Linux sistemlerinde ise yalnızca ilgili "thread" üzerinde etkili olmaktadır. * Örnek 1, Aşağıdaki programı çalıştırabilmek için prosesimizin uygun önceliğe sahip olması gerekmektedir. #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # sched_setparam: Invalid argument */ struct sched_param sparam; sparam.sched_priority = 10; if(sched_setscheduler(0,SCHED_FIFO, &sparam) == -1) exit_sys("sched_setscheduler"); printf("Static Priority: %d\n", sparam.sched_priority); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "sched_yield" : Özellikle "SCHED_FIFO" veya "SCHED_RR" politikasını takip eden "thread" lerin kendi istekleri ile CPU'yu bırakması istenmektedir. Burada ilgili "thread" bloke olmamakta, kendi çalışma kuyruğunun sonuna gönderilmektedir. İşte bu işi yapan fonksiyon, bu fonksiyondur. Fonksiyonun prototipi şu şekildedir: #include int sched_yield(void); Fonksiyon başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmektedir. Fakat POSIX standartlarınca bu fonksiyon için herhangi bir hata durumu tanımlanmamıştır. Linux sistemlerinde bu fonksiyon her zaman başarılı olmaktadır. Şimdiye kadar proses temelinde çalışan çizelgeleme fonksiyonlarını gördük. Anımsanacağınız üzere Linux sistemlerinde bu çizelgeleme algoritmaları yalnızda tek bir "thread" üzerinde etkili olurken, POSIX standartlarınca o prosese ait bütün "thread" ler üzerinde etkili olmaktadır. Pekiyi POSIX standartlarına göre belli bir "thread" in çizelgeleme algoritmasını nasıl değiştirebiliriz? İşte burada "attribute" nesneleri devreye girmektedir. Anımsayacağımız üzere bir "thread" oluşturulmadan evvel oluşturacağımız "attribute" nesnelerini kullanarak, o "thread" e ilişkin bir takım özellikleri değiştirebilmekteydik. Bu amaçla oluşturulan "pthread_attr_getXXX" ve "pthread_attr_setXXX" isimli fonksiyonları kullanmaktaydık. İşte bu amaçla kullanacağımız fonksiyonlar ise "pthread_attr_setinheritsched", "pthread_attr_getschedparam" / "pthread_attr_setschedparam", "pthread_attr_getschedpolicy" / "pthread_attr_setschedpolicy" vb. isimli fonksiyonlardır. Bu fonksiyonlar "SCHED_FIFO" ve "SCHED_RR" politikasına sahip olanlar için düşünülmüştür. Öte yandan "SCHED_OTHER" politikasını izleyen spesifik bir "thread" in "nice" değerini değiştirecek POSIX fonksiyonu bulunmamaktadır. >>> "pthread_attr_setinheritsched" : Bir "thread" in çizelgeleme bilgisini değiştirmek için ilk önce bu fonksiyonu çağırarak, ilgili çizelgeleme bilgisini kendisini hayata getiren "thread" den almamasını sağlamalıyız. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched); Fonksiyonun ilk parametresi ilgili "attribute" nesnesinin adresini geçerken, ikinci parametreye şu değerlerden birisini geçmeliyiz: "PTHREAD_INHERIT_SCHED" ve "PTHREAD_EXPLICIT_SCHED". -> "PTHREAD_INHERIT_SCHED" : Çizelgeleme algoritmasını bir üst "thread" den almasını sağlamaktadır. -> "PTHREAD_EXPLICIT_SCHED" : Çizelgeleme algoritmasını bir üst "thread" den almamasını sağlamaktadır. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Pek tabii bu fonksiyonun da "get" eden versiyonu da bulunmaktadır. >>> "pthread_attr_getschedpolicy" ve "pthread_attr_setschedpolicy" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_attr_getschedpolicy(const pthread_attr_t * attr, int * policy); int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); Bu fonksiyonlar belli bir "thread" için çizelgeleme algoritmasının sırasıyla temin edilmesi ve değiştirilmesi için kullanılmaktadır. Başarı durumunda "0", hata durumunda ise hata koduna kendisine dönmektedir. >>> "pthread_attr_getschedparam" ve "pthread_attr_setschedparam" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_attr_getschedparam(const pthread_attr_t * attr, struct sched_param * param); int pthread_attr_setschedparam(pthread_attr_t * attr, const struct sched_param * param); Bu fonksiyonlar ise ilgili politikayı izleyen "thread" lerin "Static Priority" değerlerini sırasıyla temin etmek ve değiştirmek için kullanılmaktadır. Fonksiyon, başarı durumunda "0", hata durumunda ise hata koduna kendisine dönmektedir. Tabii burada ilgili "attribute" nesnesi üzerinde işlem yaparken prosesimizin uygun önceliğe sahip olmasına gerek yoktur. Ancak "thread" oluşturma aşamında prosesimizin uygun önceliğe sahip olması gerekmektedir. * Örnek 1, Aşağıdaki örnekte "SCHED_FIFO" çizelgeleme algoritmasına sahip ve "Static Priorit" değeri "10" olan bir "thread" oluşturulmak istenmiştir. #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys_errno(const char* msg, int eno); int main(void) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys_errno("pthread_attr_init", result); if((result = pthread_attr_setinheritsched(&tattr, PTHREAD_EXPLICIT_SCHED)) != 0) exit_sys_errno("pthread_attr_setinheritsched", result); if((result = pthread_attr_setschedpolicy(&tattr, SCHED_FIFO)) != 0) exit_sys_errno("pthread_attr_setschedpolicy", result); struct sched_param sparam; sparam.sched_priority = 10; if((result = pthread_attr_setschedparam(&tattr, &sparam)) != 0) exit_sys_errno("pthread_attr_setschedparam", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys_errno("pthread_attr_destroy", result); printf("Press CTRL+C to exit!..\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pause(); return NULL; } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Bu program çalışırken ikinci bir terminal programı üzerinden, "ps -T --tty pts/2 -o pid, pri, policy, cmd, tid" komutunu çalıştırırsak ki yukarıdaki örnek programımızı "sudo" ile çalıştırmayı unutmamalıyız, aşağıdaki biçimde bir çıktı elde etmiş olacağız: PID PRI POL CMD TID 9251 19 TS sudo ./sample 9251 9252 19 TS ./sample 9252 9252 50 FF ./sample 9253 Burada "PID" ile "TID" değeri "9252" olan aslında "main-thread". Ancak bizim oluşturduğumuz "thread" in ID değeri ise "9253". İlgili ID değerlerine karşılık gelen "POL" sütunundan da görüleceği üzere yeni oluşturulan "thread" in çizelgeleme algoritması değiştirilmiştir. Diğer yandan bir "thread" i hayata varsayılan "attribute" ile getirdikten sonra da çizelgeleme politikası ve "Static Priority" değerini değiştirebiliriz. Bunun için "pthread_getschedparam" ve "pthread_setschedparam" isimli fonksiyonları kullanacağız. >>> "pthread_getschedparam" ve "pthread_setschedparam" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_getschedparam(pthread_t thread, int * policy, struct sched_param * param); int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Tabii "set" işlemi için de prosesimizin uygun önceliğe sahip olması gerekmektedir. Yine bu fonksiyonlar da "SCHED_FIFO" ve "SCHED_RR" politikasını izleyen "thread" ler için kullanılmalıdır. * Örnek 1, #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys_errno(const char* msg, int eno); int main(void) { int result; struct sched_param sparam; sparam.sched_priority = 10; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_setschedparam(tid, SCHED_FIFO, &sparam)) != 0) exit_sys_errno("pthread_setschedparam", result); printf("Press CTRL+C to exit!..\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pause(); return NULL; } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Bu örnekteki değişimi görebilmek için, program çalışırken başka bir terminale geçip, "ps -T --tty pts/2 -o pid, pri, policy, cmd, tid" komutunu çalıştırmalıyız. Böylelikle aşağıdaki çıktıyı ekranda göreceğiz: PID PRI POL CMD TID 9909 19 TS sudo ./sample 9909 9910 19 TS ./sample 9910 9910 50 FF ./sample 9911 Görüldüğü üzere "9910" ID numaralı olan "main-thread" iken, "9911" olan bizim oluşturduğumuz "thread" dir. Eğer bir "thread" in yalnızca "Static Priority" değerini değiştirmek istiyorsak, "pthread_setschedprio" fonksiyonunu kullanmalıyız. >>> "pthread_setschedprio" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_setschedprio(pthread_t thread, int prio); Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisinde dönmektedir. Yine bu fonksiyon da "SCHED_FIFO" ve "SCHED_RR" politikasını izleyen "thread" ler için kullanılmalıdır. Bu fonksiyonun "get" versiyonu bulunmamaktadır. * Örnek 1, Aşağıdaki örnekte sonradan oluşturulan "thread" in "Static Priority" değeri ilk olarak 10, daha sonra "20" olarak değiştirilmiştir. #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys_errno(const char* msg, int eno); int main(void) { int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); struct sched_param sparam; sparam.sched_priority = 10; if((result = pthread_setschedparam(tid, SCHED_FIFO, &sparam)) != 0) exit_sys_errno("pthread_setschedparam", result); int policy; if((result = pthread_getschedparam(tid, &policy, &sparam)) != 0) exit_sys_errno("pthread_getschedparam", result); printf("Policy : %d\n", sparam.sched_priority); if((result = pthread_setschedprio(tid, 20)) != 0) exit_sys_errno("pthread_setschedprio", result); if((result = pthread_getschedparam(tid, &policy, &sparam)) != 0) exit_sys_errno("pthread_getschedparam", result); printf("Policy : %d\n", sparam.sched_priority); printf("Press CTRL+C to exit!..\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pause(); return NULL; } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Tabii bütün bunları özetleyecek olursak; -> "SCHED_OTHER" politikasını izleyen spesifik bir "thread" in "nice" değerini değiştirecek bir POSIX fonksiyonu yoktur. Çünkü "setpriority" ve "getpriority" fonksiyonları POSIX standartlarınca proses temelli çalışmaktadır. Fakat Linux sistemlerinde ise sadece "main-thread" üzerinde etkili olmaktadırlar. Dolayısıyla Linux sistemlerinde, bu iki fonksiyonu kullanarak, bir "thread" in "nice" değerini DEĞİŞTİREBİLİRİZ. FAKAT BUNU POSIX SİSTEMLERİNDE YAPAMAYIZ. -> "SCHED_FIFO" ve "SCHED_RR" politikasını izleyen "thread" lerin çizelgeleme politikasını ve "Static Priority" değerini spesifik bir "thread" için değiştirebiliriz. Yukarıdaki POSIX fonksiyonlara, o "thread" in ID değerini geçerek bunu gerçekleştirebiliriz. >> "Processor Affinity" : Birden çok CPU veya çekirdeğin bulunduğu sistemlerde, bir "thread" in işletim sisteminin isteğine bırakılmaksızın sadece o CPU'ya atanması, çizelgelenmesi durumudur. Buradaki amaç, paralel programlama esnasında "thread" lerin aynı CPU'ya atanmasını engelleyerek eş zamanlı çalışma süresini arttırmaktır. Çünkü işletim sistemleri toplam faydayı göz önüne alarak atama gerçekleştirmektedir. Örneğin, sistemimiz dört çekirdekli bir sistem olsun. Biz de dört "thread" barındıran bir program yazmış olalım. İşletim sistemi bu dört "thread" i de ayrı ayrı çekirdeklere atamayabilir çünkü bu çekirdeklerin "cache" kısımları birbirinden ayrıdır ve aynı "thread" in tekrar aynı çekirdeğe atanması toplam fayda açısından da tercih edilebilir. Ancak programcı bir takım nedenlerden dolayı da belli "thread" lerin belli çekirdeklere atanması isteyebilir ki paralel programlama sekteye uğramasın. Böylelikle "thread" ler farklı çekirdeklere atanarak, onların eş zamanlı çalışmasına olanak sağlamış oluruz. "Processor Affinity" konusu taşınabilir bir konu olmadığından, POSIX standartlarına yansıtılmamıştır. Dolayısıyla bu işler işletim sistemine özgü sistem fonksiyonlarıyla ya da onları sarmalayan kütüphane fonksiyonları ile gerçekleştirilmektedir. "GNU libc" kütüphanesinde, yani Linux sistemlerinde, bu iş için iki adet fonksiyon bulundurulmaktadır. Bunlar, "sched_setaffinity" ve "sched_getaffinity" isimli fonksiyonlardır. >>> "sched_setaffinity" ve "sched_getaffinity" : Fonksiyonun prototipi aşağıdaki gibidir: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask); int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); Fonksiyonların birinci parametresi ilgili "thread" in ID değerini belirtmektedir. Yani bu fonksiyonlara biz, "gettid()" ile elde edilen "thread" e özgü değerleri geçebiliriz. Eğer "0" geçilirse, bu fonksiyonu çağıran "thread" in ID değeri geçilecektir. Fonksiyonların üçüncü parametreleri "cpu_set_t" türündendir ki bu tür arka planda varsayılan durumda 1024 bit'lik bir veri yapısıdır. Bu veri yapısı muhtemelen aşağıdaki biçimde "typedef" edilmiştir. typedef struct { unsigned long bitarray[16]; }cpu_set_t; Fonksiyonların ikinci parametresi ise üçüncü parametdeki ilgili "bit" dizisinin "byte" uzunluğunu belirtmektedir. Tipik olarak bu parametreye, üçüncü parametredeki yapı nesnesinin "sizeof" değeri geçilir. Fonksiyonun geri dönüş değeri başarı durumunda "0", hata durumunda ise "-1" biçimindedir. Diğer yandan "cpu_set_t" türden bir nesnenin bit'lerini "set" veya "reset" işlemi için de çeşitli makrolar bulundurulmuştur. Bu makrolardan önemli olanları ise şunlardır: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include void CPU_ZERO(cpu_set_t *set); void CPU_SET(int cpu, cpu_set_t *set); void CPU_CLR(int cpu, cpu_set_t *set); int CPU_ISSET(int cpu, cpu_set_t *set); int CPU_COUNT(cpu_set_t *set); Bunlardan, -> "CPU_ZERO" : Tüm bit dizisini sıfırlamaktadır. -> "CPU_SET" : Bit dizisi içerisindeki belli bir bit'i "1" yapmaktadır. -> "CPU_CLR" : Bit dizisi içerisindeki belli bir bit'i "0" yapmaktadır. -> "CPU_ISSET" : Bit dizisi içerisindeki belli bir bit'in durumunu almak için kullanılır. -> "CPU_COUNT" : Bit dizisi içerisindeki kaç bit olduğunu döndürmektedir. İlgili başlık dosyasından diğer makrolara da ulaşabiliriz. Öte yandan, bu iki fonksiyonu kullanabilmek için, ilgili kaynak dosyasının en üstüne "_GNU_SOURCE" sembolik sabitini "#define" etmeliyiz. Tabii şu noktaya da dikkat çekmekte fayda vardır: "sched_setaffinity" fonksiyonuna geçtiğimiz "cpu_set_t" yapısı içerisindeki ilgili dizinin her bir bitinin kullanılabilirliği, o sistemdeki toplam işlemci sayısına göre değişkenlik de göstermektedir. Bütün bunlara ek olarak, makinelerdeki işlemci veya çekirdeklerin numaraları ise sıfırdan başlamaktadır. Son olarak "sched_setaffinity" fonksiyonu ile kendi "thread" lerimizin CPU ayarını değiştirebiliriz ancak başkalarına ait "thread" lerin ayarlarını değiştiremeyiz. Bu fonksiyonun başarılı olabilmesi için, ilgili fonksiyonu çağıran prosesin Etkin Kullanıcı ID değerinin hedef "thread" in sahibi olan prosesin Gerçek veya Etkin Kullanıcı ID değeri ile aynı olması gerekmektedir. Tabii prosesimiz uygun önceliklere sahipse, herhangi bir "thread" in CPU ayarını da değiştirebilir. "sched_setaffinity" ve "sched_getaffinity" fonksiyonlarının ikinci parametresine neden "sizeof" değerini geçtiğimiz konusunda tereddütler oluşabilir çünkü "cpu_set_t" türünü bildirenler, bu türün kaç "byte" olduğunu bilmektedir. Ancak her ne kadar bu tür "1024" bit uzunluğunda olsa bile, işlemci ya da çekirdek sayısının çok fazla olduğu sistemlerde bu "bit" uzunluğu yeterli gelmeyebilir. İşte bu durumda dinamik bellek yöntemleri devreye girmektedir. Yani "CPU_ALLOC" ve "CPU_FREE" fonksiyonlarını kullanmalı ve "CPU_ALLOC_SIZE" fonksiyonu ile de oluşturulan "byte" uzunluğu elde edilmelidir. Dolayısıyla bu fonksiyonlar ile elde edeceğimiz uzunluk bilgisini de "affinity" fonksiyonlarına geçeceğiz. İşte bu nedenden dolayıdır. Şimdi de pekiştirici örneklere bakalım: * Örnek 1, "main-thread" i bir CPU'ya atamak: #define _GNU_SOURCE #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { cpu_set_t cpu_set; // Bütün bit'leri sıfırlandı. CPU_ZERO(&cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "1" numaralı CPU'da "main-thread" çalışacaktır. CPU_SET(1, &cpu_set); if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ile yeni oluşturulan "thread" ile "main-thread" farklı CPU'lara atanmıştır. #define _GNU_SOURCE #include #include #include #include #include #include void* thread_proc(void*); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); cpu_set_t cpu_set; // Bütün bit'leri sıfırlandı. CPU_ZERO(&cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "1" numaralı CPU'da "main-thread" çalışacaktır. CPU_SET(1, &cpu_set); if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void*) { cpu_set_t cpu_set; // Bütün bit'leri sıfırlandı. CPU_ZERO(&cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "2" numaralı CPU'da "main-thread" çalışacaktır. CPU_SET(2, &cpu_set); if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte ise birden fazla "thread" oluşturulup, her biri farklı CPU'lara atanmıştır. #define _GNU_SOURCE #include #include #include #include #include #include #define TOTAL_CPUs 7 void* thread_proc(void*); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { /* # OUTPUT # */ int result; pthread_t tid[TOTAL_CPUs]; for(size_t i = 0; i < TOTAL_CPUs; ++i){ if((result = pthread_create(&tid[i], NULL, thread_proc, (void*)i)) != 0) exit_sys_errno("pthread_create", result); } for(size_t i = 0; i < TOTAL_CPUs; ++i){ if((result = pthread_join(tid[i], NULL)) != 0) exit_sys_errno("pthread_join", result); } return 0; } void* thread_proc(void* i) { size_t index = (size_t)i; cpu_set_t cpu_set; // Bütün bit'leri sıfırlandı. CPU_ZERO(&cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "2" numaralı CPU'da "main-thread" çalışacaktır. CPU_SET(index, &cpu_set); if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Linux sistemlerinde yukarıdaki "affinity" fonksiyonlarına ek olarak iki fonksiyon daha bulunmaktadır. Bunlar "pthread_setaffinity_np" ve "pthread_getaffinity_np" isimli fonksiyonlardır. İsimlerin sonundaki "_np" son eki ise "non-portable" anlamındadır. Bu fonksiyonların yukarıdaki fonksiyonlardan farkı, ilk parametreye "pthread_t" türünden bir ID değeri almasıdır. Böylelikle başka bir "thread" akışı içerisinde olmadan onların CPU ayarlarını değiştirebiliriz. Tabii bu iki fonksiyon da yine Linux sistemi için geliştirilmiştir. Son olarak fonksiyonlar başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. * Örnek 1, Aşağıda "main-thread" ile bizim oluşturduğumuz yeni "thread" farklı CPU'lara atanmıştır. #define _GNU_SOURCE #include #include #include #include #include #include void* thread_proc(void*); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); cpu_set_t cpu_set; // Bütün bit'leri sıfırlandı. CPU_ZERO(&cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "1" numaralı CPU'da "main-thread" çalışacaktır. CPU_SET(1, &cpu_set); if((result = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set)) != 0) exit_sys_errno("pthread_setaffinity_np", result); // Bütün bit'leri sıfırlandı. CPU_ZERO(&cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "2" numaralı CPU'da yeni oluşturulan // "thread" çalışacaktır. CPU_SET(2, &cpu_set); if((result = pthread_setaffinity_np(tid, sizeof(cpu_set), &cpu_set)) != 0) exit_sys_errno("pthread_setaffinity_np", result); puts("Ok\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void*) { puts("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Pekiyi bizler işin başında o sistemde kaç adet CPU olduğunu nasıl anlayabiliriz? Bunun için "shell" programı üzerinden "nproc" veya "lscpu" isimli komutları kullanabiliriz. * Örnek 1, "nproc" komut çıktısı: $ nproc 8 * Örnek 2, "lscpu" komut çıktısı: $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 9 BogoMIPS: 5615.99 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm consta nt_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf _lm abm 3dnowprefetch invpcid_single pti ssbd ibrs ibpb stibp fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt xsaveopt xsavec xgetbv1 xsaves flush_l1d arch_capabilities Virtualization features: Hypervisor vendor: Microsoft Virtualization type: full Caches (sum of all): L1d: 128 KiB (4 instances) L1i: 128 KiB (4 instances) L2: 1 MiB (4 instances) L3: 6 MiB (1 instance) Vulnerabilities: Itlb multihit: KVM: Mitigation: VMX unsupported L1tf: Mitigation; PTE Inversion Mds: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown Meltdown: Mitigation; PTI Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Spectre v2: Mitigation; Full generic retpoline, IBPB conditional, IBRS_FW, STIBP conditional, RSB filling Srbds: Unknown: Dependent on hypervisor status Tsx async abort: Not affected Aslında bu iki komut bu bilgileri "proc" dosya sistemindeki "/proc/cpuinfo" dizininden alınmaktadır. Doğrudan "cat" komutunu kullanıp bu dizindekileri ekrana da yazdırabiliriz. Pekala sistemimizde o anda kaç CPU ya da çekirdek bulunduğu bilgisini, "get_nprocs", "get_nprocs_conf" ve "sched_getcpu()" isimli fonksiyonlarla elde edebiliriz. >>> "get_nprocs" ve "get_nprocs_conf" : Bu fonksiyonlar birer Linux fonksiyonudur ve prototipleri aşağıdaki gibidir: #include int get_nprocs(void); int get_nprocs_conf(void); Buradaki "get_nprocs_conf" fonksiyonu makinadeki toplam işlemci ya da çekirdek sayısını, "get_nprocs" ise işletim sisteminin o makinede dikkate aldığı işlemci ya da çekirdek sayısını döndürmektedir. Bu fonksiyonlar başarısız olamamaktadır. * Örnek 1, #define _GNU_SOURCE #include #include int main(void) { /* # OUTPUT # 8 of 8 is used right now!.. */ printf("%d of ", get_nprocs()); printf("%d is used right now!..\n", get_nprocs_conf()); return 0; } >>> "sched_getcpu" : Bu fonksiyon ise o anda hangi CPU'da çalıştırıldığı bilgisini vermektedir. Yine bu fonksiyon da bir Linux fonksiyonudur. Tabii çalışmakta olan bir "thread" işletim sistemi tarafından farklı CPU'lara atanabilir. Dolayısıyla bu fonksiyon ile elde edeceğimiz değer de farklılık gösterecektir. Fonksiyonun prototipi aşağıdaki gibidir: #include int sched_getcpu(void); * Örnek 1, #define _GNU_SOURCE #include #include int main(void) { /* # OUTPUT # 1 */ printf("%d\n", sched_getcpu()); return 0; } >> "Thread Safety" : "Thread Safety" konusu genel olarak fonksiyonlar için kullanılan bir kavramdır. Bir fonksiyonun "Thread Safety" olması demek, o fonksiyonun birden fazla "thread" tarafından çağrılması durumunda, bir sorun oluşmaması demektir. Pekiyi bir fonksiyonu "Thread Safety" olmaktan çıkartan şeyler nelerdir? Şüphesiz yalnızca yerel değişkenleri ve parametre değişkenlerini kullanan bir fonksiyon "Thread Safety" dir. Dolayısıyla "static" ömürlü kaynak kullanan fonksiyonlar "Thread Safety" olmaktan çıkacaktır. Çünkü böylesi bir fonksiyonu birden fazla "thread" çağırdığında, ilgili kaynak bozulacaktır. Örneğin, Standart C fonksiyonu olan "localtime" fonksiyonu aslında "Thread Safety" değildir çünkü arka planda "static" ömürlü yerel değişken kullanmaktadır. Buradaki kilit nokta ilgili fonksiyonun illa da "static" ömürlü nesne kullanması değildir. "static" ömürlü nesne kullanması gibi ortak başka kaynakları da kullanması o fonksiyonu "Thread Safety" olmaktan çıkartacaktır. Pekiyi bizler bir fonksiyonun "Thread Safety" olmadığı biliyorsak, onu nasıl kullanmalıyız? Bu durumda devreye "mutex" gibi nesneler girmektedir. Tabii burada devreye yine o fonksiyonun dökümantasyonu devreye girmektedir. Dolayısıyla üçüncü parti fonksiyon kullanırken, ilgili fonksiyonun bulunduğu kütüphaneyi inceleyerek durumunu öğrenmeliyiz. Standart C fonksiyonları söz konusu olduğunda da şöyle yapabiliriz: -> Microsoft dünyasında, 2004 yılına kadar, Standart C fonksiyonlarının "Thread Safety" versiyonları ve olmayan versionlarını birlikte bulundurmaktaydı. Ancak 2004 yılı itibariyle artık sadece "Thread Safety" versiyonlarını bulundurmaktadır. Dolayısıyla Microsoft C derleyicisi ile çalışıyorsak, bütün Standart C fonksiyonları "Thread Safety" dir. -> UNIX/Linux dünyasında ise iki farklı version bulundurulmaktadır. Varsayılan isimde olanlar "Thread Safety" değilken, sonu "_r" ile biten fonksiyonlar "Thread Safety" dir. "localtime" fonksiyonunu ele alırsak; "localtime" isimli fonksiyon "Thread Safety" değilken, "localtime_r" isimli fonksiyon "Thread Safety" dir. Fakat şu noktayı da unutmamak gerekir ki sonu "_r" ile bitenler, orjinal versiyonlarından farklı prototiplere sahiptirler. Şimdi de UNIX/Linux dünyasındaki bazı fonksiyonları inceleyelim: >>> "rand_r" fonksiyonu: "rand" fonksiyonunun "Thread Safety" versiyonudur. Anımsanacağı üzere "rand" fonksiyonu, "srand" ile birlikte çalışır ve arka planda "global" isim alanındaki bir değişkeni kullanırlar. Dolayısıyla farklı "thread" ler "rand" fonksiyonunu aynı anda çağırdığında "Data Racing" meydana gelecektir eğer herhangi bir önlem almamışsak. Fonksiyonun prototipi aşağıdaki gibidir: #include int rand_r(unsigned int *seedp); Fonksiyona tohum değerine ilişkin nesnenin adresini almaktadır. Buraya "global" isim alanındaki bir nesnenin adresini de geçebiliriz. Fakat kilit nokta farklı "thread" lerin farklı tohum nesnelerini kullanıyor olmasıdır. * Örnek 1, Aşağıdaki örnekte iki "thread" ile "rand_r" fonksiyonunu kullanmıştır. Ancak bu "thread" ler farklı tohum değerleri kullandığı için bir sorun olmayacaktır. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # Thread-Consumer: 0 Thread-Consumer: 1 Thread-Producer: 0 Thread-Producer: 1 Thread-Consumer: 2 Thread-Producer: 2 Thread-Producer: 3 Thread-Consumer: 3 Thread-Producer: 4 Thread-Consumer: 4 Thread-Consumer: 5 Thread-Consumer: 6 Thread-Consumer: 7 Thread-Producer: 5 Thread-Producer: 6 Thread-Producer: 7 Thread-Consumer: 8 Thread-Producer: 8 Thread-Consumer: 9 Thread-Producer: 9 */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Producer: %d\n", i); } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Consumer: %d\n", i); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise farklı "thread" ler "global" isim alanındaki nesneyi kullanmışlardır fakat yine farklı tohum değerleri ile. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys_errno(const char* msg, int eno); unsigned int seed; int main(int argc, char** argv) { /* # OUTPUT # Thread-Consumer: 0 Thread-Consumer: 1 Thread-Producer: 0 Thread-Producer: 1 Thread-Consumer: 2 Thread-Producer: 2 Thread-Producer: 3 Thread-Consumer: 3 Thread-Producer: 4 Thread-Consumer: 4 Thread-Consumer: 5 Thread-Consumer: 6 Thread-Consumer: 7 Thread-Producer: 5 Thread-Producer: 6 Thread-Producer: 7 Thread-Consumer: 8 Thread-Producer: 8 Thread-Consumer: 9 Thread-Producer: 9 */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_producer(void* param) { seed = time(NULL) + 123; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Producer: %d\n", i); } return NULL; } void* thread_consumer(void* param) { seed = time(NULL) + 456; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Consumer: %d\n", i); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Aşağıda ise "rand" fonksiyonunun ve "srand" fonksiyonunun temsili implementasyonları verilmiştir: * Örnek 1, Bu örnek "The C Programming Language" kitabından alınmıştır. unsigned long int next = 1; void srand(unsigned int seed){ next = seed; } int rand(void){ next = next * 1103515245 + 12345; return (unsigned int)(next / 65536) % 32768; } >>> "localtime_r" fonksiyonu: Bu fonksiyon da "localtime" fonksiyonunun "Thread Safety" halidir. "localtime" fonksiyonu "static" ömürlü nesne kullanması hasebiyle "Thread Safety" değildir. "localtime_r" ise bizden aldığı ilgili nesneyi değiştirerek bize geri döndürmektedir. Dolayısıyla arka planda "static" değişken kullanılmamaktadır. Fonksiyonun prototipi aşağıdaki gibidir: #include struct tm *localtime_r(const time_t *timep, struct tm *result); Burada fonksiyon ikinci parametresi ile aldığı yapı nesnesinin adresini kullandıktan sonra tekrar geri döndürecektir. * Örnek 1, #include #include #include int main(int argc, char** argv) { /* # OUTPUT # 04:37:12 */ time_t t; struct tm tm_val, *p_tm; t = time(NULL); p_tm = localtime_r(&t, &tm_val); printf("%02d:%02d:%02d\n", p_tm->tm_hour, p_tm->tm_min, p_tm->tm_sec); return 0; } Öte yandan UNIX/Linux sistemlerinde yalnızca bazı Standart C fonksiyonlarının "_r" son eki ile biten versiyonları yoktur. Benzer biçimde yine "_r" son eki ile biten POSIX fonksiyonları da vardır. Örneğin, "getpwnam", "getpwuid" gibi fonksiyonlar arka planda yine "static" ömürlü nesnelerin adreslerini geri döndürmektedir. İşte bu fonksiyonların "_r" son eki almış versiyonları ise şu prototiplere sahiptir: #include #include int getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result); int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result); Bu fonksiyonlar, "etc/passwd" dosyasındaki satırların yerleştirileceği tampon alanın başlangıç adresini ve bu alanın uzunluk bilgisini istemektedir. Bu uzunluk bilgisini ise "sysconf(_SC_GETPW_R_SIZE_MAX)" çağrısı ile elde edebiliriz. Yine ek olarak "struct passwd" nesnesinin adres bilgisini de geçeriz. Son parametrelere ise "struct passwd" nesnesinin adresi yerleştirilir, başarı durumunda. Başarısız durumunda bu adrese "NULL" değeri geçilir. Ayrıca fonksiyonların başarı durumundaki değeri "0", hata durumunda ise hata kodunun kendisidir. * Örnek 1, Aşağıdaki örnekte ilgili tampon bölgesinin büyüklüğü olarak yeteri kadar büyük bir alan belirlenmiştir. #include #include #include #include int main(void) { /* # OUTPUT # root, 0 */ char buffer[4096]; struct passwd pwd, *p_pwd; int result; getpwnam_r("root", &pwd, buffer, 4096, &p_pwd); if(result != 0){ fprintf(stderr, "getpwnam_r: %s!..\n", strerror(result)); exit(EXIT_FAILURE); } if(p_pwd == NULL){ fprintf(stderr, "no user found!..\n"); exit(EXIT_FAILURE); } printf("%s, %lld\n", pwd.pw_name, (long long)pwd.pw_uid); return 0; } >> "Thread Specific Global Variables" : Anımsanacağı üzere "global" isim alanındaki nesneler hem "static" ömürlüler hem de "thread" ler tarafından ortak bir şekilde görülür vaziyettedirler. Diğer yandan "automatic" ömürlü nesneler ise "thread" lere özgü biçimdedir. İşte "global" isim alanındaki bu "static" ömürlü nesnelerin "thread" lere özgü olmasına, "thread local storage" denmektedir. Buradaki ilgili nesne yine "global" isim alanında ve "static" ömürlü fakat o "thread" e özgüdür. Dolayısıyla bir işletim sistemi bir "thread" oluştururken sadece ilgili "stack" alanı değil, "thread local storage" alanı da oluşturmaktadır. Buradaki "thread local storage" bölümünde, o "thread" e özgü fakat "static" ömürlü nesneler bulundurulmaktadır. Tabii "dynamic" ömürlü nesneler için de bu ayrım söz konusudur. Yani onlar da "thread" e özgü olabilmektedir. Fakat burada kullandığımız "thread local storage" kavramı aslında Windows dünyasında kullanılan bir kavramdır. UNIX/Linux dünyasında bu kavramın karşılığı "thread specific data" biçimindedir. Pekiyi işletim sisteminin ekstra oluşturduğu bu "thread specific data" alanı nasıldır? Burada aşağıdaki gibi bir veri yapısı kullanılmaktadır: Slot No In-use Flags Pointer 0 1 ---> 1 0 NULL 2 1 ---> 3 1 ---> 4 1 ---> ... ... ... Görüleceği üzere ilgili veri yapısı "slot" lardan oluşmaktadır ve her birisinin bir numarası vardır. Bu "slot" ların türleri ise "pthread_key_t" türündendir. Ayrıca o "slot" un boş ya da dolu olduğunu da "flag" ile belirtilmektedir. Eğer o "slot" dolu ise ilgili "Pointer" kısmındaki gösterici ilgili nesneyi, eğer o "slot" boş ise "NULL" değerini göstermektedir. Tabii buradaki "flag" kısmına da gerek yoktur, diyebiliriz. Fakat bu "flag" sütununa, "Pointer" kısmına "NULL" adres yerleştirilebileceği için gereksinim duyulmaktadır. Pekiyi bizler bu tabloyu nasıl kullanacağız? Burada dört adet POSIX fonksiyonu elimizin altındadır: "pthread_key_create", "pthread_getspecific", "pthread_setspecific" ve "pthread_key_delete" simli fonksiyonlarıdır. Bu fonksiyonları ise şu şekilde kullanabiliriz: -> "pthread_key_create" ile aslında biz bir "slot" oluştururuz. Bu "slot", daha önce hayata getirilen ve hayata getirilecek olan her bir "thread" de var olacaktır. Tabii her bir "thread" için bu "slot" a farklı bir adres tayin edebilmekteyiz. Fonksiyonun prototipi şu şekildedir: #include int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); Fonksiyonun birinci parametresi, "slot" numarasının yerleştirileceği "pthread_key_t" türünden nesnenin adresini almaktadır. Bu "pthread_key_t" türünden nesne tipik olarak programın "global" isim alanında tanımlanmaktadır. Fonksiyonun ikinci parametresi ise o "slot" yok edilirken çağrılacak fonksiyonu belirtmektedir. Bu parametre "NULL" değerini alabilir. Fonksiyonun geri dönüş değeri ise hata durumunda hata kodunun kendisine, başarı durumunda ise "0" değerine geri dönmektedir. Artık her bir "thread" de geçerli bir "slot" tahsis etmiş olduk. -> Programcı "thread" e özgü "static" verileri bir yapı ile sarmalar ve bu yapı türünden dinamik ömürlü nesne oluşturur. Daha sonra bu nesnenin adresini de "pthread_setspecific" fonksiyonu ile o "thread" in ilgili "slot" una yerleştirir. "pthread_setspecific" fonksiyonunu hangi "thread" çağırmışsa, adres bilgisi o "thread" in ilgili slotuna yerleştirilecektir. İş bu fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_setspecific(pthread_key_t key, const void *value); Fonksiyon birinci parametresine, "pthread_key_create" fonksiyonunun birinci parametresine geçtiğimiz, "slot" bilgisisini içeren "pthread_key_t" türünden değişkeni geçeriz. İkinci parametresine de o "slot" a yazılacak olan adres bilgisini geçeriz. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. -> Pekala o "thread" in belli bir "slot" una yerleştirilen adresin temin edilmesi ise "pthread_getspecific" fonksiyonu ile mümkündür. Fonksiyonun prototipi aşağıdaki gibidir: #include void *pthread_getspecific(pthread_key_t key); Fonksiyon birinci parametresine, "pthread_key_create" fonksiyonunun birinci parametresine geçtiğimiz, "slot" bilgisisini içeren "pthread_key_t" türünden değişkenini alır. Geri dönüş değeri olarak da o "slot" da bulunan adres bilgisini geri döndürür. Ancak öyle bir "slot" yoksa ya da o "slot" da herhangi bir adres belirtilmemişse "NULL" değerine geri dönmektedir. Başarısızlık için herhangi bir hata kodu belirtilmemiştir. -> Son olarak oluşturulan "slot" un tekrar geri verilmesi uygundur. Bunun için "pthread_key_delete" isimli fonksiyonu kullanacağız. Zaten bir "thread" de sona ermek üzereyse, ona ait tüm "slot" lar da boşaltılacaktır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_key_delete(pthread_key_t key); Fonksiyon parametre olarak, "pthread_key_create" fonksiyonunun birinci parametresine geçtiğimiz, "slot" bilgisisini içeren "pthread_key_t" türünden değişkenini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. * Örnek 1, Aşağıdaki örnekte "thread" ler zaten sona ereceğinden "pthread_key_delete" fonksiyon çağrısına lüzum görülmemiştir. #include #include #include #include #include pthread_key_t g_key; typedef struct tagTHREAD_SPECIFIC_DATA{ int count; /* ... */ } THREAD_SPECIFIC_DATA; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 31 69 */ int result; if((result = pthread_key_create(&g_key, NULL)) != 0) exit_sys_errno("pthread_key_create", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 31; pthread_setspecific(g_key, tsd); foo(); return NULL; } void* thread_proc2(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 69; pthread_setspecific(g_key, tsd); foo(); return NULL; } void foo(void) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)pthread_getspecific(g_key)) == NULL){ fprintf(stderr, "cannot get the thread specific data!..\n"); exit(EXIT_FAILURE); } printf("%d\n", tsd->count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } -> Pekiyi bu örnekteki dinamik ömürlü bellek alanlar ne zaman geri verilmelidir? Burada iki farklı yöntem karşımıza çıkmaktadır. İlki, o bellek alanını tahsis eden ilgili "thread" tarafından, "manuel" olarak sonlandırılmasıdır. Diğeri ise "pthread_key_create" fonksiyonunun ikinci parametresine bu işi üstlenecek fonksiyonun adresini geçmektir. İş bu ikinci parametreye geçilen o fonksiyon, o "key" değeri ile oluşturulan her bir "slot" için, tabii o "slot" a da bir adres değeri geçilmişse, sistem tarafından bir kez çağrılacaktır. * Örnek 1, Aşağıdaki örnekte her bir "thread" in tahsis ettiği dinamik ömürlü alan "key_deleter" isimli fonksiyon tarafından serbest bırakılmaktadır. #include #include #include #include #include pthread_key_t g_key; typedef struct tagTHREAD_SPECIFIC_DATA{ int count; /* ... */ } THREAD_SPECIFIC_DATA; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void key_deleter(void* ptr); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # foo called!.. 31 key_deleter called!.. foo called!.. 69 key_deleter called!.. */ int result; if((result = pthread_key_create(&g_key, key_deleter)) != 0) exit_sys_errno("pthread_key_create", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_key_delete(g_key)) != 0) exit_sys_errno("pthread_key_delete", result); return 0; } void* thread_proc1(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 31; pthread_setspecific(g_key, tsd); foo(); return NULL; } void* thread_proc2(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 69; pthread_setspecific(g_key, tsd); foo(); return NULL; } void foo(void) { puts("foo called!.."); THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)pthread_getspecific(g_key)) == NULL){ fprintf(stderr, "cannot get the thread specific data!..\n"); exit(EXIT_FAILURE); } printf("%d\n", tsd->count); } void key_deleter(void* ptr) { puts("key_deleter called!.."); free(ptr); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi buraya kadarki örneklerde "thread specific data" kullanan fonksiyonları bizler yazarak onları "thread safe" hale getirdik. Pekiyi başkaları tarafından yazılan fonksiyonlar "thread safe" değilse onları nasıl "thread safe" hale getirebiliriz? Bunun bir yolu, eğer bahsi geçen fonksiyon Standart C fonksiyon ise veya kullanacağımız kütüphane derleyici ile aşağı seviyeli bir bağlantıya sahipse, "thread specific data" için oluşturmamız gereken "slot" ları programın "start-up" kodları içerisinde oluşturmalıyız. Bu "start-up" kodları, aslında programın "main" fonksiyonunun çağrılmasından evvel çağrılan kodlardır. Örneğin, Microsoft derleyicileri Standart C fonksiyonlarını bu şekilde "thread safe" hale getirmiştir. >>> Burada da şu noktayı açıklamak gereklidir: Bir C programında ilk çağrılan fonksiyon "main" fonksiyonu değildir. Aslında ilk çağrılan kodlar, derleyicinin "start-up" kodları dediği kodlardır ki bu kodlar "main" fonksiyonunu çağırmaktadır. Yani programın akışı şu şekilde diyebiliriz: ... ... ... call to main call to exit Buradan da görüleceği üzere "main" fonksiyonu aslında "start-up" kodları tarafından çağrılmakta, programın akışı "main" fonksiyonundan geri geldikten sonra "exit" fonksiyonu çağrılmaktadır. İşte derleyici ile aşağı seviyeli bir bağlantıya sahip bir kütüphanede "thread safe data" kullanılacaksa, iş bu "start-up" kodlarının bulunduğu alanda ilgili "slot" lar oluşturulabilir (C++ gibi dillerde de "global" değişkenler kullanılarak bir takım kodlar "main" fonksiyonundan evvel çağrılmasını sağlayabiliriz fakat bu yaklaşımı C dilinde uygulayamayız çünkü C dilinde böylesi bir kullanım için sabit ifadesi kullanmalıyız.). Pekiyi bizler böylesi fonksiyonları kendi kütüphanemiz için nasıl yazabiliriz? Burada iki farklı yöntem karşımıza çıkmaktadır: Kütüphanemize "init" ismine benzer isimde bir fonksiyon eklemek ve işin başında bu fonksiyonun programcı tarafından çağrılması gerektiğini belirtmek, "pthread_once" fonksiyonunu kullanmak. Şimdi de bu yöntemleri inceleyelim: >>> "pthread_once": Bazı durumlarda bir "thread" akışının aynı yerden tekrar tekrar geçtiği halde sadece bir defa bir şeyin yapması istenebilmektedir. Bunun için "pthread_once" isimli bir POSIX fonksiyonu bulundurulmaktadır. Fonksiyonun prototipi şu şekildedir: #include int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); Fonksiyonun birinci parametresi "pthread_once_t" türünden bir nesnenin adresidir. Fakat bu nesneye aşağıdaki gibi ilk değer verilmiş olması gerekmektedir: pthread_once_t once_control = PTHREAD_ONCE_INIT; Fonksiyonun ikinci parametresi ise bir defaya mahsus çağrılacak olan fonksiyonun adresidir. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. Eğer fonksiyonun birinci parametresine geçtiğimiz nesne tekil bir nesne ise, fonksiyonun ikinci parametresindeki fonksiyon iki defa çağrılacaktır. Eğer farklı nesneler geçersek, o nesnelerin adedince ilgili fonksiyon çağrılacaktır. * Örnek 1, #include #include #include #include #include pthread_once_t g_ones = PTHREAD_ONCE_INIT; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void init(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void foo(void) { int result; if((result = pthread_once(&g_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); } void init(void) { printf("init proc!..\n"); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include pthread_once_t g_ones[5]; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void init(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. init proc!.. init proc!.. init proc!.. init proc!.. */ for(int i = 0; i < 5; ++i) g_ones[i] = PTHREAD_ONCE_INIT; int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void foo(void) { int result; for(int i = 0; i < 5; ++i) if((result = pthread_once(&g_ones[i], init)) != 0 ) exit_sys_errno("pthread_once", result); } void init(void) { printf("init proc!..\n"); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(pthread_once_t* t_ones); void init(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. init proc!.. */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { pthread_once_t t_ones = PTHREAD_ONCE_INIT; for(int i = 0; i < 10; ++i) foo(&t_ones); return NULL; } void* thread_proc2(void* param) { pthread_once_t t_ones = PTHREAD_ONCE_INIT; for(int i = 0; i < 10; ++i) foo(&t_ones); return NULL; } void foo(pthread_once_t* t_ones) { int result; if((result = pthread_once(t_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); } void init(void) { printf("init proc!..\n"); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Aslında bu fonksiyonun yaptığı şeyi "mutex" kullanarak da gerçekleştirebiliriz. Şöyleki: * Örnek 1, #include #include #include #include #include #define MY_PTHREAD_ONCE_INIT 0 pthread_once_t g_ones = MY_PTHREAD_ONCE_INIT; void* thread_proc1(void* param); void* thread_proc2(void* param); void init(void); int my_pthread_once(pthread_once_t* once_control, void(*init_routing)(void)); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { int result; if((result = my_pthread_once(&g_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); return NULL; } void* thread_proc2(void* param) { int result; if((result = my_pthread_once(&g_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); return NULL; } void init(void) { printf("init proc!..\n"); } int my_pthread_once(pthread_once_t* once_control, void(*init_routing)(void)) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int result; if((result = pthread_mutex_lock(&mutex)) != 0) return result; if(*once_control == 0){ init_routing(); *once_control = 1; } if((result = pthread_mutex_unlock(&mutex)) != 0) return result; return 0; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Artık bu fonksiyonu kullanarak, yalnızca ilk "thread" in bir kez "thread specific data" için "slot" oluşturmasını sağlayabiliriz. >>> Buradaki "init" ismi temsili bir isimdir. Buradaki amaç, böylesi bir fonksiyonun işin başında programcı tarafından çağrılmasını sağlatmaktır. İş bu fonksiyonun bünyesinde "thread specific data" için "slot" oluşturabiliriz. Bu yöntem diğer yönteme nazaran daha etkilidir fakat programcının burada bu fonksiyonu çağırmayı unutmaması gerekmektedir. Şimdi de Standart C fonksiyonu olan "srand" ve "rand" fonksiyonlarının "thread safe" biçimlerini fonksiyonların prototiplerini değiştirmeden kendimiz yazalım: * Örnek 1, Aşağıdaki örnekte "srand" ve "rand" fonksiyonlarının parametrik yapıları aynı kalacak biçimde "thread safe" hale getirilmiştir. Bir "thread" ilk kez "srand" ya da "rand" fonksiyonunu çağırdığında bir "slot" oluşturacaktır. Sonraki "thread" ler ise bu "slot" u kullanacaktır. Buradaki kilit nokta her iki fonksiyon için sadece bir adet "slot" oluşturulmasıdır. Benzer yaklaşımı büyük kütüphanelerde de uygulayabiliriz. /* my_rand.h */ #ifndef MY_RAND_H #define MY_RAND_H /* VARIABLES */ #define SEED_INIT_VALUE 12345 /* FUNCTIONS */ void my_srand(unsigned int seed); int my_rand(void); #endif /* my_rand.c */ #include #include #include #include #include "my_rand.h" typedef struct tagTSD_RAND{ unsigned int seed; //... }TSD_RAND; pthread_once_t g_rand_once; pthread_key_t g_rand_key; static void exit_sys_errno(const char* msg, int eno); static void destructor(void* ptr); static void rand_init_once(void); static TSD_RAND* rand_init(void); void my_srand(unsigned int seed) { TSD_RAND* tsd_rand = rand_init(); tsd_rand->seed = seed; } int my_rand(void) { TSD_RAND* tsd_rand = rand_init(); tsd_rand->seed = tsd_rand->seed * 1103515245 + 12345; return (unsigned int)(tsd_rand->seed / 65536) % 32768; } static void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } static void destructor(void* ptr) { free(ptr); } static void rand_init_once(void) { int result; if((result = pthread_key_create(&g_rand_key, destructor)) != 0) exit_sys_errno("pthread_key_create", result); } static TSD_RAND* rand_init(void) { int result; if((result = pthread_once(&g_rand_once, rand_init_once)) != 0) exit_sys_errno("pthread_once", result); TSD_RAND* tsd_rand; if((tsd_rand = pthread_getspecific(g_rand_key)) == NULL){ if((tsd_rand = (TSD_RAND*)malloc(sizeof(TSD_RAND))) == NULL){ fprintf(stderr, "fatal error: cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if((result = pthread_setspecific(g_rand_key, tsd_rand)) != 0) exit_sys_errno("fatal error: pthread_setspecific", result); tsd_rand->seed = SEED_INIT_VALUE; } return tsd_rand; } /* main.c */ #include #include #include #include #include #include "my_rand.h" #define MY_PTHREAD_ONCE_INIT 0 #define EPOCH 5 void* thread_proc1(void* param); void* thread_proc2(void* param); void* thread_proc3(void* param); void* thread_proc4(void* param); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0. Thread-3 : 83 0. Thread-4 : 86 0. Thread-2 : 68 0. Thread-1 : 68 1. Thread-3 : 77 1. Thread-4 : 15 1. Thread-2 : 88 1. Thread-1 : 88 2. Thread-3 : 93 2. Thread-4 : 35 2. Thread-2 : 17 2. Thread-1 : 17 3. Thread-3 : 86 3. Thread-2 : 98 3. Thread-4 : 92 3. Thread-1 : 98 4. Thread-3 : 49 4. Thread-2 : 27 4. Thread-1 : 27 4. Thread-4 : 21 */ int result; pthread_t tid1, tid2, tid3, tid4; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid3, NULL, thread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid4, NULL, thread_proc4, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid4, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = my_rand() % 100; printf("%d. Thread-1 : %d\n", i, result); } return NULL; } void* thread_proc2(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = my_rand() % 100; printf("%d. Thread-2 : %d\n", i, result); } return NULL; } void* thread_proc3(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = rand() % 100; printf("%d. Thread-3 : %d\n", i, result); } return NULL; } void* thread_proc4(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = rand() % 100; printf("%d. Thread-4 : %d\n", i, result); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi de bir diğer Standart C fonksiyonu olan "strerror" fonksiyonunu inceleyelim. Anımsanacağı üzere bu fonksiyonu bizler "exit_sys_errno" fonksiyonu ile sarmalayarak kullandık. Aslında bu fonksiyon "thread safe" DEĞİLDİR. Dolayısıyla "strerror" yerine "strerror_r" fonksiyonunu kullanmalıyız. Fakat "strerror_r" fonksiyonunun kullanması da biraz zahmetlidir. Bu nedenle GNU/glibc kütüphanesinin belli bir versiyonundan sonra "strerror" fonksiyonu "thread safe" olarak tanımlanmıştır. Ama yine de taşınabilirlik açısından "strerror_r" fonksiyonunu kullanmamız uygun olacaktır. Öte yandan bazı C derleyicileri "extension" olarak, "thread local storage" denilen bir özellik de sunmaktadır. Örneğin, "gcc" ve "clang" derleyicileri bu özelliği sunmaktadır. Ancak bu özellik UNIX/Linux genelindeki bütün derleyici ve platformlarda bulunmamaktadır. Pekiyi nedir bu özellik? Bu özelliğe göre "static" ömürlü bir değişken "__thread" niteleyicisi kullanılarak bildirilmesi halinde artık ilgili değişken "thread" e özgü olmaktadır. * Örnek 1, Aşağıdaki "g_x" değişkeni "thread" e özgüdür. Burada programcı bu değişkeni normal bir değişken gibi kullanabilir. int __thread g_x; * Örnek 2.0, Aşağıdaki örnekte "static" ömürlü ilgili değişken herhangi bir "slot" kullanılmadan iki "thread" tarafından da kullanılmıştır. Dolayısıyla bir "race conditions" meydana gelmiştir. #include #include #include #include #include int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 69 69 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { g_count = 31; sleep(1); foo(); return NULL; } void* thread_proc2(void* param) { g_count = 69; sleep(1); foo(); return NULL; } void foo(void) { printf("%d\n", g_count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2.1, Aşağıdaki örnekte ise "static" ömürlü nesne için bir "slot" oluşturulmuş ve iki "thread" tarafından da kullanılması sağlanmıştır. Dolayısıyla bir "race condition" meydana GELMEMİŞTİR. #include #include #include #include #include pthread_key_t g_key; int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void key_deleter(void* ptr); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # foo called!.. 31 key_deleter called!.. foo called!.. 69 key_deleter called!.. */ int result; if((result = pthread_key_create(&g_key, key_deleter)) != 0) exit_sys_errno("pthread_key_create", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_key_delete(g_key)) != 0) exit_sys_errno("pthread_key_delete", result); return 0; } void* thread_proc1(void* param) { int* tsd; if((tsd = (int*)malloc(sizeof(int))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } sleep(1); *tsd = 31; pthread_setspecific(g_key, tsd); foo(); return NULL; } void* thread_proc2(void* param) { int* tsd; if((tsd = (int*)malloc(sizeof(int))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } sleep(1); *tsd = 69; pthread_setspecific(g_key, tsd); foo(); return NULL; } void foo(void) { puts("foo called!.."); int* tsd; if((tsd = (int*)pthread_getspecific(g_key)) == NULL){ fprintf(stderr, "cannot get the thread specific data!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *tsd); } void key_deleter(void* ptr) { puts("key_deleter called!.."); free(ptr); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2.2.0, Aşağıdaki örnekte ise "static" ömürlü nesne için "__thread" niteleyicisi kullanılmıştır. Dolayısıyla artık bu değişken "thread" e özgüdür. #include #include #include #include #include int __thread g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 31 69 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { g_count = 31; sleep(1); foo(); return NULL; } void* thread_proc2(void* param) { g_count = 69; sleep(1); foo(); return NULL; } void foo(void) { printf("%d\n", g_count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2.2.1, Aşağıdaki örnekte ise "static" ömürlü nesne için "__thread" niteleyicisi kullanılmıştır. Dolayısıyla artık bu değişken "thread" e özgüdür. #include #include #include #include #include int __thread g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(const char* str); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # Thread - 1 : 11 Thread - 2 : 110 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { g_count = 1; foo("Thread - 1"); return NULL; } void* thread_proc2(void* param) { g_count = 100; foo("Thread - 2"); return NULL; } void foo(const char* str) { for(int i = 0; i < 5; ++i){ g_count += i; } printf("%s : %d\n", str, g_count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Pekiyi giriş çıkış akımları da birer "thread safe" öğeler midir? Anımsanacağı üzere "fopen" fonksiyonu "FILE" türünden bir yapı nesnesinin adresini döndürmekte, bu yapının içerisinde de kullanılacak tamponun adresi bulunmaktaydır. Pekala bizler "global" isim alanında "FILE" türden bir gösterici tanımlasak ve bunu da iki farklı "thread" ile kullansak herhangi bir sorun meydana gelmeyecektir. Çünkü POSIX standartlarınca "stream" işlemleri "thread safe" dir. Dolayısıyla özel olarak bir şey yapmamıza gerek yoktur. Benzer biçimde de Windows sistemlerinde de "stream" işlemleri "thread safe" dir. Ancak Standart C standartlarında böyle bir garanti VERİLMEMEKTEDİR. * Örnek 1, #include #include #include #include #include FILE* g_f; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # */ if((g_f = fopen("test.txt", "w")) == NULL){ fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); fclose(g_f); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 10000; ++i) fprintf(g_f, "Thread - 1: %d\n", i); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 10000; ++i) fprintf(g_f, "Thread - 2: %d\n", i); return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >> "Thread Pool" Kavramı: "thread" ler konusunda karşımıza çıkabilecek bir kavramdır. Belli bir "thread" i işin başında oluşturup, onları "suspend" edip, tam ihtiyaç anında kullanmaktır. Böylelikle tam ihtiyaç anında "thread" oluşturmak için zaman kaybetmemiş olacağız. Daha sonra da ihtiyaç anına göre "thread" lerin belli bir kısmını yok ediyorlar. C ve C++ dillerinde bu kavram için standart bir kütüphane yoktur. Benzer şekilde "glibc" kütüphanesinde de bu kavram için fonksiyonlar bulunmamaktadır. Fakat Windows sistemleri bu kavramı desteklemektedir. Dolayısıyla taşınabilirlik açısından üçüncü parti kütüphaneleri kullanmalıyız. Ancak Qt kütüphanesi veya Java ve C# gibi nesne yönelimli bazı diller bu kavramı hazır olarak bünyelerinde bulunmaktadır. Buradaki asıl amaç taşınabilirlik sağlamaktır. Yoksa ilgili dil ve kütüphanelerdeki fonksiyonlar, günün sonunda POSIX "pthread" fonksiyonlarını veya "Windows API" fonksiyonlarını çağırmaktadır. "server" uygulamalarında "server" program çok sayıda "client" dan gelen mesajları işlemesi gerekebilir. Bu tip uygulamalarda tipik döngü ise aşağıdaki gibidir: for(;;){ msg = get_msg(); process_msg(msg); } Burada "get_msg" fonksiyonu "client" dan mesajı alan, "process_msg" ise bu mesajı işleyen fonksiyon olmak üzere; "server" programın amacı "client" ları bekletmeden, onların taleplerini hızlıca yapmaktır. Öte yandan bu tip uygulamalar "TCP/IP socket" programlamada karşımıza çıkmaktadır ki bu proglamlama izleyen paragraflarda açıklanacaktır. Pekiyi bizler buradaki döngüyü nasıl hızlandırabiliriz? Akla ilk gelen yöntem "process_msg" fonksiyonunun başka bir "thread" e yaptırılması ve böylelikle "server" ın hemen bir sonraki mesajı ele almaya çalışmasıdır. Fakat buradaki sıkıntı ise şudur: Her mesaj için bir "thread" oluşturmak, o mesaj ile işimiz bittikten sonra da o "thread" in yok edilmesi gerekmektedir. Fakat bu oluşturma ve yok etme işlemleri de zaman maliyeti oluşturmaktadır. Burada iki yöntem uygulayabiliriz: -> Yukarıdaki döngüyü çalıştıran "thread" in gelen mesajları bir kuyruk sistemine yazması ve "üretici-tüketici" problemi biçiminde "n" tane "client thread" in bu kuyruktaki mesajları işlemesi yöntemidir. Burada işin başında "n" tane "thread" oluşturulacağı için, her mesaj için yeniden "thread" oluşturmaya lüzum kalmayacaktır. Tabii işin sonunda yine ilgili "thread" ler yok edilmektedir. -> "thread pool" yöntemini burada kullanabiliriz. Burada "n" tane "thread" iş başlamadan oluşturulur fakat "suspend" edilir. Her mesaj geldiğinde "thread" ler sırayla uyandırılır ve mesajı işlemesi sağlanır. Fakat mesajın işlenmesi bittiğinde ilgili "thread" ler yok edilmemekte, "suspend" edilerek uyutulmaktadır. Görüleceği üzere bu iki yöntem de birbirine benzer yöntemlerdir. Fakat birinci yöntemdeki "üretici-tüketici" problemi için kuyruk sisteminin oluşturulması ve senkronize edilmesi gerekmektedir. Tabii gelen mesajların bu kuyruğa yazılması ve bu kuyruktan alınması da yine zaman kaybına yol açmaktadır. İkinci yöntemde ise daha doğrudan bir işlem söz konusudur. Çünkü her gelen mesaj için bir "thread" direkt olarak uyandırılmakta, araya herhangi bir kuyruk sistemi dahil edilmemekte, işin sonunda da tekrardan uyutulmaktadır. Pekiyi işin başında bizler kaç adet "thread" oluşturmalıyız, hangi yöntemi kullanmamızdan bağımsız olarak? Açıkçası sistemimiz tek çekirdekli veya işlemcili ise bu "n" sayısının büyük tutulması önemli bir hız kazancı sağlamayacaktır. Tabii bu sistemlerde bizim prosesimiz ile rekabet eden diğer üçüncü parti prosesler varsa, bizimkinin fazla "thread" oluşturması bizim prosesimizin daha çok çalıştırılmasına neden olabilecektir. Eğer ilgili makinede rekabet edecek başka programlar yoksa, bu "n" sayısının büyük tutulması sanıldığı kadar fazla olmayabilecektir. Öte yandan mesajların işlenmesi için başka "thread" ler oluşturulmazsa, bu mesajın işlenmesi sırasında bir bloke oluşması durumunda programın geneli etkilenecektir. Diğer durumda ise sadece o mesajı işleyen tekil "thread" bloke olacağından programın geneli etkilenmeyecektir. Dolayısıyla bu "n" sayısının belirlenmesinde şu kritere dikkat etmeliyiz: -> Mesajın işlenmesi önemli blokelere neden olmuyorsa, bu "n" sayısının işlemci ya da çekirdek kadar olması uygundur. Hatta "Processor Affinity" ile mesajları farklı çekirdeklere bağlayabilir. Eğer mesajın işlenmesi sırasında blokeler oluşacaksa, bu "n" değeri de daha da arttırılabilir. (Tabii bu tür durumlarda Linux sistemlerinde, "Processor Affinity" uygulanmasa bile, CFS algoritmasından dolayı toplam fayda olacak biçimde dengelenme sağlanmaya çalışılmaktadır.) Bütün bunları özetleyecek olursak; -> Tek çekirdekli ya da tek işlemcili sistemlerde mesajın işlenmesi işi "n" tane "thread" e yaptırılması, eğer mesajın işlenmesi sırasında önemli blokeler oluşturacaksa, fayda sağlayacaktır. Ancak ilgili mesajın işlenmesi CPU yoğun ise veya sistemde programımız için rekabet edecek başka prosesler yoksa, sanıldığı kadar fayda sağlamayacaktır. -> Çok çekirdekli ya da çok işlemcili sistemlerde mesajın işlenmesi CPU yoğun ise bu "n" değeri çekirdek ya da işlemci sayısı kadar olabilir. Eğer mesajın işlenmesi sırasında önemli blokeler oluşacaksa, bu "n" değeri büyütülebilir. Öte yandan "thread pool" yönteminde şunu da belirtmekte fayda vardır: Mesajların işlenmesi sırasında havuzda "suspend" edilmiş hiç "thread" kalmadıysa ve hala işlenmeyi bekleyen mesaj varsa, bu durumda yeni "thread" ler oluşturularak havuza eklenecektir. İş yoğunluğu azaldığında da yine havuzdakilerin bir bölümü yok edilecektir. >> "threads.h" kütüphanesi: 2011 yılı itibariyle Standart C kütüphanesine isteğe bağlı bir "thread" kütüphanesi eklenmiştir. Bu kütüphane hala Microsoft tarafından desteklenmemekte, fakat güncel gcc ve clang derleyicileri tarafından desteklenmektedir. Bu kütüphanenin kullanımı kabaca şu şekildedir: -> Bir "thread" oluşturmak için "thrd_create" isimli Standart C Fonksiyonunu çağırmalıyız. Fonksiyonun prototipi aşağıdaki gibidir: int thrd_create( thrd_t *thr, thrd_start_t func, void *arg ); Fonksiyonun birinci parametresi oluşturulacak "thread" i temsil edecek ID değerinin yazılacağı, "thrd_t" türünden, nesnenin adresidir. İkinci parametre ise "thread" akışının başlatılacağı fonksiyonun adresini, üçüncü parametre ise iş bu fonksiyona geçilecek argümanı belirtmektedir. Buradaki "thrd_start_t" türü aşağıdaki biçimde "typedef" edilmiştir: typedef int (*thrd_start_t)(void*); -> Bir "thread" oluşturduktan sonra "thrd_join" ile "join" edilebilir veya "thrd_detach" ile "detached" duruma sokulabilir. Fonksiyonların prototipleri ise aşağıdaki gibidir: int thrd_join( thrd_t thr, int *res ); int thrd_detach( thrd_t thr ); -> Bir "thread" i belli bir süre blokede bekletmek için "thrd_sleep" fonksiyonu kullanılır ki prototipi aşağıdaki gibidir: int thrd_sleep( const struct timespec* duration, struct timespec* remaining ); "threads." kütüphanesi, "time.h" kütüphanesini de bünyesinde barındırması garanti altındadır. Dolayısıyla ekstradan "time.h" kütüphanesini eklememize gerek yoktur. -> Bu kütüphanedeki fonksiyonların geri dönüş değerleri genellikle "int" türden olup başarı durumunda "thrd_success", hata durumunda "thrd_error" ya da "thrd_nomem" değerlerine geri dönerler. Başarı kontrolü ise aşağıdaki biçimde yapılabilir: if(thrd_xxx(...) != thrd_success){ //... } Aşağıda kullanıma ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include int thread_proc(void* param); int main() { /* # OUTPUT # main-thread : 0 other-thread: 0 main-thread : 1 other-thread: 1 main-thread : 2 other-thread: 2 main-thread : 3 other-thread: 3 main-thread : 4 other-thread: 4 main-thread : 5 other-thread: 5 main-thread : 6 other-thread: 6 main-thread : 7 other-thread: 7 main-thread : 8 other-thread: 8 main-thread : 9 other-thread: 9 */ thrd_t tid; if(thrd_create(&tid, thread_proc, "other-thread") != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } struct timespec ts; ts.tv_sec = 1; ts.tv_nsec = 0; for(int i = 0; i < 10; ++i){ printf("%s : %d\n", "main-thread", i); thrd_sleep(&ts, NULL); } if(thrd_join(tid, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } return 0; } int thread_proc(void* param) { char* name = (char*)param; struct timespec ts; ts.tv_sec = 1; ts.tv_nsec = 0; for(int i = 0; i < 10; ++i){ printf("%s: %d\n", name, i); thrd_sleep(&ts, NULL); } return 0; } C11 ile birlikte senkronizasyon için sadece "mutex" ve "conditional variables" nesneleri bulundurulmuştur. "mutex" nesnesi "mtx_t" türü ile temsil edilmekte olup, kullanım biçimi aşağıdaki gibidir: -> "mtx_init" -> "mtx_lock" veya "mtx_timedlock" veya "mtx_trylock" -> "mtx_unlock" -> "mtx_destroy" Aşağıda ise bu fonksiyonların kullanımına bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte "mutex" koruması YAPILMAMIŞTIR. #include #include #include int g_count; int thread_proc1(void* param); int thread_proc2(void* param); int main() { /* # OUTPUT # Result: 0 Result: 1082511 */ printf("Result: %d\n", g_count); thrd_t tid1, tid2; if(thrd_create(&tid1, thread_proc1, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_create(&tid2, thread_proc2, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid1, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid2, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } printf("Result: %d\n", g_count); return 0; } int thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return 0; } int thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return 0; } * Örnek 2, Aşağıdaki örnekte "mutex" koruması YAPILMIŞTIR. #include #include #include int g_count; mtx_t g_mutex; int thread_proc1(void* param); int thread_proc2(void* param); int main() { /* # OUTPUT # Result: 0 Result: 2000000 */ printf("Result: %d\n", g_count); if(mtx_init(&g_mutex, mtx_plain) != thrd_success){ fprintf(stderr, "cannot init mutex!..\n"); exit(EXIT_FAILURE); } thrd_t tid1, tid2; if(thrd_create(&tid1, thread_proc1, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_create(&tid2, thread_proc2, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid1, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid2, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } printf("Result: %d\n", g_count); return 0; } int thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i){ mtx_lock(&g_mutex); ++g_count; mtx_unlock(&g_mutex); } return 0; } int thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i){ mtx_lock(&g_mutex); ++g_count; mtx_unlock(&g_mutex); } return 0; } Burada bizler ilgili kütüphanenin çok az bir kısmına değindik. Kütüphanedeki diğer Standart C Fonksiyonları için ilgili dökümanlara bakabiliriz. >> C++ dilindeki "thread" kütüphanesi: Şablon temelli bir kütüphane olup, sınıfsal bir tasarıma sahiptir. Yine 2011 yılında dile eklenmiştir. C dilinin kütüphanesine nazaran daha ayrıntılıdır. Bu sınıf türünden nesnenin "ctor." fonksiyonuna akışın başlayacağı fonksiyonu geçebilmekteyiz. Fakat mutlak suretle ".join()" ya da ".detach()" isimli fonksiyonları çağırmamız gerekmektedir aksi halde bir "exception throw" söz konusu olacaktır. * Örnek 1, #include #include #include void thread_proc(); int main() { /* # OUTPUT # main-thread: 0 other-thread: 0 main-thread: other-thread: 11 other-thread: main-thread: 2 2 main-thread: other-thread: 3 3 other-thread: main-thread: 44 other-thread: 5 main-thread: 5 other-thread: main-thread: 66 main-thread: other-thread: 7 7 other-thread: main-thread: 8 8 other-thread: main-thread: 99 */ std::thread t{thread_proc}; for(int i = 0; i < 10; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "main-thread: " << i << "\n"; } t.join(); return 0; } void thread_proc() { for(int i = 0; i < 10; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "other-thread: " << i << "\n"; } } * Örnek 2, #include #include #include void thread_proc(int count); int main() { /* # OUTPUT # main-thread: 10 other-thread: 0 other-thread: 1 main-thread: 11 other-thread: 2 main-thread: 12 other-thread: 3 main-thread: 13 other-thread: 4 main-thread: 14 other-thread: 5 main-thread: 15 other-thread: 6 main-thread: 16 other-thread: 7 main-thread: 17 other-thread: 8 main-thread: 18 other-thread: 9 main-thread: 19 */ std::thread t{thread_proc, 10}; for(int i = 10; i < 20; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "main-thread: " << i << "\n"; } t.join(); return 0; } void thread_proc(int count) { for(int i = 0; i < count; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "other-thread: " << i << "\n"; } } Yine C++ dilinde de çeşitli senkronizasyon nesneleri bulunmaktadır. Bu nesneler ise "mutex" isimli başlık dosyasındadır. Artık "lock" ve "unlock" işlemleri ilgili sınıfın üye fonksiyonları üzerinden gerçekleştirilmektedir. * Örnek 1, #include #include #include #include int g_count; std::mutex g_mutex; void thread_proc1(int count); void thread_proc2(int count); int main() { /* # OUTPUT # Count: 0 Count: 2000000 */ std::cout << "Count: " << g_count << "\n"; std::thread t1{thread_proc1, 10}; std::thread t2{thread_proc2, 100}; t1.join(); t2.join(); std::cout << "Count: " << g_count << "\n"; return 0; } void thread_proc1(int count) { for(int i = 0; i < 1000000; ++i){ g_mutex.lock(); ++g_count; g_mutex.unlock(); } } void thread_proc2(int count) { for(int i = 0; i < 1000000; ++i){ g_mutex.lock(); ++g_count; g_mutex.unlock(); } } Buradaki C++ kütüphanesi C dilinin kütüphanesinden daha kapsamlıdır. Kütüphane şablon temelli olması hasebiyle, kullanırken daha da dikkatli olmamız gerekmektedir. Tabii C++ dilindeki bu kütüphane, UNIX/Linux ve macOS sistemlerinde "pthread", Windows sistemlerinde ise "Windows API" fonksiyonları kullanılarak gerçekleştirilmiştir. > Hatırlatıcı Notlar: >> "sleep()" fonksiyonu, kendisini çağıran "thread" i belirtilen süre boyunca bloke etmektedir. >> "thread" lerde "Race Condition" durumu senkronize edilmemiş "thread" ler için kullanılan genel bir kavramdır. >> "quanta" süresi biten "thread" tekrardan "Run Queue" sırasına alınır. Eğer koşul sağlanmadığı için bloke edilmesi veya herhangi bir sebepten dolayı bloke edilmesi durumunda "Wait Queue" sırasına alınır. Şartların sağlanmasından sonra tekrardan "Run Queue" sırasına alınır. >> Semafor nesneleri de yine "Kernel Persistance" biçimindedir. Ya sistem "boot" edilene kadar ya da o semafor nesnesi silinene kadar sistemde kalmaktadır. >> Aslında bekleme kuyrukları wait_queue_entry isimli yapı nesnelerinden oluşan bir çift bağlı listedir. wait_queue_head_t yapısı da bağlı listenin ilk ve son elemanlarının adresini tutmaktadır: wait_queue_head ---> wait_queue_entry <-----> wait_queue_entry <-----> wait_queue_entry <-----> wait_queue_entry ... Çekirdek kodlarında bu yapılar "include/linux/wait.h" dosyası içersinde aşağıdaki gibi bildirilmiştir: struct wait_queue_head { spinlock_t lock; struct list_head head; }; struct wait_queue_entry { unsigned int flags; void *private; wait_queue_func_t func; struct list_head entry; }; >> CPU'ya atanan "thread" in zorla kopartılmasına İngilizce'de "preemptive" denir. >> "task_struct" içerisinde saklanan "quanta" sayacı, aslında "quanta" süresidir. Her ne kadar daha önceki derslerde bu değer için tipik olarak 60 milisaniye olduğunu belirtsek de "nice" değeri gibi faktörlere bağlı olarak değişmektedir. Buradaki "nice" değerinin yüksek olması, diğerlerine karşı nazik olunacağı anlamına gelmektedir. Bundan dolayıdır ki öncelik olarak diğerlerinin daha yüksek önceliği olacaktır, CPU kullanımı açısından. >> Linux sistemlerinde "thread" ler için de "task_struct" yapısı kullanılmaktadır. Dolayısıyla "thread" lerin de bir ID değeri bulunmaktadır. Dolayısıyla bir prosesin ID değerini almak istediğimiz zaman, o prosesin "main-thread" inin ID değerini elde etmiş olmaktayız. POSIX standartları bu biçimdeki "task_struct" kullanımını desteklememektedir. >> Spesifik bir "thread" in "nice" değerini değiştirmeye yarayan POSIX fonksiyonu mevcut değildir. "setpriority" gibi POSIX fonksiyonlar prosesinkini değiştirdiği için, o prosesteki bütün "thread" lerinki de değişecektir. "getpriority" de yine benzer şekilde prosesinkini aldığı için ve o prosesteki bütün "thread" lerin değeri aynı olduğu için yine bir sorun yoktur. Ancak Linux sistemlerinde ise sadece "main-thread" e ilişkin değerler etkilenmektedir. Dolayısıyla Linux sistemlerde spesifik bir "thread" e ilişkin "nice" değerini değiştirebiliriz, ona ait ID değerini kullanarak. >> Gerek "nice" gerek "setpriority" fonksiyonları sadece "main-thread" in "nice" değerini değiştirmektedir. Dolayısıyla bir "thread" oluşturduktan sonra "nice" değerini değiştirirsek, sadece fonksiyonu çağıranınki değişecektir. Velev ki oluşturmadan önce "nice" değerini değiştirseydik, bu değer oluşturulan "thread" e aktarılacaktı. Tabii bu durum Linux için geçerlidir çünkü POSIX standartlarınca bütün "thread" ler etkilenecektir. >> "pthread_t" türü ile "tid_t" türleri arasındaki fark şudur: Anımsanacağı üzere POSIX standartlarınca yalnızca prosesler ID değerine sahiptir. Fakat Linux sistemlerinde durum böyle değildir. Bu sistemlerde her bir "thread" kendi ID değerine sahiptir. Dolayısıyla Linux sistemlerinde hangi akış "gettid()" fonksiyonunu çağırırsa, kendisine ait olan ID değerini elde edecektir. Eğer bu sistemlerde "getpid" fonksiyonu çağırırsak, "main-thread" in ID değerini elde etmiş oluruz. Diğer yandan POSIX sistemlerinde "gettid" fonksiyonu bulunmamaktadır. Dolayısıyla bu sistemlerde "getpid" fonksiyonunu çağırırsak, prosesin ID değerini elde etmiş oluruz. Özetle; POSIX Linux getpid() Proses main-thread gettid() N/A the-thread >> Bu dünyada senkron ve asenkron kelimeleri şu anlamlarda kullanılırlar: -> Asenkron demek; esas akış devam ederken, o olayın arka planda yapılması demektir. Örneğin, biz bir yerden okuma yapacağız. Asenkron çalışırsak, okuma işlemini başlatırız fakat onun bitmesini beklemeyiz. Arka planda okuma işi yapılırken, esas akış da kendi işine bakar. -> Senkron demek; esas akışın devam edebilmesi için, o olayın tamamlanması gerekmektedir. >> İşletim sistemleri dünyasında bir fonksiyonun senkron işlem yapması demek; o fonksiyon geri döndüğünde o işin bitmiş olmasının garanti edilmesi demektir. Örneğin "read", "write" gibi fonksiyonlar senkron işlem yapmaktadır. Çünkü "read" fonksiyonu geri döndüğünde okuma işi bitmiş durumdadır. Öte yandan bir fonksiyonun asenkron işlem yapması demek; o fonksiyon geri döndüğünde o işin bitmek zorunda olmadığı anlamına gelmektedir. Örneğin, "kill" fonksiyonu geri döndüğünde sinyalin ilgili proses tarafından işlenmiş olduğunun bir garantisi yoktur.