> Threads : Kursumuzun bu bölümünde thread kavramını göreceğiz. Çok thread'li çalışma modeli ve thread senkronizasyonu konularını ele alacağız. Bir prosesin bağımsız olarak çizelgelenen farklı akışlarına "thread" denilmektedir. (Thread sözcüğü İngilizce "iplik" anlamına gelmektedir. Akışlar ipliğe benzetilerek bu sözcük uydurulmuştur.) Proses kavramı çalışmakta olan programın tüm bilgilerini temsil eden bir kavramdır. Thread'ler proseslerin akışlarını temsil etmektedir. Yani thread'ler proses'lerin bir unsurudur. Bir proses tek bir akışa (yani thread'e) sahip olabileceği gibi birden fazla akışa (yani thread'e) sahip olabilmektedir. Thread'ler 90'lı yılların ortalarında işletim sistemlerine sokulmuştur. Bugün artık thread'ler programlamanın önemli konularından sayılmaktadır. Windows sistemi ilk kez Windows NT ile (1993) sonra da Windows 95 ile thread'li çalışma modeline sahip olmuştur. Benzer biçimde UNIX/Linux sistemlerine de yine 90'lı yılların ortalarında thread'ler eklenmiştir. İşletim sistemlerinde prosesler çalışmaya tek bir thread'le başlamaktadır. Bu thread proses yaratılırken işletim sistemi tarafından yaratılmaktadır. Prosesin bu thread'ine "ana thread (main thread)" denilmektedir. Prosesin diğer thread'leri program çalışırken sistem fonksiyonlarıyla ya da bunları çağıran kütüphane fonksiyonlarıyla yaratılmaktadır. Yani program tek bir thread'le çalışmaya başlamaktadır. Diğer thread'ler programcı tarafından oluşturulmaktadır. Thread'ler tamamen işletim sisteminin kontrolü altında yaratılıp çalıştırılmaktadır. Dolayısıyla Windows sistemlerinde thread'ler Windows API fonksiyonları ile, UNIX/Linux ve macOS sistemlerinde ise POSIX fonksiyonlaır ile yaratılıp idare edilmektedir. Bazı programlama dillerinin standart kütüphanelerinde platform bağımzıs thread sınıfları ya da fonksiyonları bulunabilmektedir. Tabii burada platform bağımsızlık kaynak kod temelindedir. Bu kütüphaneler aslında farklı sistemlerde o sistemlerin sistem fonksiyonlarını çağırarak thread işlemlerinş yapmaktadır. Örneğin C++11 ile birlikte C++'a bir thread kütüphanesi eklenmiştir. Biz C++'ta threda işlemlerini hep aynı biçimde yapabiliriz. Ancak buradaki fonksiyonlar ve sınıflar Windows sistemlerinde Windows API fonksiyonları, UNIX/Linux sistemleridne POSIX fonksiyonları çağrılarak işlemlerini gerçekleştirmektedir. Pekiyi thread'lere neden gereksinim duyulmaktadır? Bunu birkaç maddeyle açıklayabiliriz: -> Thread'ler arkaplan işlemlerin yapılması için iyi araç oluşturmaktadır. Örneğin biz bir yandan bir şeyler yaparken arka planda da periyodik birtakım işlemleri yapmak isteyebiliriz. Thread'ler yokken bu tür işlemler çok zor yapılıyordu. Ancak thread'ler işletim sistemlerine eklenenince bu işlemleri yapmak çok kolaylaştı. Tread'ler sayesinde biz normal işlemlerimizi yaparken bir thread oluşturup arkaplan işlemleri o thread'e havale edebiliriz. -> Thread'ler programları hızlandırmak amacıyla kullanılabilmektedir. Bir işi birden fazla kaışa yaptırmak hız kazancı sağlamaktadır. Sistemimizde tek bir işlemci olsa bile bizim prosesimiz diğer proseslere göre daha fazla CPU zamanı kullanabilir hale gelebilmektedir. -> Thread'ler blokeye yol açan durumlarda işlemlerin devam ettirilmesini sağlayabilmektedir. Çünkü işletim sistemleri thread temelinde bloke uygulamaktadır. Örneğin prosesin bir thread'i bir kalvye okuması sırasında blokede beklerken diğer thread'leri çalışmaya devam edecektir. Bu durum da prosesin başka işlemlere yanıt verebilmesini sağlamaktadır. -> Thread'ler paralel programlama için mecburen kullanılması gereken unsurlardır. Paralel programlama "birden fazla CPU ya da çekirdeğin olduğu durumda prosesin thread'lerinin farklı CPU ya da çekirdeklere atanarak aynı anda çalıştırıması" anlamına geşmektedir. Örneğin çok büyük bir diziyi sıraya dizecek olalım. Makinemizde 10 tane çekirdek olsun. Biz bu işlemi tek bir thread'le yaparsak makinemizdeki 10 çekirdek olmasının avantajından faydalanamayız. Halbuki biz dizimizi 10 parçaya ayırıp 10 farklı thread'i farklı çekirdeklere atarsak bunlar paralel bir biçimde dizinin çeşitli parçalarını aynı anda sort edecektir. Sonra bunları birleştirirsek toplamda zamanı çok kısaltmış olabiliriz. -> Thread'ler GUI programlama ortamlarında bir mesaj geldiğinde uzun süren işlemlerin yapılabilmesine olanak sağlamaktadır. Bir proses çalışmaya tek bir thread ile başlar. Buna prosesin ana thread'i (main thread) denilmektedir. Bu ana thread C programlarında tipik olarak akışın main fonksiyonundan girdiği thread'tir. Diğer thread'ler işletim sistem fonksiyonlarıyla yaratılmaktadır. Tabii Windows'ta bu sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux sistemlerinde de POSIX fonksiyonları bulunmaktadır. Modern işletim sisemlerinin çizelgeleyici (scheduler) alt sistemleri thread'leri çizelgelemektedir. Yani işletim sistemi hangi prosese ilişkin olursa olsun sıradaki thread'i CPU'ya atar, onu belli süre çalıştırır. Sonra çalışmasına ara vererek sonraki thread'i CPU'ya atar. Modern çok thread'li (multithreaded) işletim sistemlerinde prosesler değil thread'ler çizelgelenmektedir. Yukarıda da belirttiğimiz gibi prosesin bir thread'i bloke olduğunda diğerleri çalışmaya devam edebilmektedir. Bir thread'in çalışmasına ara verilerek diğer bir thread'in kaldığı yerden çalışmasına devam ettirilmesi sürecine "bağlamsal geçiş (context switch)" denilmektedir. Bağlamsal geçiş sırasında önceki thread ile sonraki thread aynı prosesin thread'leri olabildiği gibi farklı proseslerin thread'leri de olabilir. Çok işlemcili ya da çekirdekli sistemlerde zaman paylaşımlı çalışma modeli değişmemektedir. Yalnızca servis veren birden fazla işlemci ya da çekirdek söz konusu olmaktadır. Thread'lerin sıra beklediği kuyruk sistemine işletim sistemleri terminolojisinde "çalışma kuyruğu (run queue)" denilmektedir. Çok işlemcili ya da çekirdekli sistemlerde çalışma kuyruğu bir tane olabilir ya da her işlemci ya da çekirdek için ayrı bir çalışma kuyruğu söz konusu olabilir. Örneğin Linux bir ara O(1) çizelgelemesi adı altında toplamda bir tane çalışma kuyruğu oluşturuyordu. Hangi işlemci ya da çekirdekteki thread'in işi biterse o çalışma kuyruğundan o işlemci ya da çekirdeğe atama yapıyordu. Ancak daha sonra bu çizelgeleme algoritması yine değiştirildi. Bugünkü çizelgeleme algoritmasında her işlemci ya da çekirdeğin ayrı bir çalışma kuyruğu bulunmaktadır. Tabii işletim sistemi bu tür durumlarda işlemci ya da çekirdeklerin çalışma kuyruklarını iş yükü bakımından dengelemeye çalışmaktadır. Thread'lerin stack'leri birbirinden ayrılmıştır. Yerel değişkenlerin "stack" denilen alanda yaratıldığını anımsayınız. Thread'lerin stack'leri biribirindne ayrıldığı için bir thread akışı bir fonksiyon üzerinde ilerlerken o fonksiyonun yerel değişkenleri o stack üzerinde yaratılmaktadır. Diğer bir thread de aynı fonksiyon üzerinde ilerliyorsa o yerel değişkenlee de o thread'in stack'inde yaratılacaktır. Böylece iki thread aynı kod üzerinde ilerliyor olsa da aslında yerel değişkenlerin farklı kopyalarını kullanıyor olacaklardır. Başka bir deyişle yerel değişkenlerin her thread için ayrı bir kopyası bulunmaktadır. Örneğin: void foo(void)) { int a; a = 10; ... ++a; ... ++a; ... } İki farklı thread bu foo fonksiyonunda ilerliyor olsun. Burada aslında her thread'in ayrı bir a değişkeni vardır. Dolayısıyla thread'lerden biri bu a değişkenini değiştirdiğinde kendi thread'inin stack'indeki a değişkenini değiştirmiş olur. Bu değişiklikten diğer thread'in a değişkeni etkilenmeyecektir. Ancak global değişkenler tüm thread'ler tarafından ortak biçimde kullanılmaktadır. Başka bir deyişle ".data" ve ".bss" bölümleri thread'e özgü değil prosese özgüdür. Örneğin bir thread bir global değişkeni değiştirdiğinde diğer thread o global değişkeni değişmiş görmektedir. Benzer biçimde heap alanı da prosese özgüdür. Yani thread'leri ayrı heap'leri yoktur. Toplamda bir tane heap vardır. O da prosesin heap alanıdır. Bir işin birden fazla akışa yaptırılması gerektiği durumlarda thread'ler proseslere göre çok daha etkin bir çözüm sunmaktadır. Çünkü yaratılması proseslerin yaratılmasından daha hızlı yapılmakta ve thread'ler proseslere göre daha az sistem kaynağı harcamaktadır. Prosesler önceki konularda da gördüğümüz gibi proseslerarası haberleşme yöntemleri (IPC) denilen yöntemlerle haberleşmektedir. Oysa thread'ler global nesneler yoluyla haberleşebilmektedir. Eskiden thread'ler yokken bir işin birden fazla akışa yaptırılması prosesler yoluyla gerçekleştiriliyordu. Oysa thread'ler işletim sistemlerine girince artık bunun için thread'ler kullanılmaya başlanmıştır. Artık günümüzde thread'ler ptogramlamanın temel unsurları durumuna gelmiştir. Pek çok programla dilinde thread'lerle kolay işlemler yapabilmek için standart kütüpaheneler bulunmaktadır. Hatta bazı dillerde artık thread'ler dilin sentaksının da içine sokulmuştur. Eskiden işlemciler mega hertz düzeyinde çalışıyordu. Zamanla bunların hızları 1000 kat civarında artırıldı. Ancak artırmanın fiziksel bir sınırına da yaklaşıldı. Artık hızlandırma işlemciyi bireysel olarak hızlandırmak yerine birden fazla işlemci (ya da çekirdek) kullanarak sağlanmaktadır. İşletim sistemleri de işlemcilere ya da çekirdeklere thread'leri atamaktadır. İşletim sistemlerinin çizelgeleyici alt sistemlerinin atama birimleri thread'lerdir. Eskiden board'lara birden fazla işlemci takılabiliyordu. Ancak zamanla teknoloji gelişince birden fazla işlemci tek bir chip'e yerleştirilmeye başlandı. Tek bir chip içindeki farklı işlemcilere "çekirdek (core)" denilmeye başlandı. Yukarıda da belirttiğimiz gibi thread'ler işletim sistemlerinin sistem fonksiyonlarıyla yaratılmaktadır. Windows'ta bu sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux be macOS sistemlerinde de POSIX fonksiyonları vardır. Yüksek seviyeli programlama dillerinin kütüphanelerinde buluan thread fonksiyonları da aslında bu fonksiyonlar kullanılarak yazılmıştır. Örneğin biz C# ya da Java'da thread yarattığımızda aslında bu dillerin kütüphaneleri yaratımı Windows sistemlerinde Windows API fonksiyonlarını çağırarak UNIX/Linux ve macOS sistemlerinde de POSIX fonksiyonlarını çağırarak yapmaktadır. Thread'ler çeşitli biçimlerde sonlanabilmektedir. Bir thread'in en doğal sonlanması thread fonksiyonunun bitmesi ile gerçekleşir. Hem Windows sistemlerinde hem de UNIX/Linux ve macOS sistemlerinde thread fonksiyonu bittiğinde thread'ler de otomatik olarak sonlanmaktadır. Aşağıdaki örneklerimizde sonlanma bu biçimde doğal yolla gerçekleşmiştir. Tavsiye edilen sonlanma biçimi böyledir. Thread'lerin stack'leri birbirlerinden ayrılmıştır. Bu nedenle farklı thread akışları aynı fonksiyon üzerinde ilerlerken o fonksiyondaki yerel değişkenlerin ve parametre değişkenlerinin farklı kopyalarını kullanıyor durumda olurlar. Aşağıdaki Windows örneğinde ana thread ve yaratılan thread aynı Foo fonksiyonunu çağırmıştır. Ancak Foo içerisindeki i nesnesi iki threda için de farklı i nesneleridir. Bu örneği i yerel değişkenini global değişken yaparak da çalışırınız. İki çalıştırma arasındaki farkı gözlemleyiniz. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); Foo("main thread"); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { Foo("other thread"); return 0; } int i = 0; void Foo(LPCSTR pszName) { while (i < 10) { printf("%s: %d\n", pszName, i); Sleep(1000); ++i; } } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Şimdi de sistemler özelinde thread'leri inceleyelim: >> Windows Sistemlerinde: Windows sistemlerinde thread'ler CreateThread isimli API fonksiyonuyla yaratılmaktadır. Fonksiyonun prototipi şöyledir: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); Fonksiyonun birinci parametresi thread kernel nesnesinin güvenlik bilgilerini belirtmektedir. Bu parametre NULL geçilebilir. İkinci parametre yaratılacak thread'in stack miktarını byte olarak belirtmektedir. Bu parametre 0 geçilirse default stack uzunluğu çalıştırılabilir dosyada (PE formatında) belirtilen uzunluk olarak alınır. Genel olarak default uzunluk 1MB'dir. Fonksiyonun üçüncü parametresi thread akıının başlatılacağı fonksiyonun adresini almaktadır. Her thread bizim belirlediğimiz bir fonksiyondan çalışmaya başlar. LPTHREAD_START_ROUTINE aşağıdaki gibi typedef edilmiştir: typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter); Görüldüğü gibi LPTHREAD_START_ROUTINE geri dönüş değeri DWORD, parametresi LPVOID (yani void *) türünden olan bir fonksiyon adresini belirtmektedir. 32 bit Windows sistemlerinde "fonksiyon çağırma (calling convention)" biçimi __stdcall olmak zorundadır. Bu __stdcall anahtar sözcüğü Microsoft'a özgü olan bir eklentidir (extension). __stdcall WINAPI olarak da define edişmiştir: #define WINAPI __stdcall Bu çağırma biçimi Microsoft derleyicilerinde fonksiyon isminin solunda bulundurulmak zorundadır. Örnek bir thread fonksiyonu şöyle olabilir: DWORD WINAPI ThreadProc(LPVOID param) { ... } 64 bit Windows sistemlerinde "çağırma biçimi (calling convention)" kaldırılmıştır. Bu nedenle bus sistemlerde __stdcall önişlemci komutlarıyla aşağıdaki gibi silinmiştir: #define __stdcall O halde Windows programımızı 32 bit ve 64 bit uyumlu yazmak istiyorsak bu çağırma biçimini fonksiyonun soluna yazabiliriz. Nasıl olsa 64 bit derlemede bu çağırma biçimi silinecektir. CreateThread fonksiyonunun dördüncü parametresi thread fonksiyonuna geçirilecek olan parametreyi belirtmektedir. Thread'leryaratıldığında akış başlatılacağı thread fonksiyonuna bu parametrede belirtilen değer aktarılmaktadır. Biz böyle bir parametre geçmek istemiyorsak bu parametreyi NULL biçimde belirtebiliriz. Fonksiyonun beşinci parametresi tipik olarak 0 biçiminde ya da CREATE_SUSPENDED biçiminde geçilir. Eğer bu parametre 0 geçilirse thread yaratılır yaratılmaz çalışmaya başlar. Eğer bu parametreye CREATE_SUSPENDED değeri geçilirse threda yaratılır ancak henüz çalışmaya başlamaz. Thread'i çalıştırmak için ResumeThread API fonksiyonu kullanılmalıdır. Fonksiyonun son parametresi thread'in ID değerinin yerleştirileceği DWORD nesnesinin adresini almaktadır. Bu ID değeri bazı durumlarda gerekebilmektedir. Eğer thread'in ID değeri alınmayacaksa bu parametreye NULL geçilebilir. CreateThread fonksiyonu başarı durumunda yaratılan thread'in handle değerine, başarısızlık durumunda NULL adres değerine geri dönmektedir. Buradan elde edilen handle değeri diğer thread işlemlerinde kullanılmaktadır. Thread'in değeri thread işlemlerinde kullanılmaz. Handle değeri thread işlemlerinde kullanılmaktadır. Örneğin: HANDLE hThread; DWORD dwThreadId; DWORD __stdcall ThreadProc(LPVOID param); ... if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); ... Thread fonksiyonu bittiğinde thread kaynaklarının önemli bir bölümü zaten boşaltılmaktadır. Ancak thread kernel nesnesinin tam olarak boşaltımını sağlamak için CloseHandle fonksiyonu uygulanabilir. Aşağıdaki örnekte bir thread yaratılmıştır. Hem ana threadhem de yaratılan thread çalışmaktadır. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); for (int i = 0; i < 10; ++i) { printf("Main thread: %d\n", i); Sleep(1000); } CloseHandle(hThread); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Bir thread herhangi bir noktada Windows'ta ExitThread API fonksiyonu ile sonlandırabilir. Bu fonksiyonları hangi thread akışı çağırırsa o thread sonlanmaktadır. exit fonksiyonunun tüm prosesi sonlandırdığına ancak ExitThread fonksiyonunun yalnızca tek bir thread'i sonlandırdığına dikkat ediniz. Therad'lerin de tıpkı prosesler gibi exit kodları vardır. Windows sistemlerinde thread'lerin exit kodları DWORD değerle temsil edilmektedir. Thread'in exit kodları thread sonlandığında ilgili prosesler tarafından alınıp çeşitli amaçlarla kullanılabilmektedir. Ancak uygulamaların çoğunda bu exit kodunu kullanamaya gerek duyulmamaktadır. Windows'taki ExitThread API fonksiyonunun prototipi şöyledir: void ExitThread( DWORD dwExitCode ); Fonksiyon thread'in exit kodunu parametre olarak almaktadır. Aşağıda Windows sistemlerinde ExitThread fonksiyonu ile thread'in sonlandırılmasına bir örnek verilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); for (int i = 0; i < 10; ++i) { printf("Main thread: %d\n", i); Sleep(1000); } CloseHandle(hThread); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); if (i == 5) ExitThread(0); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Threadler arasında Windows sistemlerinde ve UNIX/Linux sistemlerinde üstlük/altlık (parent/child) ilişkisi yoktur. Yani bir thread'i hangi thread'in yarattığının genel olarak bir önemi yoktur. (Bu konuda bazı ayrıntılar bulunmaktadır.) Bir thread başka bir thread'i Windows sistemlerinde TerminateThread API fonksiyonuyla o anda zorla sonlandırabilir. Bu tür sonlandırmalar tavsiye edilmemektedir. Çünkü bir thread'in belli bir noktada (örneğin printf fonksiyonunun içerisinde) zorla sonlandırılması programın çökmesine yol açabilmektedir. TerminateThread API fonksiyonunun prototipi şöyledir: BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); Fonksiyonun birinci parametresi sonlandırılack thread'inb handle değerini almaktadır. İkinci parametre ise thread'in exit kodunu belirtmektedir. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Aşağıdaki örnekte ana thread diğer thread'i zorla TerminateThread API fonksiyonuyla sonlandırmıştır. Burada tüm program bu zorla sonlandırmadan olumsuz etkilenebilir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); for (int i = 0; i < 10; ++i) { if (i == 5) if (!TerminateThread(hThread, 0)) ExitSys("TerminateThread"); printf("Main thread: %d\n", i); Sleep(1000); } CloseHandle(hThread); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Bir thread'in sonlanmasının blokede beklenmesi sıkça gerekebilmektedir. Örneğin ana thread bir thread yaratıp belli bir noktada thread sonlanana kadar beklemek isteyebilir. Windows sistemlerinde thread sonlanana kadar bekleme yapmak için WaitForSingleObject ve WaitForMultipleObjects isimli API fonksiyonları bulunmaktadır. Bu fonksiyonlar yalnızca thread'leri beklemek için değil diğer kernel senkronizasyon nesnelerini de beklemek için kullanılmaktadır. Yani bu fonksiyonlar genel bir bekleme amacıyla tasarlanmıştır. Biz bu fonksiyonların kernel senkronizasyon nesneleriyle nasıl kullanılacağını ilerleyen paragraflarda göreceğiz. Ancak burada yalnızca bu fonksiyonların thread'leri beklemek için nasıl kullanılacağı üzerinde duracağız. >>> WaitForSingleObject fonksiyonun prototipi şöyledir: DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds ); Yukarıda da belirttiğimiz gibi WaitForSingleObject fonksiyonu genel bir fonksiyondur. Bu fonksiyon bir senkronizasyon nesnesi kapalı (nonsignaled) olduğu sürece bekleme yapar. Senktronizasyon nesnesi açık duruma (signaled) geçtiğinde bekleme sonlanır. Her senkronizasyon nesnesinin hangi durumda kapalı hangi durumda açık olduğu ayrıca öğrenilmelidir. İşte thread'ler de bir senkronizasyon nesnesi gibi kullanılabilmektedir. Thread senkronizasyon nesnesi thread devam ettiği sürece kapalı durumdadır. Thread sonlanınca açık duruma geçer. O halde bu fonksiyon thread bitene kadar bekleme sağlamaktadır. Fonksiyonun ikinci parametresi milisaniye cinsinden "zaman aşımı (timeout)" belirtmektedir. Eğer nesne burada belirtilen zaman aşımı dolana kadar açık hale gelmezse en fazla bu zaman aşıma kadar bekleme yapılmaktadır. Bu parametre için INFINITE özel değeri girilirse zaman aşımı kullanılmaz. NEsne açık duruma geçene kadar bekleme yapılır. WaitForSingleObject fonksiyonu başarısızlık durumunda WAIT_FAILED değeri ile geri dönmektedir. Bunun dışında diğer geri dönüş değerleri şunlardan biri olabilir: -> WAIT_OBJECT_0: Nesne açık duruma geçiği için fonksiyon sonlanmıştır. Bu en normal durumdur. -> WAIT_TIMEOUT: Zaman aşımı nedenyiyle fonksiyon sonlanmıştır. -> WAIT_ABONDONED: Mutex'in beklendiği durumda mutex'in sahipliğini alan thread'in sonlanması nedeniyle fonksiyon sonlanmıştır. Bu duruma "abondoned mutex" denilmektedir. WaitForSingleObject fonksyionu çağrıldığında zaten nesne açık durumdaysa (signaled) fonksiyon hiç bekleme yapmaz, WAIT_OBJECT_0 değeri ile hemen geri döner. Bu durumda bir thread sonlanana kadar bekleme yapmak şöyle sağlanabilir: if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED) ExitSys("WaitForSingleObject") Aşağıda bu programa ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); printf("main thread waits at WaitForSingleObject...\n"); if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED) ExitSys("WaitForSingleObject"); CloseHandle(hThread); printf("main thread continues...\n"); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> WaitForMultipleObjects fonksiyonu birden fazla senkronizasyon nesnesini beklemek için kullanılmaktadır. Örneğin biz 10 thread yaratmış olalım. Bunların hepsi sonlanana kadar bekleme yapmak isteyelim. Bunun bir yolu WaitForSingleObject fonksiyonunu 10 kez çağırmaktır. Diğer bir yolu ise bir kez WaitForMultipleObject fonksiyonunu kullanmaktır. WaitForMultipleObjects fonksiyonunun prototipi şöyledir: DWORD WaitForMultipleObjects( DWORD nCount, const HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds ); Fonksiyonun birinci parametresi kaç senkronizasyon nesnesinin bekleneceğini belirtmektedir. (Yani bu parametre ikinci parametredeki dizinin uzunluğunu belirtir.) İkinci parametre beklenecek senkronizasyon nesnelerinin hhandle değerlerinin bulundurğu dizinin adresini almaktadır. Üçüncü parametre tek bir nesnenin mi yoksa bütün nesnelerin mi açık duruma geçince beklemenin sonlandırılacağını belirtir. Eğer bu parametre TRUE geçilirse tüm nesneler açık duruma geçene kadar bekleme yapılır. Eğer bu parametre FALSE geçilirse en az bir nesne açık duruma geçene kadar bekleme yapılır. Son parametre yine zaman aşımı belirtmektedir. Bu parametre INFINITE geçilebilir. WaitForMultiplrObjects fonksiyonu başarısız olursa WAIT_FAILED değerine geri dönmektedir. Eğer fonksiyon zaman aşımı dolayısıyla sonlanmışsa yine WAIT_TIMEOUT değerine geri döner. Eğer fonksiyonun üçüncü parametresi TRUE geçilirse tüm senkronizasyon nesneleri açıldığında fonksiyon WAIT_OBJECT_0 değerinden WAIT_OBJECT_0 + nCunt değerine kadar herhangi bir değerle geri dönebilir. Eğer fonksiyonun üçüncü parametresi FALSE geçilirse fonksiyon hangi senkronizasyon nesnesi açık duruma geçtiyse ona ilişkin WAIT_OBJET_0 + n değerine geri döner. Burada n açık duruma geçen nesnenin dizideki indeksini belirtmektedir. Bu durumda birden fazla nesne açık duruma geçerse fonksiyon en düşük indeksle geri dönmektedir. Aşağıdaki örnekte 10 thread yaratılmış ve 10 thread'in sonlanması WaitForMultipleObjects fonksiyonu ile beklenmiştir. * Örnek 1, #include #include #include #define NTHREADS 10 DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadIds[NTHREADS]; HANDLE hThreads[NTHREADS]; char szName[64]; char *pszName; printf("main thread begins...\n"); for (int i = 0; i < NTHREADS; ++i) { sprintf(szName, "Thread %d", i); if ((pszName = strdup(szName)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if ((hThreads[i] = CreateThread(NULL, 0, ThreadProc, pszName, 0, &dwThreadIds[i])) == NULL) ExitSys("CreateThread"); } printf("main thread waits at WaitForSingleObject...\n"); if (WaitForMultipleObjects(NTHREADS, hThreads, TRUE, INFINITE) == WAIT_FAILED) ExitSys("WaitForMultipkeObjects"); for (int i = 0; i < NTHREADS; ++i) CloseHandle(hThreads[i]); printf("main thread continues...\n"); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { const char *pszName = (const char *)param; for (int i = 0; i < 10; ++i) { printf("%s: %d\n", pszName, i); Sleep(1000); } free(pszName); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Thread'lerin de exit kodları vardır. Windows'ta thread'lerin exit kodları GetExitCodeThread fonksiyonu ile, UNIX/Linux sistemlerinde sonraki paragrafta göreceğimiz pthread_join fonksiyonu ile elde edilmeketdir. Tabii sonlanmamış bir thread'in exit kodunun elde edilmeye çalışılması anlamsızdır. Therad'lerin exit kodları thread fonksiyonlarının geri dönüş değerleridir. Windows'ta thread'lerin exit kodları DWORD bir değerken UNIX/Linux sistemlerinde void * türünden bir değerdir. Anımsanacağı gibi thread'lerde üstlük-altlık (parent-child) durumu yoktur. Bir thread'in exit kodu herhangi bir thread tarafındna alınabilir. Windows'ta GetExitCodeThread fonksiyonunun protoipi şöyledir: BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode ); Foksiyonun birinci parametresi exit kodu elde edilecek thread'in HANDLE değerini belirtir. İkinci parametre exit kodunun yerleştirileceği DWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri döner. Aşağıdaki örnekte Windows'ta yaratılan bir thread'in exit kodu elde eidlmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; DWORD dwExitCode; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); printf("main thread waits at WaitForSingleObject...\n"); if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED) ExitSys("WaitForSingleObject"); if (!GetExitCodeThread(hThread, &dwExitCode)) ExitSys("GetExitCodeThread"); CloseHandle(hThread); printf("Exit Code: %u\n", dwExitCode); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 123; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Thread'ler konusunun en önemli alt konusu thread senkronizasyonudur. Bir grup thread birlikte bir işi gerçekleştirirken kimi zaman birbirlerini beklemesi, birbirleriyle koordineli bir biçimde çalışması gerekmektedir. İşte işletim sistemlerinde bunu sağlamaya yönelik mekanizmalara "thread senkronizasyonu" denilmektedir. Thread senkronizasyonunun önemi basit örnekle anlaşılabilir. Thread'lerin aynı global nesneleri kullandığını belirtmiştik. İki thread aynı global değişkeni bir döngü içerisinde bir milyon kere artırıyor olsun. Bu global değişkenin değerinin iki milyon olması beklenir. Ancak senkronizasyon problemi yüzünden muhtemelen iki milyon olamayacaktır. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); int g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); printf("%d\n", g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Thread senkronizasyon problemleri tipik olarak birden fazla thread'in ortak bir kaynak üzerinde çalıştığı durumda ortaya çıkmaktadır. Thread'lerden biri bu ortak kaynak üzerinde yazma (gemel olarka güncelleme) yaptığı bir durumda bu işlemler sırasında thread'ler arası geçiş oluşursa ve başka bir thread de bu kararsız durumda kalmış kaynağı kullanırsa ya da o da bu kaynağa yazma yaparsa diğer thread kaldığı yerden çalışmasına devam ettiğinde sorun oluşacaktır. Burada kaynak (resource) demekle ortak kullanılan bir nesne kastedilmektedir. Bu nesne global bir değişken olabileceği gibi dış dünyadaki gerçek bir donanımsal aygıt olabilir. Örneğin iki thread dış dünyadaki bir makineyi sırasıyla 1, 2, 3, 4, 5 konumlarına sokarak kullanıyor olsun. Bu kodu aşağıdaki gibi temsil edelim: ... ... Şimdi thread'lerden biri aşağıdaki noktada threadler arası geçiş (context switch) oluşup kesilmiş olsun: ... ----> Bu noktada "context switch" olsun ... Başka bir thread aynı makineyi kullanmak istesin ve başından sonuna kadar kesilmeden makineyi konumlara sokarak kullansın. Makine şimdi beşinci konumdadır. Şimdi daha önce kesilen thread kaldığı noktadn çalışmaya devam etsin. Bu thread makineyi 3'üncü konumda sanmaktadır. Halbuki makine şu anda 5'inci konumdadır. Muhtemelen beklenmedik sorunlar oluşacaktır. Şimdi de yukarıdaki sayaç örneğinin neden düzgün çalışmadığını açıklayalım. Bu örnekte iki thread de ++g_count ile global değişkeni 1 artırmaktadır. Ancak derleyiciler bu ++g_count işlemini tipik olarak üç makine komutuyla yapmaktadır: MOV reg, g_count INC reg MOV g_count, reg Önce g_count CPU yazmacına çekilmiştir. Sonra bu yazmaç değer 1 artırılmıştır. Sonra da artırılmış değer yeniden g_count nesnesine yerleştirilmiştir. Şimdi tam aşağıdaki noktada tesadüfen bir thread'ler arası geçişin oluştuğunu varsayalım: MOV reg, g_count ----> Bu noktada "context switch" olsun INC reg MOV g_count, reg Bu noktada g_cunt değerinin 1250305 olduğunu varsayalım. Şimdi diğer thread kesilmedne bir quanta çalışıp g_count değerini örneğin 1756340'a getirmiş olsun. Önceki thread kalan noktadan çalışmaya devam ettiğinde yazmaçta 1250305 değeri olacaktır. Bunu 1 artırdığında 1250306 elde edilir. Bu değeri yeniden g_count nesnesine yerleştrecek ve g_count'taki değeri bozacaktır. Thread'ler arası geçiş bir makine komutu çalışırken oluşmaz, iki makine komutu arasında oluşabilir. Ancak hangi makine komutunda bu geçişin oluşacağu "quanta" durumuna bağlıdır. Dolayısıyla bir thread herhangi bir makine komutunda kesilebilmektedir. Başından sonuna kadar tek bir thread akışı tarafından çalıştırılması gereken kod bloklarına "kritik kod blokları (critical sections)" denilmektedir. Kritik kod bloklarına bir thread girdiğinde thread'ler arası geçiş (context switch) oluşabilir. Ancak diğer thread'ler bu bloğa girmek istediğinde daha girmiş olan thread'in buradan çıkmasını beklerler. Böylece başından sonuna kadar tek bir threda akışı tarafından kritik kodlar çalıştırılmış olur. Yukarıdaki örneklerimizde g_count nesnesinin artırılması bir kritik koddur. Örneğin: ... MOV reg, g_count INC reg MOV g_count, reg ... Bu üç makine komutu bir kritik kod oluşturmaktadır. Yani thread bu kodları çalıştırırken kesilebilir ancak başka bir thread diğeri çıkana kadar bu kritik koda girmez. Tabii kritik kod oluşturmanın diğer bir yolu geçici süre thread'ler arası geçişi engellemek olabilir. Ancak bu yöntemin user mode'tan uygulanması mümkün değildir. Yukarıdaki makine örneğimizde de makineyi kullanan kod kritik bir koddur: ... ... Bir thread bu kritik koda girdiğinde arada kesilse bile başka thread bu thread kritik koddan çıkana kadar kritik koda girmemelidir. Senkronizasyon bağlamında bir grup makine komutunun sanki tek bir makine komutuymuş gibi kesilmeden çalıştırılmasına "atomiklik (atomicity)" denilmektedir. Yani "bir işlemin atomik bir biçimde yapılması" demek "kesilmeden başka bir deyişle thread'ler arası geçiş oluşmadan" yapılması demektir. Kritik kodların oluşturulabilmesi işletim sistemi tarafından sağlanan özel sistem fonksiyonları ya da bunları kullanan kütüphane fonksiyonlarıyla sağlanabilmektedir. Kritik kodlar manuel biçimde işletim sisteminin desteği olmadan ya da özel birtakım yööntemler kullanılmadan oluşturulamaz. Örneğin aşağıdaki gibi bir kritik kod oluşturma mümkün değildir: int g_flag = 0; ... while (g_flag == 1) ; g_flag = 1; ...... ...... ...... g_falg = 0; Buradaki kodun iki önemli problemi vardır: -> Burada tam while döngüsünden çıkılmışken ancak g_flag = 1 işlemi yapılmadan thread'ler arası geçiş oluşabilir: while (g_flag == 1) ; ----> Dikkat tan bu noktada "context switch" olabilir g_flag = 1; ...... ...... ...... g_falg = 0; Bu durumda g_flag 0 konumunda kalmıştır ancak thread kritik koda girmiştir. Yani bir thread de bu thread de kritik kodda ilerleyebilir. -> Burada bekleme "meşgul bir döngüyle (busy loop)" yapılmaktadır. Yani bekleme yapılırken gereksiz biçimde CPU zamanı harcanmaktadır. İşte bu tür kodlar özel birtakım mekanizmaalr olmadan oluşturulamamaktadır. Kritik kodlar tek bir blok biçiminde olmayabilir. Birden fazla yere yayılmış olarak bulunabilirler. Örneğin global bir bağlı listeye thread'lerden biri ekleme yaparken diğer bir thread ekleme de silme de hatta dolaşma da yapmamı gerekir. Burada bağlı listeye ekleme yapan, bağlı listeden silme yapan ve bağlı listeyi dolaşan kodlar kritik kodlardır. Windows'ta kritik kod oluşturmak için en yalın ve hızlı yöntem CRITICAL_SECTION isimli nesneyi kullanmaktır. Bu nesne ile kritik kodlar şöyle oluşturulmaktadır: -> Önce global bir değişken biçiminde CRITICAL_SECTION türünden bir nesne yaratılır. CRITICAL_SECTION typedef edilmiş bir yapı türünü belirtmektedir. Ancak programcının bu yapının içeriğini bilmesine gerek yoktur. Örneğin: CRITICAL_SECTION g_cs; -> Yaratılan bu nesneye initializeCriticalSection API fonksiyonuyla ilkdeğerleri verilir. Fonksiyonun prototipi şöyledir: void InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); Fonksiyon CRITICAL_SECTION nesneninin adresini parametre olarak almaktadır. Bu ilkdeğer verme işlemi henüz thread'ler yaratılmadan yapılabilir. Örneğin: InitializeCriticalSection(&g_cs); -> Kritik kod EnterCriticalSection ve LeaveCriticalSection API fonksiyonları arasına alınır. Örneğin: EnterCriticalSection(&g_cs); ... ... ... LeaveCriticalSection(&g_cs); Fonksiyonların prototipleri şöyledir: void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); Her iki fonksiyon da CRITICAL_SECTION nesnesinin adresini almaktadır. Bir thread EnterCriticalSection fonksiyonundan girdiğinde LeaveCriticalSection fonksiyonunu çağırana kaadar nesneyi kilitlemiş olur. Böylece başka bir thread EnterCriticalSection fonksiyonundan geçiş yapma istediğinde bloke olur. Ta ki önceki thread LeaveCriticalSection fonksiyonunu çağırana kadar. CRITICAL_SECTION nesnesinin bir kilit gibi davrandığına dikkat ediniz. Bir thread bu fonksiyondan geçtiğinde nesne kilitlenmekte başka thread'ler kritik koda girememektedir. LeaveCriticalSection kritik kodun kilidini açmaktadır. Bir thread EnterCriticalSection fonksiyonundan geçerek nesneyi kilitlemiş olsun. Bu sırada birden fazla thread nesne kilitli olduğu için EnterCriticalSection fonksiyonunda bekliyor olsun. İlk thread kritik koddan çıktığında EnterCriticalSection fonskiyonunda bloke olmuş olan hangi thread kritik koda girecektir? En adil durumun ilk gelen thread'in girmesi olduğunu düşünebilirsiniz. Ancak Windows sistemleri çeşitli nedenlerden dolayı bunun garantisini vermemektedir. -> Kullanım bittikten sonra CRITICAL_SECTION nesnesi DeleteCriticalSection fonksiyonu ile yok edilmeldir. Örneğin: DeleteCriticalSection(&g_cs); Fonksiyonun prototipi şöyledir: VOID DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); Fonksiyon yine CRITICAL_SECTION nesnesinin adresini almaktradır. Kritik kod bloğu birden fazla yere yayılmış olarak bulunabilir. Önemli olan Bu fonksiyonlarda kullanılan nesnedir. Aynı nesne aynı kilidi temsil etmektedir. Örneğin: void insert_item(...) { ... EnterCriticalSection(&g_s); .... .... .... LeaveCriticalSection(&g_s); ... } void delete_item(...) { ... EnterCriticalSection(&g_s); .... .... .... LeaveCriticalSection(&g_s); ... } Burada bir thread eleman insert ederken diğer thread elemanı silemeyecektir, bir thread eleman silerken diğer thread eleman insert edemeyecektir. Çünkü bu iki kritik kod aynı nesneyi yani kilidi kullanmaktadır. Dolayısıyla aslında aynı kritik kodun değişik parçalarıdır. * Örnek 1, Aşağıda daha önce yapmış olduğumuz sayaç artırma örneğini CRITICAL_SECTION nensnesi kullanarak düzeltiyoruz: #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); int g_count; CRITICAL_SECTION g_cs; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; InitializeCriticalSection(&g_cs); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); printf("%d\n", g_count); DeleteCriticalSection(&g_cs); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); ++g_count; LeaveCriticalSection(&g_cs); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); ++g_count; LeaveCriticalSection(&g_cs); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aağıdaki örnekte iki thread aynı makineyi sırasıyla 1, 2, 3, 4 ve 5 numaralı konumlara sokmaktadır. Kritik kod bloğu sayesinde thread'lerden biri kritik koda girdiğinde diğeri kiritk koda girmemektedir. #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void UseMachine(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); CRITICAL_SECTION g_cs; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; InitializeCriticalSection(&g_cs); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); DeleteCriticalSection(&g_cs); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Therad-1"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Thread-2"); return 0; } void UseMachine(LPCSTR pszName) { EnterCriticalSection(&g_cs); printf("-------------------\n"); printf("%s: 1. Step\n", pszName); Sleep(rand() % 500); printf("%s: 2. Step\n", pszName); Sleep(rand() % 500); printf("%s: 3. Step\n", pszName); Sleep(rand() % 500); printf("%s: 4. Step\n", pszName); Sleep(rand() % 500); printf("%s: 5. Step\n", pszName); Sleep(rand() % 500); LeaveCriticalSection(&g_cs); } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 3, Örneğin iki thread aynı global bağlı listeye ekleme yapacak olsun. Eğer işlemler senkronize edilmezse program çökebilir ya da tanımsız davranışlar oluşabilir. Aşağıda buna bir örnek verilmiştir. Örneği kritk kodları kaldırarak da test ediniz. /* sample.c */ #include #include #include #include "llist.h" DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); CRITICAL_SECTION g_cs; HLLIST g_hllist; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; InitializeCriticalSection(&g_cs); if ((g_hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list!..\n"); exit(EXIT_FAILURE); } if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); DeleteCriticalSection(&g_cs); printf("%zd\n", count_llist(g_hllist)); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); if (add_tail(g_hllist, i) == NULL) { fprintf(stderr, "cannot add item..\n"); exit(EXIT_FAILURE); } LeaveCriticalSection(&g_cs); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); if (add_tail(g_hllist, i) == NULL) { fprintf(stderr, "cannot add item..\n"); exit(EXIT_FAILURE); } LeaveCriticalSection(&g_cs); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE head; NODE *tail; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val); NODE *add_tail(HLLIST hllist, DATATYPE val); NODE *addp_tail(HLLIST hllist, const DATATYPE *val); NODE *add_head(HLLIST hllist, DATATYPE val); NODE *addp_head(HLLIST hllist, const DATATYPE *val); void remove_next(HLLIST hllist, NODE *node); void remove_head(HLLIST hllist); DATATYPE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } #endif /* llist.c */ #include #include #include "llist.h" /* static Functions Prototypes */ static bool disp(DATATYPE *val); /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; return hllist; } NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *add_tail(HLLIST hllist, DATATYPE val) { return insert_next(hllist, hllist->tail, val); } NODE *addp_tail(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, hllist->tail, val); } NODE *add_head(HLLIST hllist, DATATYPE val) { return insert_next(hllist, &hllist->head, val); } NODE *addp_head(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, &hllist->head, val); } void remove_next(HLLIST hllist, NODE *node) { NODE *next_node; if (node == hllist->tail) return; if (node->next == hllist->tail) hllist->tail = node; next_node = node->next; node->next = next_node->next; --hllist->count; free(next_node); } void remove_head(HLLIST hllist) { remove_next(hllist, &hllist->head); } DATATYPE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head.next; for (size_t i = 0; i < index; ++i) node = node->next; return &node->val; } bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } void clear_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; } void destroy_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } free(hllist); } static bool disp(DATATYPE *val) { printf("%d ", *val); fflush(stdout); return true; } * Örnek 4, Aşağıda C++'ta birden fazla thread'in "vector" isimli dinamik diziye ekleme yapmasına ilişkin benzer bir örnek verilmiştir. Bu örneği de krtik kodları kaldırarak ve muhafaza ederek ayrı ayrı test ediniz. #include #include #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); CRITICAL_SECTION g_cs; std::vector g_v; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; int i; srand(time(NULL)); InitializeCriticalSection(&g_cs); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); DeleteCriticalSection(&g_cs); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) { EnterCriticalSection(&g_cs); g_v.push_back(i); LeaveCriticalSection(&g_cs); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) { EnterCriticalSection(&g_cs); g_v.push_back(i); LeaveCriticalSection(&g_cs); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Kritik kod oluşturmak için kullanılan diğer bir senkronizasyon mekanizması da "mutex (mutual exclusion)" denilen mekanizmadır. Mutex nesneleri pek çok işletim sisteminde benzer bir biçimde bulunmaktadır. Örneğin CRITICAL_SECTION nesnesi Windows sistemlerine özgü olduğu halde mutex nesneleri hem Windows, hem UNIX/Linux hem de macOS sistemlerinde benzer biçimde bulunmaktadır. Mutex nesnelerinin thread temelinde bir "sahipliği (ownership)" vardır. Bir mutex nesnesinin sahipliğini bir thread almış ise o thread o mutex nesnesini kilitlemiştir. Başka bir thread mutex nesnesinin sahipliğine almaya çalışırsa diğer thread sahipliği bırakana kadar blokede bekler. Mutex nesnesinin sahipliğini nesnenin sahipliğin almış olan thread bırakabilmektedir. Eğer bir thread bir mutex nesnesinin sahipliğini almışken onu bırakmadan sonlanırsa böyle mutex nesnelerine "terkedilmiş mutex nesneleri (abondened mutexes)" denilmektedir. Windows'ta mutex nesneleri hem aynı prosesin thread'leri arasında hem de farklı proseslerin thread'leri arasında senkronizasyon amacıyla kullanılabilmektedir. Windows sistemlerinde aynı prosesin thread'leri arasında kritik kod oluşturmak için CRITICAL_SECTION nesneleri mutex nesnelerinden daha hızlı çalışmaktadır. Windows'ta mutex nesneleri şöyle kullanılmaktadır: -> Önce mutex nesnesi CreateMutex isimli API fonksiyonuyla yaratılır. Tabii bu yaratım henüz thread'ler yaratılmadan önce yapılabilir. CreateMutex fonksiyonun prototipi şöyledir: HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName ); Fonksiyonun birinci parametresi mutex nesnesinin güvenlik bilgilerini belirlemek için kullanılmaktadır. Bu parametre NULL geçilebilir. (Windows'ta mutex nesneleri birer kernel nesnesidir. Tüm kernel nesnelerinin SECURITY_ATTRIBUTES türünden bir güvenlik parametresi vardır. Bu konu oldukça karmaşık bir konu olduğu için "Windows Sistem Programlama" kursunda ele alınmaktadır.) Fonksiyonun ikinci parametresi TRUE geçilirse mutex nesnesini yaratan thread aynı zamanda sahipliğinde alır (yani aynı zamanda mutex nesnesini kilitler.) Bu parametre tipik olarak FALSE biçiminde geçilmektedir. Fonksiyonun son parametresi mutex nesnesi farklı prosesler tarafından kullanılacaksa nesneyi temsil eden ismi belirtmektedir. Bu isim programcı tarafından herhangi bir biçimde verilebilir. Eğer aynı prosesin thread'leri arasında senkronizasyon yapılacaksa bu parametre NULL geçilmelidir. Fonksiyon başarı durumunda yaratılan mutex nesnesinin handle değerine başarısızlık durumunda NULL adrese geri dönmektedir. Eğer aynı prosesin thread'leri arasında senkronizasyon yapılacaksa bu durumda CreateMutex fonksiyonundan elde edilen handle değeri global bir değişkene atanmalıdır. Böylece bu global değişken farklı thread'lerden kullanılabilecektir. Örneğin: HANDLE g_hMutex; ... if ((g_hMutex = CreateMutex(NULL, FALSE, NULL)) == NULL) ExitSys("CreateMutex"); -> Kritik kod aşağıdaki gibi WaitForSingleObject (ya da WaitForMultipleObjects) ve ReleseMutex API fonksiyonları arasına yerleştirilmektedir. WaitForSingleObject(g_hMutex, INFINITE); ... ... KRİTİK KOD ... ReleaseMutex(g_hMutex); WaitForSingleObject fonksiyonunu daha önce görmüştük. Bu fonksiyon (ve WaitForMultipleObjects fonksiyonu) senkronizasyon nesnelerini beklemek için kullanılan genel bir fonksiyondu. Eğer WaitForSingleObejct fonksiyonu ile bir mutex nesnesi bekleniyorsa fonksiyon nesnenin sahipliğini başka bir thread almamışsa nesnenin sahipliğini alarak kritik koda girişi sağlar. Eğer nesnenin sahipliği başka bir thread tarafından alınmışsa WaitForSingleObject fonksiyonu nesnenin sahipliğini almış olan thread bu sahipliği bırakana kadar blokede beklemektedir. Böylece aynı anda tek bir thread'in kritik koda girişine izin verilmektedir. Eğer nesnenin sahipliğini almış olan thread sahipliğini bırakmadan sonlanırsa bu durumda WaitForSingleObject fonksiyonu "kilitlenme (deadlock)" oluşmasını engellemek için WAIT_ABANDONED özel değeri ile geri dönmektedir. ReleaseMutex fonksiyonu mutex nesnesinin sahipliğini bırakmak için kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL ReleaseMutex( HANDLE hMutex ); Fonksiyon mutex nesnesinin handle değerini parametre olarak alır ve nesnenin sahipliğini bırakır. Başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. -> Mutex nesnesinin kullanımı bittikten sonra nesne CloseHandle fonksiyonu ile yok edilebilir. (Anımsanacağı gibi Windows'te ismine "kernel nesneleri (kernel objects)" denilen tüm nesneler ortak biçimde CloseHandle fonksiyonuyla kapatılmaktadır). Aşağıda Windows'ta mutex nesneleri ile kritik kod oluşturulmasına bir örnek verilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void UseMachine(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); HANDLE g_hMutex; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_hMutex = CreateMutex(NULL, FALSE, NULL)) == NULL) ExitSys("CreateMutex"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(g_hMutex); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Therad-1"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Thread-2"); return 0; } void UseMachine(LPCSTR pszName) { WaitForSingleObject(g_hMutex, INFINITE); printf("-------------------\n"); printf("%s: 1. Step\n", pszName); Sleep(rand() % 500); printf("%s: 2. Step\n", pszName); Sleep(rand() % 500); printf("%s: 3. Step\n", pszName); Sleep(rand() % 500); printf("%s: 4. Step\n", pszName); Sleep(rand() % 500); printf("%s: 5. Step\n", pszName); Sleep(rand() % 500); ReleaseMutex(g_hMutex); } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Windows'ta mutex nesneleri farklı proseslerin thread'lerini senkronize etmek için de kullanılabilmektedir. Bunun için CreateMutex fonksiyonunun üçüncü (son) parametresine mutex nesnesi için bir isim girilir. İki farklı proses CreateMutex fonksiyonunda mutex nesnlerine aynı isimleri verirse bu durumda iki farklı mutex yaratılmamakta aynı mutex nesnesi üzerinde çalışılmaktadır. Başka bir deyişle CreateMutex fonksiyonu son parametresiyle belirtilen isimde daha önce bir mutex nesnesi yaratılmışsa yeni bir mutex nesnesi yaratmaz. Zaten yaratılmış olan mutex nesnesi açmış olur. Böylece iki proses de aynı isimle CreateMutex fonksiyonunu çağırdığında yalnızca bunlardan biri mutex nesnesini yaratacak diğer yaratılmış olan nesneyi açacaktır. Tabii mutex nesnesine verilen isme dikkat edilmelidir. Eğer sistemde o isimli bir mutex nesnesi zaten başkaları tarafından yaratılmışsa böyle bir yaratım yapılmayacaktır. Pekiyi farklı proseslerin thread'lerinin senkronize edilmesi neden gerekebilir? İşte prosesler kendi aralarında "paylaşılan bellek alanlarıyla" ortak veriler üzerinde işlem yapıyor olabilirler. Bu durumda bu paylaşılan bellek alanındaki verilerin prosesler arası çalışan senkronizasyon nesneleriyle senkronize edilmesi gerekebilmektedir. Windows sistemlerinde mutex nesneleri "özyinelemeli (recursive)" davranışa sahiptir. Bir mutex nesnesinin özyinelemeli olması demek mutex nesnesinin thread tarafından sahipliği alındığında aynı thread'in yeniden aynı mutex nesnesinin sahipliğini bloke olmadan alabilmesi demektir. Örneğin: void foo(void) { ... WaitForSingleObject(g_hMutex, INFINITE); ... bar(); ... ReleaseMutex(g_hMutex); ... } void bar(void) { ... WaitForSingleObject(g_hMutex, INFINITE); ... ... ... ReleaseMutex(g_hMutex); ... } Burada programcının foo fonksiyonunu çağırdığını varsayalım. foo fonksiyonu Mutex'in sahipliğini aldıktan sonra bar fonksiyonunu çağırdığında aynı thread aynı mutex'in sahipliğini ikinci kez almaktadır. İşte Windows'ta bu durumda bir sorun oluşmamaktadır. Ancak thread mutex'in sahipliğini ne kadar almışsa ReleaseMutex ile o kadar bırakmalıdır. Aşağıdaki örnekte iki proses paylaşılan bellek alanı oluşturup oradaki bir sayacı belli miktar artırmaktadır. Senkronizasyon yapılmaığı durumda bu sayaç değeri yanlış çıkacaktır. Senkronizasyon yapıldığında sayacın artırılması seri hale getirildiği için sorun oluşmayacaktır. * Örnek 1, /* prog1.c */ #include #include #include #define SHARED_MEMORY_NAME "MySharedMemory" #define MUTEX_NAME "MyMutexObject" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileMapping; HANDLE hMutex; long long *pCount; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, SHARED_MEMORY_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((pCount = (long long *) MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 4096)) == NULL) ExitSys("MapViewOfFile"); if ((hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME)) == NULL) ExitSys("CreateMutex"); for (long long i = 0; i < 10000000; ++i) { WaitForSingleObject(hMutex, INFINITE); ++*pCount; ReleaseMutex(hMutex); } printf("Press ENTER to continue...\n"); getchar(); printf("%lld\n", *pCount); UnmapViewOfFile(pCount); CloseHandle(hFileMapping); getchar(); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #define SHARED_MEMORY_NAME "MySharedMemory" #define MUTEX_NAME "MyMutexObject" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileMapping; HANDLE hMutex; long long *pCount; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, SHARED_MEMORY_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((pCount = (long long *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 4096)) == NULL) ExitSys("MapViewOfFile"); if ((hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME)) == NULL) ExitSys("CreateMutex"); for (long long i = 0; i < 10000000; ++i) { WaitForSingleObject(hMutex, INFINITE); ++*pCount; ReleaseMutex(hMutex); } UnmapViewOfFile(pCount); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Diğer çok kullanılan bir senkronizasyon nesneleri de "semaphore" denilen nesnelerdir. (Semaphore trafikteki dur-geç lambalarına denilmektedir. eskiden trafik ışıkları yokken onun yerine insanların manuel idare ettiği bayraklar kullanılıyormuş.) Semapore'lar sayaçlı senkronizasyon nesneleridir. Bir kritk koda en fazla N tane thread'in (akışın) girmesini sağlamak için kullanılmaktadır. Daha önce görmüş olduğumuz mutex nesneleri ve Windows'taki CRITCAL_SECTION nesneleri kritik koda yalnızca tek bir akışın girmesini sağlamaktadır. Semaphore'lar bilgisayar bilimlerinin öncülerinden Edsger Dijkstra ("edsger daystra" gibi okunuyor) tarafından bulunmuştur. Bir kritik koda birden fazla akışın girmesinin anlamı nedir? Normalde kritik koda giren iki akış bile ortak kullanılan kaynakları bozabilir. İşte semaphore'lar özellikle "kaynak paylaşımını sağlamak" için kullanılmaktadır. Örneğin elimizde 3 makine olsun. Biz de bu 3 makineyi 10 thread'e paylaştırmak isteyelim. Yani thread'ler makineyi talep etsin, eğer bir makine boştaysa o makine ilgili thread'e atansın. Ancak tüm makineler doluysa makine talep eden thread makinelerden biri boşaltılana kadar CPU zamanı harcamadan blokede beklesin. İşte bu tür problemler tipik olarak semaphore nesneleriyle çözülmektedir. Semaphore nesnelerinin bir sayacı vardır. Kritik koda her giren thread eğer sayaç 0'dan büyükse sayacı 1 eksiltir. Eğer sayaç 0 ise blokede sayacın 0'dan büyük olmasını bekler. Örneğin başlangıçtaki semaphore sayacı 3 olsun. Bir thread kritik koda girdiğinde semaphore sayacı 2 olacaktır. Diğer bir thread kritik koda girdiğinde semaphore sayacı 1 olacaktır. Diğer bir thread kritik koda girdiğinde ise semaphore sayacı 0 olacaktır. Artık gelen thread'ler kritik koda giremeyip blokede bekleyecektir. Artık kritik kodun içerisinde 3 tane thread vardır. Şimdi bir thread kritik koddan çıkıyor olsun. Thread kritik koddan çıkarken semaphore sayacı 1 artırılır. Böylece semaphore sayacı 1 olur. Kritik kodda şu anda 2 thread vardır. İşte semaphore sayacı artık 0'dan büyük olduğu için bekleyen thread'lerden biri de kritik koda girgirer. Böylece semaphore sayacı yeniden 0 olur. Şimdi kritik kod içerisinde yine 3 thread vardır. Başlangıçtaki semaphore sayacı kaç ise kritik kodun içerisinde "en fazla" o kadar thread bulunacaktır. Başlangıçtaki semaphore sayacı 1 ise kritik koda en fazla 1 thread girebilir. Bu tür semaphore'lara "ikili semaphore'lar (binary semaphores)" denilmektedir. İkili semaphore'lar mutex nesnelerine benzese de onlardan önemli bir farklılığa sahiptir. Mutex nesnelerinin sahipliğini (yani kilidini) ancak sahipliğini almış olan thread bırakabilir. Ancak semaphore sayaçları başka thread'ler tarafından artırılabilmektedir. Windows sistemlerinde semaphore nesneleri şu adımlardan geçilerek kullanılmaktadır: -> Önce semaphore nesnesi CreateSemaphore API fonksiyonuyla yaratılır. CreateSemaphore API fonksiyonunun prototipi şöyledir: HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCSTR lpName ); Fonksiyonun birinci parametresi semaphore nesnesinin güvenlik bilgilerini belirtir. Bu parametre NULL adres geçilebilir. İkinci parametre semaphore sayacının başlangıçtaki sayaç değerini belirtir. Üçüncü parametre semaphore sayacının erişebileceği maksimum sayaç değeridir. Genellikle ikinci ve üçüncü parametreye aynı değer girilmektedir. Son parametre semaphore nesnesinin proseslerarası kullanımdaki ismini belirtmektedir. Eğer semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa bu parametreye NULL adres geçilebilir. Fonksiyon başarı durumunda semaphore nesnesinin handle değerine başarısızlık durumunda NULL adrese geri dönmektedir. Semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa CreateSemaphore fonksiyonunun geri dönüş değeri (yani nesnenin handle değeri) global bir değişkende tutulmalıdır. Örneğin: HANDLE g_hSemaphore; ... if ((g_hSemaphore = CreateSemaphore(NULL, 1, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); -> Kritik kod aşağıdaki gibi oluşturulmaktadır: WaitForSingleObject(g_hSemaphore, INFINITE); ... ... KRİTİK KOD ... ReleaseSemaphore(g_hSemahore, 1, NULL); Burada akış WaitForSingleObject fonksiyonuna geldiğinde eğer semaphore sayacı 0'dan büyükse bloke olunmadan kritik koda girilir. Ancak semaphore sayacı atomik bir biçimde 1 eksiltilir. Eğer semaphore sayacı 0 ise WaitForSingleObject bloke oluşturarak thread'i bekletir. ReleaseSemaphore fonksiyonu semaphore sayacını ikinci parametresinde belirtilen miktar kadar (genellikle 1) artırmaktadır. ReleaseSemaphore fonksiyonun prototipi şöyledir: BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount ); Fonksiyonun birinci parametresi semaphore nesnesinin handle değerini almaktadır. İkinci parametre artırım değerini belirtmektedir. (Bu değer hemen her zaman 1 olur) son parametre semaphore sayacının önceki değerinin yerleştirileceği nesnenin adresini almaktadır. Bu parametre NULL adres biçiminde geçilirse önceki sayaç değeri yerleştirilmez. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı bir değere geri dönmektedir. Örneğin: for (int i = 0; i < 1000000; ++i) { WaitForSingleObject(g_hSemaphore, INFINITE); ... ... ... ReleaseSemaphore(g_hSemaphore, 1, NULL); } -> Semaphore kullanımı bittiğinde semaphore nesnesi diğer kernel nesnelerinde oluduğu gibi CloseHandle fonksiyonu ile yok edilmelidir. Örneğin: CloseHandle(g_hSemaphore); Aşağıdaki daha önce yaptığımız mutex örneği semaphore nesneleriyle gerçekleştirilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); HANDLE g_hSemaphore; int g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_hSemaphore = CreateSemaphore(NULL, 1, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(g_hSemaphore); CloseHandle(hThread1); CloseHandle(hThread2); printf("%d\n", g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { WaitForSingleObject(g_hSemaphore, INFINITE); ++g_count; ReleaseSemaphore(g_hSemaphore, 1, NULL); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { WaitForSingleObject(g_hSemaphore, INFINITE); ++g_count; ReleaseSemaphore(g_hSemaphore, 1, NULL); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Üretici-Tüketici Problemi (Producer-Consumer Problem) gerçek hayatta en sık karşılaşılan senkronizasyon problemlerinden biridir. Bu problemde thread'lerden biri (üretici thread) bir döngü içerisinde bir değer üretir ve onu paylaşılan bellek alanına yerleştirir. Diğer thread de (tüketici thread) bir döngü içerisinde onu oradan alıp kullanır. Burada şöyle bir sorun vardır: Üretici thread henüz tüketici thread eski değeri almdan paylaşılan alana yeni değeri yerleştirirse eski değer ezilir. Benzer biçimde tüketici thread de üretici thread paylaşılan alana yeni bir değer koymadan paylaşılan alandan değeri alırsa eski değeri yeniden almış olur. O halde iki thread'in bu işi düzgün yapabilmesi için uygun biçimde senkronize edilmesi gerekmektedir. Öyle ki üretici thread tüketici thread eski değeri almadan paylaşılan alana yeni değeri yerleştirmemeli, tüketici thread de üretici thread paylaşılan alana yeni bir değer yerleştirmeden eski değeri yeniden almamalıdır. Üretici-Tüketici probleminde aslında üretici thread'ler ve tüketici thread'ler birden fazla olabilmektedir. Örneğin üç thread üretici iki thread tüketici olabilir. Ancak problemin en basit halinde tek üretici ve tek tüketici vardır. Pekiyi üretici-tüketici probleminde neden üretici thread elde ettiği değeri kendisi işlemiyor da onu tüketici thread'e pas edip onun işlemesini sağlıyor? İşte bunun en açık nedeni hız kazancı sağlamaktır. Tek bir thread bu işi yaptığında önce değeri elde edip işleyecek ve sonra yeni değeri elde edip işleyecektir. Ancak thread'lerden biri değeri elde ederken diğeri onu işlerse işlemler toplamda daha hızlı yürütülmüş olur. Üretici-Tüketici probleminde paylaşılan alan tek bir değeri içerecek biçimde olmayabilir. Bu alan bir kuyruk sistemi biçiminde de olabilir. Böylece üretici ve tüketici birbirlerini daha az bekler. Uygulamada genellikle paylaşılan alan bir kuyruk sistemi biçiminde olarak organize edilmektedir. * Örnek 1, Windows sistemlerinde üretici-tüketici probleminin semaphore nesneleriyle çözümüne ilişkin bir örnek aşağıda verilmiştir. Bu örnek aşağıda verilen UNIX/Linux örneğinin Windows eşdeğeri gibidir. #include #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProducer(LPVOID lpvParam); DWORD __stdcall ThreadConsumer(LPVOID lpvParam); HANDLE g_hSemProducer; HANDLE g_hSemConsumer; int g_shared; int main(void) { HANDLE hThreadProducer, hThreadConsumer; DWORD dwThreadIDProducer, dwThreadIDConsumer; srand(time(NULL)); if ((g_hSemProducer = CreateSemaphore(NULL, 1, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((g_hSemConsumer = CreateSemaphore(NULL, 0, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((hThreadProducer = CreateThread(NULL, 0, ThreadProducer, NULL, 0, &dwThreadIDProducer)) == NULL) ExitSys("CreateThread"); if ((hThreadConsumer = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, &dwThreadIDConsumer)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThreadProducer, INFINITE); WaitForSingleObject(hThreadConsumer, INFINITE); CloseHandle(hThreadProducer); CloseHandle(hThreadConsumer); CloseHandle(g_hSemProducer); CloseHandle(g_hSemConsumer); return 0; } DWORD __stdcall ThreadProducer(LPVOID lpvParam) { int val; val = 0; for (;;) { Sleep(rand() % 300); WaitForSingleObject(g_hSemProducer, INFINITE); g_shared = val; ReleaseSemaphore(g_hSemConsumer, 1, NULL); if (val == 99) break; ++val; } return 0; } DWORD __stdcall ThreadConsumer(LPVOID lpvParam) { int val; for (;;) { WaitForSingleObject(g_hSemConsumer, INFINITE); val = g_shared; ReleaseSemaphore(g_hSemProducer, 1, NULL); printf("%d ", val); fflush(stdout); if (val == 99) break; Sleep(rand() % 300); } putchar('\n'); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Yukarıda da belirttiğmiz gibi üretici-tüketici probleminde paylaşılan alan bir kuyruk sistemi olursa üretici ve tüketicinin birbirlerini bekleme olasılıkları azaltılmış olur. Çünkü bu durumda üretici yalnızca "kuyruk tamamen doluyken", tüketici ise yalnızca "kuyruk tamamen boşken" bekleyecektir. Üretici-Tüketici probleminin kuyruklu versiyonunda üretici semaphore sayacının başlangıçta kuyruk uzunluğuna kurulması gerekmektedir. (Yani tüketici hiç çalışmasa üretici tüm kuyruğu doldurup bekleyecektir.) #include #include #include #include #define QUEUE_BUFFER_SIZE 10 void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProducer(LPVOID lpvParam); DWORD __stdcall ThreadConsumer(LPVOID lpvParam); HANDLE g_hSemProducer; HANDLE g_hSemConsumer; int g_queueBuf[QUEUE_BUFFER_SIZE]; size_t g_head; size_t g_tail; int main(void) { HANDLE hThreadProducer, hThreadConsumer; DWORD dwThreadIDProducer, dwThreadIDConsumer; srand(time(NULL)); if ((g_hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((g_hSemConsumer = CreateSemaphore(NULL, 0, QUEUE_BUFFER_SIZE, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((hThreadProducer = CreateThread(NULL, 0, ThreadProducer, NULL, 0, &dwThreadIDProducer)) == NULL) ExitSys("CreateThread"); if ((hThreadConsumer = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, &dwThreadIDConsumer)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThreadProducer, INFINITE); WaitForSingleObject(hThreadConsumer, INFINITE); CloseHandle(hThreadProducer); CloseHandle(hThreadConsumer); CloseHandle(g_hSemProducer); CloseHandle(g_hSemConsumer); return 0; } DWORD __stdcall ThreadProducer(LPVOID lpvParam) { int val; val = 0; for (;;) { Sleep(rand() % 300); WaitForSingleObject(g_hSemProducer, INFINITE); g_queueBuf[g_tail++] = val; g_tail = g_tail % QUEUE_BUFFER_SIZE; ReleaseSemaphore(g_hSemConsumer, 1, NULL); if (val == 99) break; ++val; } return 0; } DWORD __stdcall ThreadConsumer(LPVOID lpvParam) { int val; for (;;) { WaitForSingleObject(g_hSemConsumer, INFINITE); val = g_queueBuf[g_head++]; g_head = g_head % QUEUE_BUFFER_SIZE; ReleaseSemaphore(g_hSemProducer, 1, NULL); printf("%d ", val); fflush(stdout); if (val == 99) break; Sleep(rand() % 300); } putchar('\n'); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 3, Windows sistemlerinde de proseslerarası üretici-tüketici problemi benzer biçimde gerçekleştirilmektedir. Aşağıda buna ilişkin bir örnek verilmiştir. Bu örnekte de yine "producer" ve "consumer" isimli iki program vardır. Bu örneği daha da aşağıda verdiğimiz UNIX/Linux sistemlerindeki örneğin Windows karşılığı olarak düşünebilirsiniz. /* producer.c */ #include #include #include #define FILE_MAPPING_NAME "ProducerConsumerSharedMemoryName" #define PRODUCER_SEMAPHORE_NAME "ProducerSemaphoreName" #define CONSUMER_SEMAPHORE_NAME "ConsumerSemaphoreName" #define QUEUE_BUFFER_SIZE 10 void ExitSys(LPCSTR lpszMsg); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { HANDLE hFileMapping; HANDLE hSemProducer; HANDLE hSemConsumer; int val; struct SHARED_OBJECT *sharedObject; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((sharedObject = (struct SHARED_OBJECT *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); if ((hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, PRODUCER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); if ((hSemConsumer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, CONSUMER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); val = 0; for (;;) { Sleep(rand() % 300); WaitForSingleObject(hSemProducer, INFINITE); sharedObject->qbuf[sharedObject->tail++] = val; ReleaseSemaphore(hSemConsumer, 1, NULL); sharedObject->tail = sharedObject->tail % QUEUE_BUFFER_SIZE; if (val == 99) break; ++val; } CloseHandle(hSemProducer); CloseHandle(hSemConsumer); UnmapViewOfFile(sharedObject); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* consumer.c */ #include #include #include #define FILE_MAPPING_NAME "ProducerConsumerSharedMemoryName" #define PRODUCER_SEMAPHORE_NAME "ProducerSemaphoreName" #define CONSUMER_SEMAPHORE_NAME "ConsumerSemaphoreName" #define QUEUE_BUFFER_SIZE 10 void ExitSys(LPCSTR lpszMsg); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { HANDLE hFileMapping; HANDLE hSemProducer; HANDLE hSemConsumer; struct SHARED_OBJECT *sharedObject; int val; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((sharedObject = (struct SHARED_OBJECT *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); if ((hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, PRODUCER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); if ((hSemConsumer = CreateSemaphore(NULL, 0, QUEUE_BUFFER_SIZE, CONSUMER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); for (;;) { WaitForSingleObject(hSemConsumer, INFINITE); val = sharedObject->qbuf[sharedObject->head++]; ReleaseSemaphore(hSemProducer, 1, NULL); sharedObject->head = sharedObject->head % QUEUE_BUFFER_SIZE; printf("%d ", val); fflush(stdout); if (val == 99) break; Sleep(rand() % 300); } putchar('\n'); CloseHandle(hSemProducer); CloseHandle(hSemConsumer); UnmapViewOfFile(sharedObject); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Windows sistemlerinde "event senkronizasyon nesnesi" denilen önemli bir senkronizasyon nesnesi daha vardır. Bu senkronizasyon nesnesinin UNIX/Linux sistemlerinde tam bir karşılığı yoktur. Ancak UNIX/Linux sistemlerindeki "koşul değişkenleri (conditioned variable)" işlev olarak Windows'taki event senkronizasyon nesnelerinin görevini de yapabilmektedir. Windows'taki event senkronizasyon nesneleri belli bir thread akışının başka bir thread bir işlemi bitirene kadar bir noktada bekletilmesi amacıyla kullanılmaktadır. Örneğin bir thread global bir diziyi sıraya dizecek olsun. Ancak bu dizi başka bir thread tarafından oluşturulacak olsun. Thread'ler asenkron çalıştığına göre sıraya dizme işlemini yapacak thread sıraya dizmenin yapıldığı noktaya geldiğinide diğer thread'in diziyi oluşturmuş olması gerekmektedir. Eğer diğer thread diziyi oluşturmamışsa sıraya dizmeyi yapacak thread o noktada beklemeli diğer thread diziyi oluşturduktan sonra bu işe başlamalıdır. Thread'in bir noktada bekletilmesi semaphore nesneleriyle de kısmen yapılabilir. Örneğin sayacı 0 olan bir semaphore blokeye yol açacağı için thread'i bekletebilir. Diğer thread de semaphore sayacını artırarak onu oradan kurtarabilir. Ancak semaphore'lar event nesnelerinin sağladığı bazı durumları sağlayamamaktadır. Örneğin diğer thread bekleyen thread'in çalışmasına devam etmesini istediğinde artık aynı senkronizasyon nesnesinde thread'in beklememesi gerekebilir. Semaphore'lar bunu doğrudan sağlaymamaktadır. Ya da örneğin diğerini beklemekten kurtaran thread bunu birden fazla kez yaptığında semaphore'lar sayaçlı olduğu için diğer thread'in bekletilmesini sağlayamayabilirler. Windows sistemlerindeki event senkronizasyon nesnesi aşağıdaki adımlardan geçilerek kullanılmaktadır: -> Event senkronizasyon nesnesi CreateEvent API fonksiyonuyla yaratılır. Fonksiyonun prototipi şöyledir: HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCSTR lpName ); Fonksiyonun birinci parametresi kernel nesnesinin güvenlik bilgilerini belirtmektedir. Bu parametre NULL geçilebilir. Fonksiyonun ikinci parametresi event nesnesinin "manuel mi otomatik mi" olduğunu belirtmektedir. Eğer bu parametreye TRUE girilirse nesne manuel olur, FALSE girilirse otomatik olur. Event nesnesi manuel ise SetEvent yapıldığında nesne açık (signaled) durumda kalır. Nesne otomatik ise SetEvent yapıldığında geçiş sağlanınca nesne yeniden otomatik olarak kapalı duruma getirilir. Bunun anlamı izleyen paragraflarda daha iyi anlaşılacaktır. Fonksiyonun üçüncü parametresi nesnenin başlangıçta açık mı (signaled) yoksa kapalı mı (nonsignaled) olacağını belirtmektedir. Bu parametre TRUE girilirse nesne başlangıçta açık, FALSE girilirse kapalı durumda olur. Genellikle event nesnesi yaratılıken kapalı bir biçimde yaratılmaktadır. Fonksiyonun son parametresi nesnenin proseslerarası kullanılması için gerekli olan ismini belirtmektedir. Eğer nesne aynı prosesin thread'leri arasında kullanılacaksa bu parametre NULL adres biçiminde geçilebilir. Fonksiyon başarı durumunda event nesnesinin handle değerine, başarısızlık durumunda NULL adrese geri dönemktedir. Örneğin: HANDLE g_hEvent; ... if ((g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL) Exitrys("CreateEvent"); -> Event nesnesi yine diğer kernel senkronizasyon nesnelerinde olduğu gibi WaitForSingleObject ve WaitForMultipleObjects fonksiyonlarıyla yapılmaktadır. WaitForSingleObject fonksiyonu eğer event nesnesi kapalıysa blokeye yol açar, eğer nesne açık durumdaysa geçiş yapar. Yani akış WaitForSingleObject fonksiyonunda beklemeden hemen fonksiyondan çıkarak devam eder. -> Event nesnesini açık duruma geçirmek için SetEvent API fonksiyonu kullanılır. SetEvent fonksiyonunun prototipi şöyledir: BOOL SetEvent( HANDLE hEvent ); Fonksiyon event nesnesinin handle değerini parametre olarak alır ve nesneyi açık duruma geçirir. Artık nesne kapalı olduğundan dolayı bekleyen thread WaitForSingleObject fonksiyonundan çıkar. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Örneğin: SetEvent(g_hEvent); Nesne SetEvent fonksiyonu ile açık duruma geçirildiktin sonra WaitForSingleObject fonksiyonunda bekleyen thread blokeden çıkar. İşte eğer nesne otomatik ise bu durumda WaitForSİngleObject fonksiyonundan çıkılırken nesne otomatik olarak yeniden kapalı duruma geçmektedir. Eğer nesne manuel durumdaysa WaitForSingleObject fonksiyonu sonlandığında nesne hala açık durumda olmaya devam eder. Event nesnelerini kapalı duruma geçirmek için ResetEvent API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL ResetEvent( HANDLE hEvent ); Fonksiyon event nesnesinin handle değerini parametre olarak alır, başarı durumunda sıfır değerine başarısızlık durumunda sıfır dışı bir değere geri döner. WaitForSingleObject (ya da WaitForMultipleObjects) fonksiyonu ile birden fazla thread otomatik moddaki event nesnesini bekliyorsa, bu event nesnesi açık duruma geçtiğinde yalnızca tek bir thread'in blokesi çözülür. Çünkü event nesnesi otomatik durumda olduğu için WaitForSingleObject fonksiyonundan çıkılır çıkılmaz nesne atomik bir biçimde kapalı duruma geçirilecektir. Tabii eğer event nesnesi manuel modda ise SetEvent yapıldığında event nesnesini bekleyen thread'lerin blokesi çözülecektir. Aşağıda event senkronizasyon nesnesinin kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); HANDLE g_hEvent; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL)) == NULL) ExitSys("CreateEvent"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); printf("Press ENTER to set event...\n"); getchar(); SetEvent(g_hEvent); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { printf("Thread1 running...\n"); printf("waiting for the event object...\n"); WaitForSingleObject(g_hEvent, INFINITE); printf("Ok, thread1 resumes...\n"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { printf("Thread2 running...\n"); printf("waiting for the event object...\n"); WaitForSingleObject(g_hEvent, INFINITE); printf("Ok, thread2 resumes...\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Sık karşılaşılan diğer bir senkronizasyon nesnesi de "okuma-yazma kilitleri (reder-writer lock)" denilen nesnelerdir. Önce bu nesnelere neden gereksinim duyulduğunu açıklayalım. Bir kaynak üzerinde bir grup thread'in "okuma" bir grup thread'in de "yazma" eyleminde bulunduğunu düşünelim. Burada "okuma" eylemi demekle "kaynakta değişiklik yaratmayan", yazma eylemi demekle de "kaynakta değişiklik yaratan" eylemleri kastediyoruz. Örneğin global bir bağlı liste söz konusu olsun. Thread'lern bazıları bu bağlı liste üzerinde "arama" işlemi yaparken bazıları da bu bağlı liste üzerinde insert işlemi yapıyor olsunlar. Burada arama işlemi okuma eylemini, insert işlemi ise yazma eylemini temsil etmektedir. Birden fazla thread'in aynı anda bağlı listede arama yapmasının sakıncası yoktur. Ancak bir thread bağlı listede insert işlemi yaparken başka thread'lerin o işlem bitene kadar arama işlemi ya da insert işlemi yapmaması gerekir. Benzer biçimde bir thread bağlı liste üzerinde arama işlemi yaparken başka bir thread'in insert işlemi de yapmaması gerekir. İki thread için buradaki olası durumları şöyle ifade edebiliriz: Thread1 Thread2 Ne Yapılmalı? ------------------------------------------------------- Okuma Okuma İzin Verilmeli Okuma Yazma Senkronize Edilmeli Yazma Okuma Senkronize Edilmeli Yazma Yzma Senkronize Edilmeli O halde buradan çıkan sonuç şudur: -> Bir thread yazma yaparken yazma ve okuma yapacak thread'ler bu yazma olayının bitmesini beklemelidir. -> Bir thread okuma yaparken yazma yapacak thread'ler bu okuma işlminin bitmesini beklemelidir. -> Bir thread okuma yaparken, okuma yapacak diğer thread'ler eş zamanlı olarak bu işlemi yapabilirler. Okuma-yazma kilitleri Windows sistemlerinde de UNIX/Linux sistemlerinde de var olan senkronizasyon nesnelerindendir. Biz burada önce Windows sistemlerindeki okuma-yazma kilitlerini daha sonra UNIX/Linux sistemlerindeki okuma-yazma kilitlerini göreceğiz. Pekiyi reader-writer lock yerine yukarıdaki işlem mutex nesneleri ile yapılamaz mı? Eğer yukarıdaki problem mutex nesneleriyle çözülmeye çalışılırsa bu durumda mecburen okuma sırasında da kilitleme yapılır. Dolayısıyla gereksiz bir biçimde okuma yapmak isteyen birden fazla thread birbirlerini beklemek zorunda kalır. Aşağıdaki temsili (pseudo) kodu inceleyiniz: pthread_mutex_t g_mutex; ... read() { pthread_mutex_lock(&g_mutex); ... pthread_mutex_unlock(&g_mutex); } write() { pthread_mutex_lock(&g_mutex); ... pthread_mutex_unlock(&g_mutex); } Burada görüldüğü gibi bireden fazla read işlemi birlikte yapılamamaktadır. Reader-writer lock nesneleri aslında taban (base) senkronizasyon nesnelerinden değildir. Bunlar mutex ve koşul değişkenleri (condition variable) kullanılarak gerçekleştirilebilmektedir. Windows sistemlerinde okuma-yazma kilitleri şöyle kullanılmaktadır: -> Okuma-yazma kilit nesneleri SRWLOCK türüyle temsil edilmektedir. Önce global düzeyde SRWLOCK türünden bir nesne yaratılır. Bu nesneye InitializeSRWLock fonksiyonu ile ilkdeğerleri verilir. Fonksiyonun prototipi şöyledir: void InitializeSRWLock( PSRWLOCK SRWLock ); Fonksiyon SRWLOCK nesnesinin başlangıç adresini parametre olarak almaktadır. Örneğin: SWRLOCK g_srwLock; ... InitializeSRWLock(&g_srwLock); Dolaysıyla okuma amaçlı kritik kod şöyle oluşturulmaktadır: AcquireSRWLockShared(&g_srwLock); ... ... ... ReleaseSRWLockShared(&g_srwLock) Burada okuma amaçlı kilidin alınması AcquireSRWLockShared fonksiyonu ile yapılmaktadır. Fonksiyon eğer kilit başka bir thread tarafından yazma amaçlı olarak alındıysa blokeye yol açmaktadır. Ancak kilit başka bir thread tarafından okuma amaçlı alındıysa blokeye yol açmadan kritik koda geçişi sağlamaktadır. Okuma amaçlı kritik koddan çıkılırken kilit ReleaseSRWLockShared fonksiyonu ile serbest bırakılmalıdır. -> Yazma Amaçlı kritik kod da şöyle oluşturulmaktadır: AcquireSRWLockExclusive(&g_srwLock); ... ... ... ReleaseSRWLockExclusive(g_srwLock) Eğer kilit başka bir thread tarafından okuma amaçlı ya da yazma amaçlı olarak alınmışsa AcquireSRWLockExclusive fonksiyonu blokede bekler. Eğer kilit okuma ya da yazma amaçlı hiçbir thread tarafından alınmadıysa kritik koda geçiş yapılır. Yazma amaçlı kritik koddan çıkılırken kilit ReleaseSRWLockExclusive fonksiyonu ile serbest bırakılmalıdır. Fonksiyonların prototipileri şöyledir: void AcquireSRWLockShared( PSRWLOCK SRWLock ); void ReleaseSRWLockShared( PSRWLOCK SRWLock ); void AcquireSRWLockExclusive( PSRWLOCK SRWLock ); void ReleaseSRWLockExclusive( PSRWLOCK SRWLock ); Fonksiyonlar SRWLOCK nesnelerinin adreslerini parametre olarak almaktadır. -> Windows'ta reader-writer lock nesnelerinin serbest bırakılması gibi bir durum söz konusu değildir. Bunlar zaten bir kaynak tutmamaktadır. Aşağıda Windows sistemlerinde reader-write lock nesnelerinin kullanımına ilişkin bir örnek verilmiştir. Bu örnekte belli sayıda thread yaratılıp bunların rastgele read-write işlemleri yapması sağlanmıştır. Ekrandaki çıktı incelendiğinde write işlemi başladığında bitene kadar başka hiç read ya da write işleminin yapılmadığı görülecektir. Ancak read işlemleri birlikte yapılabilmektedir. Programın ekran çıktısının bir kısmının örnek bir görüntüsü şöyledir. ... Thread-1 thread begins writing... Thread-1 thread ends writing... Thread-3 thread begins reading... Thread-3 thread ends reading... Thread-6 thread begins writing... Thread-6 thread ends writing... Thread-2 thread begins reading... Thread-7 thread begins reading... Thread-7 thread ends reading... Thread-2 thread ends reading... ... Burada örneğin Thread-2 ve Thread-7'nin read işlemine birlikte girdiği görülmektedir. Ancak bir write işlemi başladığında o write işlemi bitene kadar read ya da write yapılamamaktadır. * Örnek 1, #include #include #include #include #include #define NTHREADS 10 DWORD __stdcall ThreadProc(LPVOID lpvParam); void Read(const char *pszThreadName); void Write(const char *pszThreadName); void ExitSys(LPCSTR lpszMsg); SRWLOCK g_srwLock; int main(void) { HANDLE hThreads[NTHREADS]; DWORD dwThreadIds[NTHREADS]; char szThreadName[32]; char *pszThreadName; srand(time(NULL)); InitializeSRWLock(&g_srwLock); for (int i = 0; i < NTHREADS; ++i) { sprintf(szThreadName, "Thread-%d", i + 1); if ((pszThreadName = strdup(szThreadName)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if ((hThreads[i] = CreateThread(NULL, 0, ThreadProc, pszThreadName, 0, &dwThreadIds[i])) == NULL) ExitSys("CreateThread"); } if (WaitForMultipleObjects(NTHREADS, hThreads, TRUE, INFINITE) == WAIT_FAILED) ExitSys("CreateThread"); for (int i = 0; i < NTHREADS; ++i) CloseHandle(hThreads[i]); return 0; } DWORD __stdcall ThreadProc(LPVOID lpvParam) { const char *pszThreadName = (LPCTSTR)lpvParam; for (int i = 0; i < 10; ++i) { Sleep(rand() % 100); if (rand() % 2 == 0) Read(pszThreadName); else Write(pszThreadName); } free(lpvParam); return 0; } void Read(const char *pszThreadName) { AcquireSRWLockShared(&g_srwLock); printf("%s thread begins reading...\n", pszThreadName); Sleep(rand() % 300); printf("%s thread ends reading...\n", pszThreadName); ReleaseSRWLockShared(&g_srwLock); } void Write(const char *pszThreadName) { AcquireSRWLockExclusive(&g_srwLock); printf("%s thread begins writing...\n", pszThreadName); Sleep(rand() % 300); printf("%s thread ends writing...\n", pszThreadName); ReleaseSRWLockExclusive(&g_srwLock); } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Daha önce de belirttiğimiz gibi belli bir zamandan sonra Windows sistemlerine de durum değişkenleri eklenmiştir. Windows sistemlerindeki durum değişkenleri mutex nesneleriyle değil CRITICAL_SECTION ve Read/Write Lock nesneleriyle kullanılabilmektedir. Genel kullanım biçimi UNIX/Linux sistemlerindekine çok benzemektedir. >> UNIX/Linux Sistemlerinde: UNIX/Linux sistemlerinde thread işlemleri başı "pthread_" ila başlatılan POSIX fonksiyonlarıyla yapılmaktadır. Thread işlemleri için kullanılan bu fonksiyonların oluşturduüu topluluğa "pthread kütüphanesi" de denilmektedir. Yukarıda da belirttiğimiz gibi bu kütüpahendeki tüm fonksiyonlar pthread_xxx biçiminde isimlendirilmiştir. Tüm thread fonksiyonlarının prototipleri isimli başlık dosyasının içerisindedir. UNIX/Linux sistemlerinde thread yaratmak için pthread_create isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); Fonksiyonun birinci parametresi thread'in id'sinin yerleştirileceği pthread_t türünden nesnenin adresini almaktadır. Bu sistemlerde thread'lerin yalnızca id'leri vardır. Thread işlemleri de id'lerle yapılmaktadır. pthread_t türü işletim sistemini yazanlar tarafından herhangi bir olarak typedef edilebilmektedir. Linux sistemlerinde bu tür unsigned long olarak typedef edilmiştir. Ancak başka sistemlerde bir yapı biçiminde de typedef edilmiş olabilir. Fonksiyonun ikinci parametresi yaratılacak thread'e ilişkin bazı özelliklerin belirtildiği thread özellik nesnesinin adresinin almaktadır. Programcı thread özelliklerini bu nesne ile oluşturup bu nesnenin adresini fonksiyona vermektedir. Ancak bu parametre NULL adres olarak da geçilebilir. Bu durumda thread default özelliklerle yaratılacaktır. Fonksiyonun üçüncü parametresi thread akışının başlatılacağı fonksiynun adresini belirtmektedir. Thread fonksiyonlarının geri dönüş değerlerinin "void *" türünden parametrelerinin de void * türünden olması gerekir. Örneğin: void *thread_proc(void *param) { .... } Fonksiyonun son parametresi thread fonksiyonuna geçirilecek olan argümanı belirtmektedir. Tabii eğer thread fonksiyonuna bir parametre geçirilmek istenmiyorsa bu parametre için NULL adres kullanılabilir. pthread_create fonksiyonu başarı durumunda 0 değerine geri dönmektedir. Fonksiyon başarısızlık durumunda errno değişkeninin set etmez. Başarısızlığı belirten errno değeri ile geri döner. Biz de bu değeri strerror fonksiyonu ile yazıya dönüştürüp yazdırabiliriz. Örneğin: pthread_t tid; int result; void *thread_proc(void *param); ... if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } ... Tabii işlemleri kısaltmak için hata durumunu ele alan bir fonksiyon da yazabiliriz: void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } pthread_create fonksiyonuyla yaratılmış olan thread'ler hemen çalışmaya başlamaktadır. Aşağıda UNIX/Linux sistemlerinde thread yaratmaya bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); for (int i = 0; i < 10; ++i) { printf("main thread: %d\n", i); sleep(1); } CloseHandle(hThread); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Bir thread herhangi bir noktada UNIX/Linux ve macOS sistemlerinde pthread_exit fonksiyonu ile sonlandırabilir. Bu fonksiyonları hangi thread akışı çağırırsa o thread sonlanmaktadır. exit fonksiyonunun tüm prosesi sonlandırdığına ancak pthread_exit fonksiyonlarının yalnızca tek bir thread'i sonlandırdığına dikkat ediniz. Therad'lerin de tıpkı prosesler gibi exit kodları vardır. UNIX/Linux ve macOS sistemlerinde ise void * bir değerle temsil edilmektedir. Thread'in exit kodları thread sonlandığında ilgili prosesler tarafından alınıp çeşitli amaçlarla kullanılabilmektedir. Ancak uygulamaların çoğunda bu exit kodunu kullanamaya gerek duyulmamaktadır. pthread_exit fonksiyonunun prototipi şöyledir: #include void pthread_exit(void *retval); Aşağıda UNIX/Linux ve macOS sistemlerinde thread'in pthread_exit fonksiyonu ile sonlandırılmasına bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); for (int i = 0; i < 10; ++i) { printf("main thread: %d\n", i); sleep(1); } CloseHandle(hThread); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); if (i == 5) pthread_exit(NULL); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde de bir thread başka bir thread'i zorla pthread_cancel fonksiyonu ile sonlandırabilir. Ancak bu fonksiyon Windows sistemlerindeki TerminateThread fonksiyonu gibi çalışmamaktadır. UNIX/Linux sistemlerinde bir thread'e pthread_cancel fonksiyonu uygulanırsa thread akışı ancak bazı POSIX fonksiyonlarında sonlandırılmaktadır. Dolayısıyla bu sistemlerde pthread_cancel fonksiyonu TerminateThread fonksiyonuna göre daha güvenlidir. pthread_cancel uygulandığında thread akışının sonlandırılabileceği POSIX fonksiyonlarına İngilizce "cancellation points" denilmektedir. Bu fonksiyonarın listesi POSIX standartrlarında belirtilmiştir. pthread_cancel fonksiyonunun prototipi şöyledir: #include int pthread_cancel(pthread_t thread); Fonksiyon parametre olarak sonlandırılacak thread'in id değerini almaktadır. Başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte ana thread'teki döngü 5 kez yibelendikten sonra diğer thread'i pthread_cancel fonksiyonu ile sonlandırmaktadır. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); for (int i = 0; i < 10; ++i) { printf("main thread: %d\n", i); if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_sys_errno("pthread_cancel", result); sleep(1); } return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); if (i == 5) pthread_exit(NULL); sleep(1); } 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 pthread_cancel fonksiyonu sonsuz döngüdeki thread'i sonlandıramayacaktır. Çünkü bu thread sonlandırma için gereken POSIX fonksiyonlarına (cancellation points) girmemiştir. Thread'in sonlandırılıp sonlandırılmadığını başka bir terminalden ps komutunda -T seçeneğini kullanarak görebilirsiniz. Komut şöyle uygulanabilir: $ ps -t /dev/pts/0 -T Burada /dev/pts/0 thread'li programın çalıştığı termşnali belirtmektedir. Bu terminal sizin denemenizde farklı olabilir. Bu terminali tty komutu ile öğrenebilirisniz. #include #include #include #include #include void *thread_proc(void *param); void exit_errno(const char *msg, int result); int main(int argc, char *argv[]) { pthread_t tid; int result; int i; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_errno("pthread_create", result); for (i = 0; i < 10; ++i) { if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_errno("pthread_cancel", result); printf("main thread: %d\n", i); sleep(1); } printf("press ENTER to exit..\n"); getchar(); return 0; } void *thread_proc(void *param) { for (;;) ; return NULL; } void exit_errno(const char *msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde thread'lerin sonlanmasının beklenmesi ve exit kodlarının alınması pthread_join fonksiyonuyla sağlanmaktadır. Yani fonksiyon hem bekleme yapıp hem de exit kodu almaktadır. Fonksyonunun prototipi şöyledir: #include int pthread_join(pthread_t thread, void **retval); Fonksiyonun birinci parametresi beklemecek thread'in id değerini belirtmektedir. İkinci parametre exit kodunun yerleştirileceği void göstericinin adresini belirtmektedir. Eğer ikinci parametre NULL geçilirse thread'in birmesi beklenir ancak exit kodu çağıran fonksiyona iletilmez. Fonksiyon baları durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte main fonksiyonu içerisinde (yani ana thread'te) bir thread yaratılmış ve thread'in sonlanması pthread_join fonksiyonuyla beklenmiştir. Bu örnekte exit kodu bir tamsayı olduğu halde sanki bir adresmiş gibi oluşturulmuştur. Yine exit kod göstericinin içerisinden alınarak int tüürne dönüştürülüp kullanılmıştır. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; void *exit_code; printf("main begins...\n"); if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); printf("main thread waits at pthread_join...\n"); if ((result = pthread_join(tid, &exit_code)) != 0) exit_sys_errno("pthread_join", result); printf("Exit code: %d\n", (int)exit_code); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); sleep(1); } return (void *)123; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde "zombie proses" kavramı vardı. Pekiyi zombie thread kavramı da var mıdır? Aslında bu sistemlerde işletim sistemi tıpkı proseslerde olduğu gibi therad'in ezit kodu alınmamışsa belli bir sistem kaynağını serbest bırakmadan bekletmektedir. Yani zombie thread kavramı zombie proses kavramı gibi bu sistemlerde söz konsudur. Ancak zombie thread'ler zombie prosesler kadar probleme yol açma potansiyelinde değildir. Fakat yine pthread_join fonksiyonuyla zombie thread'lerin oluşması engellenmelidir. Eğer programcı thread'in exit kodu ile ilgilenmiyorsa thread biter bitmez kaynakalrın boşaltılmasını işletim sisteminden isteyebilir. Bunun için pthread_detach fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_detach(pthread_t thread); Fonksiyon parametre olarak thread'in id değerini alır, başarı drumunda sıfır değerine, başarısızlık durumunda errno değerine geri döner. Tabii detach moda sokulmuş thread'ler artık pthread_join fonksiyonuyla beklenemezler. Eğer bunlar beklenmeye çaışılırsa ptherad_join fonksiyonu başarısız olmaktadır. Thread'ler konusunun en önemli alt konusu thread senkronizasyonudur. Bir grup thread birlikte bir işi gerçekleştirirken kimi zaman birbirlerini beklemesi, birbirleriyle koordineli bir biçimde çalışması gerekmektedir. İşte işletim sistemlerinde bunu sağlamaya yönelik mekanizmalara "thread senkronizasyonu" denilmektedir. Thread senkronizasyonunun önemi basit örnekle anlaşılabilir. Thread'lerin aynı global nesneleri kullandığını belirtmiştik. İki thread aynı global değişkeni bir döngü içerisinde bir milyon kere artırıyor olsun. Bu global değişkenin değerinin iki milyon olması beklenir. Ancak senkronizasyon problemi yüzünden muhtemelen iki milyon olamayacaktır. Aşağıda bu örnek Unix/Linux sistemleri için oluşturulmuştur. Programın farklı çalıştırılmalarında elde edilen bazı değerler şunlardır: $ ./sample 1140644 $ ./sample 1175870 $ ./sample 1900343 Aşağıda ilgili programa ilişkin örnek verilmiştir: * Örnek 1, #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); int g_count; int main(void) { 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); printf("%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_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Yukarıda da belirtitğimiz gibi UNIX/Linux sistemlerinde Windows'taki gibi bir CRITICAL_SECTION nesnesi yoktur. Kritik kod oluşturmak için mutex nesneleri kullanılmaktadır. UNIX/Linux sistemlerinde mutex nesneleri aynı prosesin thread'leri arasındaki senkronizasyon için tasarlandığından dolayı Windows'taki mutex nesnelerine göre daha hızlıdır. Ancak istenirse biraz zor olsa da UNIX/Linux sistemlerindeki mutex nesneleri prosesler arasında da kullanılabilir. UNIX/Linux sistemlerinde mutex nesneleri şu adımlardan geçilerek kullanılmaktadır: -> Henüz thread'ler yaratılmadan pthread_mutex_init fonksiyonu ile mutex nesnesi yaraılır. Fonksiyonun prototiği şöyledir: #include int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); Fonksiyonun birinci parametresi pthread_mutex_t türünden bir nesnenin adresini almaktadır. Fonksiyon bu nesneye bazı ilkdeğeri vermektedir. Buna mutex nesnesi diyebiliriz. pthread_mutex_t bir yapı biçiminde typedef edilmiştir. Programcı aynı prosesin thread'leri arasında senkronizasyon uygulamak için bu nesneyi global düzeyde ranımlamalıdır. Fonksiyonun ikinci parametresi yaparılacak mutex nesnesinin bazı özelliklerini belirlemek için kullanılmaktadır. Bu parametre NULL geçilebilir. Bu durumda mutex nesnesi default özelliklerle yaratılacaktır. Biz bu kursta mutex nesnelerinin özellikleri üzerinde durmayacağız. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_mutex_t g_mutex; ... if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); Mutex nesnelerine pthread_mutex_init yerine doğrudan PTHREAD_MUTEX_INITIALIZER makrosuyla da ilkdeğer verilebilmektedir. Örneğin: pthread_mutex_t g_mutex = PTHERAD_MUTEX_INIALIZER; -> Kritik kod pthread_muutex_lock ve pthread_mutex_unlock çağrıları arasında yerleştirilir: pthread_mutex_lock(&g_mutex); ... ... KRİTİK KOD ... pthread_mutex_unlock(&g_mutex); Bir thread pthread_mutex_lock fonksiyonuna girdiğinde eğer mutex'in sahipliği başka bir thread tarafından alınmışsa o thread sahipliği bırakana kadar fonksiyon blokede bekler. Eğer mutex nesnesinin sahipli alınmamışsa pthread_mutex_lock nensnenin sahipliğini alarak kritik koda giriş yapar. Nesnenin sahipliği pthread_mutex_unlock fnksiyonuyla bırakılmaktadır. Fonksiyonların prototipleri şöyledir: #include int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); Fonksiyonlar mutex nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönerler. -> Mutex ile çalışma bittikten sonra mutex nesnesi pthread_mutex_destroy fonksiyonuyla yok edilir. Fonksiyonun prototipi şöyledir: #include int pthread_mutex_destroy(pthread_mutex_t *mutex); Fonksiyon mutex nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri döner. Aslında pek çok kütüphanede bu fonksiyon bir şey yapmamaktadır. Ancak başka gerçekleştirimlerde bu fonksiyon birtakım kaynakları boşaltıyor olabilir. Aşağıda UNIX/Linux sistemlerinde mutex kullanımına bir örnek verilmiştir. Yine örnekte global bir sayaç değişkeni alınmıştır. İki thread de bu sayacı artırmaktadır. Ancak artırım sırasında mutex koruması uygulanmıştır. * Örnek 1, #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); pthread_mutex_t g_mutex; int g_count; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", 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_muex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); printf("%d\n", g_count); 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_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("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_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); } 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ğıda daha önce yapmış olduğumuz makine örneğininin UNIX/Linux sistemleriyle mutex nesneleriyle gerçekleştirimini veriyoruz. #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); void use_machine(const char *name); pthread_mutex_t g_mutex; int main(void) { pthread_t tid1, tid2; int result; srand(time(NULL)); if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", 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); return 0; } void use_machine(const char *name) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("-------------------\n"); printf("%s: 1. Step\n", name); usleep(rand() % 300000); printf("%s: 2. Step\n", name); usleep(rand() % 300000); printf("%s: 3. Step\n", name); usleep(rand() % 300000); printf("%s: 4. Step\n", name); usleep(rand() % 300000); printf("%s: 5. Step\n", name); usleep(rand() % 300000); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } void *thread_proc1(void *param) { int i; for (i = 0; i < 10; ++i) use_machine("Thread-1"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10; ++i) use_machine("Thread-2"); return NULL; } 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 default durumda özyinelemeli değildir. Bu sistemlerde mutex nesnelerin özyinelemeli yapmak için önce pthread_mutexattr_t türünden bir nesne oluşturulur. Sonra bu nesne pthread_mutexattr_init fonksiyonuyla ilkdeğerlenir. Sonra da pthread_mutexattr_settype fonksiyonu ile PTHREAD_MUTEX_RECURSIVE parametresi kullanılarak mutex özelliği özyinelemeli olarak set edilir. Nihayet bu attribute nesnesi pthread_mutex_create fonksiyonunda kullanılır. Sonunda da bu attribute nesnesi pthread_mutexattr_destroy fonksiyonuyla yok edilir.Buradaki fonksiyonların prototipleri şöyledir: #include int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); Bu fonksiyonların birinci parametreleri mutex attribute nesnesinin adresini almaktadır. Fonksiyonlar başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_mutex_t g_mutex; ... pthread_mutexattr_t mattr; ... if ((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if ((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0) exit_sys_errno("pthread_mutexattr_settype", result); if ((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if ((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); Mutex attribute nesnesinin yalnızca mutex nesnesinin yaratılması sırasında kullanıldığına dikkat ediniz. Tabii UNIX/Linux sistemlerinde de özyinelemeli mutex'lerin kilidini açmak için pthread_mutex_lock işlemi kadar pthread_mutex_unlock işleminin yapılması gerekmektedir. Aşağıda buna yönelik bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); void do_machine(const char *name); void use_machine(const char *name); pthread_mutex_t g_mutex; int main(void) { pthread_t tid1, tid2; int result; pthread_mutexattr_t mattr; srand(time(NULL)); if ((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if ((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0) exit_sys_errno("pthread_mutexattr_settype", result); if ((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if ((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", 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); return 0; } void do_machine(const char *name) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); use_machine(name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } void use_machine(const char *name) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("-------------------\n"); printf("%s: 1. Step\n", name); usleep(rand() % 300000); printf("%s: 2. Step\n", name); usleep(rand() % 300000); printf("%s: 3. Step\n", name); usleep(rand() % 300000); printf("%s: 4. Step\n", name); usleep(rand() % 300000); printf("%s: 5. Step\n", name); usleep(rand() % 300000); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } void *thread_proc1(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("Thread-1"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("Thread-2"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde mutex nesnesini prosesler arasında kullanabilmek için mutex nesnesinin paylaşılan bellek alanında yaratılması gerekir. (Yani bizim pthread_mutex_t türünden nesnseyi paylaşılan bellek alanında yaratmamız gerekir.) Böylece iki proses de aynı mutex nesnesini görecektir. Ancak ayrıca mutex'in prosesler arası kullanımını mümkün hale getirmek için mutex attribute nesnesinde pthread_mutexattr_setpshared fonksiyonu ile belirleme yapmak gerekir. Bu fonksiyonun prototipi şöyledir: int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); Fonksiyonun birinci parametresi mutex attribute nesnesinin adresini almaktadır. İkinci parametre için PTHREAD_PROCESS_SHARED değeri proseslerarası paylaşım yapılabileceğini, PTHREAD_PROCESS_PRIVATE değeri ise proseslerarası paylaşım yapılamayacağını belirtmektedir. Fonksiyon başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte prog1 programı önce paylaşılan bellek alanını ve mutex nesnesini oluşturmuştur. prog2 ise paylaşılan bellek alanındaki mutex nesnesini kullanmıştır. Paylaşılan bellek alanının başı aşağıdaki gibi bir yapı ile temsilk edilmiştir: struct SHARED_OBJECT { pthread_mutex_t mutex; long long count; }; Aşağıdaki örnekte önce prog1 programını çalıştırmalısınız. Çünkü bu örnekte paylaşılan bellek alanını ve mutex nesnesini prog1 programı yaratmaktadır. * Örnek 1, /* prog1.c */ #include #include #include #include #include #include #include #include void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { pthread_mutex_t mutex; long long count; }; int main(void) { int fdshm; int result; void *shmaddr; pthread_mutexattr_t mattr; struct SHARED_OBJECT *so; if ((fdshm = shm_open("/sample_shared_memory_name", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) exit_sys("ftruncate"); if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); so = (struct SHARED_OBJECT*)shmaddr; so->count = 0; 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(&so->mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if ((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); for (long long int i = 0; i < 1000000000; ++i) { if ((result = pthread_mutex_lock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++so->count; if ((result = pthread_mutex_unlock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } printf("Press ENTER to continue...\n"); getchar(); printf("%lld\n", so->count); if ((result = pthread_mutex_destroy(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink("/sample_shared_memory_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); } /* prog2.c */ #include #include #include #include #include #include #include #include void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { pthread_mutex_t mutex; long long int count; }; int main(void) { int fdshm; int result; void *shmaddr; struct SHARED_OBJECT *so; if ((fdshm = shm_open("/sample_shared_memory_name", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); so = (struct SHARED_OBJECT*)shmaddr; for (long long int i = 0; i < 1000000000; ++i) { if ((result = pthread_mutex_lock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++so->count; if ((result = pthread_mutex_unlock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); close(fdshm); 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 tıpkı paylaşılan bellek alanlarında olduğu gibi semaphore'lar için de iki ayrı arayüz fonksiyon grubu bulunmaktadır. Eskiden beri var olan klasik semaphore fonksiyonlarına "Sistem 5 semaphore'ları" denilmektedir. Bu semaphore fonksiyonlarının kullanımı oldukça zordur. 90'lı yılların ortalarında UNIX/Linux sistemlerinde "POSIX semaphore'ları" da denilen modern semaphore fonksiyonları eklenmiştir. Artık programcılar genellikle bu POSIX semaphore fonksiyonlarını tercih etmektedir. Tabii her iki fonksiyon grubu da aslında POSIX standartlarında yer almaktadır. Ancak "POSIX semaphore fonksiyonları" denildiğinde daha sonra tasarlanmış olan modern semaphore fonksiyonları kastedilmektedir. Klasik Sistem 5 semaphore'ları proseslerarası kullanım için tasarlanmıştır. Halbuki modern POSIX semaphore'ları hem aynı prosesin thread'leri arasında hem de farklı proseslerin thread'leri arasında kullanılabilmektedir. Biz kursumuzda modern POSIX semaphore fonksiyonlarını göreceğiz. Eski tipi "Sistem 5 Semaphore" fonksiyonları "UNIX/Linux Sistem Programlama" kurslarında ele alınmaktadır. POSIX semaphore fonksiyonları aşağıdaki adımlardan geçilerek kullanılmaktadır: -> POSIX semapore nesneleri sem_t türüyle temsil edilmektedir. sem_t bir yapıyı belirten typedef ismidir. Eğer semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa sem_t türünden global bir nesne tanımlanır ve bu nesneye sem_init fonksiyonu ile ilkdeğerleri verilir. sem_init fonksiyonunun prototipi şöyledir: #include int sem_init(sem_t *sem, int pshared, unsigned int value); Fonksiyonun birinci parametresi sem_t türünden nesnenin adresini almaktadır. Fonksiyonun ikinci parametresi bu nesnenin prosesler arasında paylaşılıp paylaşılmayacağını belirtmektedir. Eğer nesne prosesler arasında paylaşılacaksa bu parametreye sıfır dışı bir değer, paylaşılmayacaksa sıfır değeri geçilmelidir. Ancak zaten proseslerarası kullanım için başka bir fonksiyon bulunduğundan genellikle bu parametre sıfır geçilir. Fonlsiyonun üçüncü parametresi semaphore sayacının başlangıç değerini belirtmektedir. Yani bu değer kritik koda en fazla kaç akışın gireceğini belirtir. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyon errno değerini set etmektedir. (Diğer thread fonksiyonlarının errno değerini set etmediğini bizzat errno değerine geri döndüğünü anımsayınız.) Eğer semaphore nesnesi prosesler arasında kullanılacaksa bu durumda sem_t nesnesi sem_open fonksiyonu ile elde edilmelidir. Bu fonksiyon üzerinde daha sonra durulacaktır. -> Kritik kod sem_wait ve sem_post çağrıları arasına yerleştirilir: sem_wait(&g_sem); ... ... ... sem_post(&g_sem); Akış sem_wait fonksiyonuna geldiğinde eğer semaphore sayacı 0 ise sem_wait blokeye yol açar ve semaphore sayacı 0'dan büyük olana kadar bekler. Eğer semaphore sayacı 0'dan büyükse akış sem_wait fonksiyonundan geçer ancak semaphore sayacı 1 eksiltilir. sem_post fonksiyonu semaphore sayacını 1 artırmaktadır. Bu fonksiyonların prototipleri şöyledir: #include int sem_wait(sem_t *sem); int sem_post(sem_t *sem); Fonksiyonlar semaphore nesnelerinin adresini parametre olarak almakta, başarı durumunda sıfır değerine başarısızlık durumunda -1 değerine geri dönmektedirler. Yine başarısızlık durumunda errno değişkeni set edilmektedir. -> Semaphore nesnesinin kullanımı bittikten sonra nesne sem_destroy fonksiyonu ile yok edilmelidir. Fonksiyonun prototipi şöyledir: #include int sem_destroy(sem_t *sem); Fonksiyon semaphore nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Başarısızlık durumunda errno değişkeni set edilmektedir. Eğer semaphore nesnesi proseslerarası kullanım için sem_open fonksiyonuyla yaratılmışsa nesnenin yok edilmesi sem_close fonksiyonu ile yapılmalıdır. Aşağıda daha önce yapmış olduğumuz global değişkeninin iki thread tarafından artırılması binary POSIX semaphore'larıyla sağlanmıştır. * Örnek 1, #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 eno); sem_t g_sem; int g_count; int main(void) { pthread_t tid1, tid2; int result; if (sem_init(&g_sem, 0, 1) == -1) exit_sys("sem_init"); 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 (sem_destroy(&g_sem) == -1) exit_sys("sem_destroy"); printf("%d\n", g_count); return 0; } void *thread_proc1(void *param) { for (int i = 0; i < 1000000; ++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_proc2(void *param) { for (int i = 0; i < 1000000; ++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ğıda daha önce yapmış olduğumuz makine konumlandırma örneği POSIX semaphore nesneleriyle gerçekleştirilmiştir. #include #include #include #include #include #include #include void *thread_proc1(void* param); void *thread_proc2(void* param); void do_machine(const char* name); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem; int main(void) { int result; pthread_t tid1, tid2; srand(time(NULL)); if (sem_init(&g_sem, 0, 1) == -1) exit_sys("sem_init"); 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); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&g_sem); return 0; } void *thread_proc1(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("thread-1"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("thread-2"); return NULL; } void do_machine(const char *name) { if (sem_wait(&g_sem) == -1) exit_sys("sem_wait"); printf("---------------\n"); printf("1) %s\n", name); usleep(rand() % 300000); printf("2) %s\n", name); usleep(rand() % 300000); printf("3) %s\n", name); usleep(rand() % 300000); printf("4) %s\n", name); usleep(rand() % 300000); printf("5) %s\n", name); usleep(rand() % 300000); if (sem_post(&g_sem) == -1) exit_sys("sem_post"); } 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 semaphore nesneleriyle çözülmektedir. Problemin çözümü için iki semaphore nesnesi yatatılır. Semaphore'lardan biri üretici semaphore'u diğeri ise tüketici semaphore'u olur. Başlangıçta üretici semaphore'u 1'e tüketici semaphore'u ise 0'a kurulur: sem_t g_sem_producer; sem_t g_sem_consumer; ... sem_init(&g_sem_producer, 0, 1); sem_init(&g_sem_consumer, 0, 0); Üretici ve tüketici döngülerinin temsili biçimi şöyledir: ÜRETİCİ ------- for (;;) { sem_wait(&g_sem_producer); sem_post(&g_sem_consumer); } TÜKETİCİ -------- for (;;) { sem_wait(&g_sem_consumer); sem_post(&g_sem_producer); } Burada üretici semaphore'unun başlangıçtaki sayaç değeri 1 olduğu için üretici sem_wait fonksiyonundan geçip elde ettiği değeri paylaşılan alana bırakacaktır. Bu sırada tüketici semaphore'unun başlangıç değeri 0 olduğu için tüketici sem_wait fonksiyonunda bekleyecektir. Üretici thread'in tüketicinin semaphore sayacını, tüketici thread'in ise üreticinin semaphore sayacını 1 artırdığına dikkat ediniz. Böylece üretici tüketiciyi tüketici de üreticiyi sem_wait fonksiyonundan geçirmektedir. Bunu tbir tahteravalliye benzetebilirsiniz. * Örnek 1, Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin POSIX semaphore nesneleriyle çözümüne bir örnek verilmiştir. Bu örnekte üretici thread 0'dan 100'a kadar sayıları rastgele beklemelerle paylaşılan alana yerleştirmekte tüketici thread de bunları rastgele beklemelerle almaktadır. Örneği senkronizasyonu kaldırarak çalıştırıp nasıl bir durum oluştuğunu gözleyiniz. #include #include #include #include #include #include #include void *thread_proc_producer(void* param); void *thread_proc_consumer(void* param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_shared; int main(void) { int result; pthread_t tid1, tid2; srand(time(NULL)); 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"); if ((result = pthread_create(&tid1, NULL, thread_proc_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&g_sem_producer); sem_destroy(&g_sem_consumer); return 0; } void *thread_proc_producer(void *param) { int val; val = 0; for (;;) { usleep(rand() % 300000); 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 == 99) break; ++val; } return NULL; } void *thread_proc_consumer(void *param) { 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"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if (val == 99) break; } putchar('\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 2, Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin kuyruklu versiyonuna bir örnek verilmiştir. Burada kuyruğun eşzamanlı erişimler için senkronize edilmesine gerek yoktur. Çünkü aslında işleyiş dikkatle incelendiğinde iki thread'in aynı nesnelere eş zamanlı erişmediği görülecektir. #include #include #include #include #include #include #include #define QUEUE_BUFFER_SIZE 10 void *thread_proc_producer(void* param); void *thread_proc_consumer(void* param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_qbuf[QUEUE_BUFFER_SIZE]; int g_head; int g_tail; int main(void) { int result; pthread_t tid1, tid2; srand(time(NULL)); if (sem_init(&g_sem_producer, 0, QUEUE_BUFFER_SIZE) == -1) exit_sys("sem_init"); if (sem_init(&g_sem_consumer, 0, 0) == -1) exit_sys("sem_init"); if ((result = pthread_create(&tid1, NULL, thread_proc_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&g_sem_producer); sem_destroy(&g_sem_consumer); return 0; } void *thread_proc_producer(void *param) { int val; val = 0; for (;;) { usleep(rand() % 300000); if (sem_wait(&g_sem_producer) == -1) exit_sys("sem_wait"); g_qbuf[g_tail++] = val; g_tail = g_tail % QUEUE_BUFFER_SIZE; /* burası kritik kodun dışına alınabilir */ if (sem_post(&g_sem_consumer) == -1) exit_sys("sem_post"); if (val == 99) break; ++val; } return NULL; } void *thread_proc_consumer(void *param) { int val; for (;;) { if (sem_wait(&g_sem_consumer) == -1) exit_sys("sem_wait"); val = g_qbuf[g_head++]; g_head = g_head % QUEUE_BUFFER_SIZE; /* burası kritik kodun dışına alınabilir */ if (sem_post(&g_sem_producer) == -1) exit_sys("sem_post"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if (val == 99) break; } putchar('\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); } Konunun başında da belirttiğimiz gibi UNIX/Linux sistemlerinde semaphore nesneleri isim verilerek de prosesler arasında kullanılabilmektedir. Bunun için sem_open fonksiyonu ile iki prosesin de ortak bir isimde anlaşarak semaphore nesnesini açması gerekmektedir. sem_open fonksiyonunun prototipi şöyledir: #include sem_t *sem_open(const char *name, int oflag, ...); Fonksiyon ya iki parametreyle ya da dört parametreyle kullanılmaktadır. Eğer fonksiyon dört parametreyle kullanılacaksa parametrik yapı aşağıdaki gibi olmalıdır: sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); Fonksiyonun birinci parametresi prosesler arasında paylaşımı sağlamak için belirlenen ismi belirtmektedir. POSIX standartlarına göre bu ismin "kök dizindeki bir dosya ismi gibi" oluşturulması gerekmektedir. (Burada bir dosya yaratılmamaktadır. Kök dizindeki dosya yalnızca bir temsildir.) Fonksiyonun ikinci parametresi yaratım bayraklarını belirtmektedir. Bu ikinci parametre üç değerden biri olarak geçilmelidir: O_CREAT O_CREAT|O_EXCL 0 O_CREAT bayrağı yine "nesne yaratılmamışsa yarat, nesne yaratılmışsa yaratılanı kullan" anlamına gelmektedir. O_CREAT ile O_EXCL birlikte kullanılırsa bu durumda "nesne zaten yaratılmış ise" fonksiyon baaşarısız olmaktadır. Bu parametreye 0 değeri geçilirse "yaratılmış olan semaphore nesnesi" kullanılmak üzere açılmaktadır. Eğer fonksiyonun ikinci parametresinde O_CREAT kullanılmışsa ve nesne daha önce yaratılmamışsa bu durumda fonksiyon üçünüc ve dördüncü parametreleri kullanmaktadır. Diğer durumlarda bu parametreleri kullanmamaktadır. Başka bir deyişle üçüncü ve dördüncü parametreler nesne ilk kez yaratılırken kullanılmaktadır. Semaphore nesnelerinde açış bayraklarında O_RDONLY, O_WRONLY ve O_RDWR kullanılması POSIX standartlarında "belirsiz (unspecified)" bırakılmıştır. Ancak Linux sistemlerinde bu bayrakların etkileri vardır. Bu konu "UNIX Linux Sistem Programlama" kurslarında ele alınmaktadır. sem_open fonksiyonu ile yaratılan isimli POSIX semaphore nesnesi yine paylaşılan bellek alanlarında olduğu gibi sistem reboot edilene kadar yaşamaya devam etmektedir (kernel persistent). Bu nesneyi reboot etmeden silmek için sem_unlik fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int sem_unlink(const char *name); Fonksiyon isimli semaphore nesnesinin ismini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner. Bu fonksiyonları kullanırken "librt" ve "libpthread" kütüphanelerini link aşamasına dahil etmelisiniz. Derleme aşağıdaki gibi yapılmalıdr: gcc -Wall -o sample sample.c -lrt -lpthread Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin proseslerarası kullanımına bir örnek verilmiştir. Bu örnekte "producer" ve "consumer" isimli iki program vardır. producer programı üretici, consumer programı ise tüketici programdır. Programlarda bir tane paylaşılan bellek alanı iki tane de proseslerarası kullanılabilen isimli semaphore nesnesi oluşturulmuştur. Buradaki programların hangisinin önce çalıştırıdığının bir önemi yoktur. Çünkü ilk çalıştırılan program nesneleri yaratmakta sonra çalıştırılan program yaratılmış olanları açmaktadır. Her iki programda da nesneler yok edilmeye çalışılmıştır. Ancak bu nesneleri diğer program yok etmişse bu durum normal karşılanıp bir hata rapor edilmemiştir. Örneğimizde kuyruk sistemi paylaşılan bellek alanında oluşturulmuştur. Programları aşağıdaki gibi derleyebilirsiniz: $ gcc -o producer producer.c -lrt -lpthread $ gcc -o consumer consumer.c -lrt -lpthread Programları farklı terminallerden çalıştırmalısınız. * Örnek 1, /* producer.c */ #include #include #include #include #include #include #include #include #include #include #include #define SHARED_MEM_NAME "/sample_shared_memory_name" #define SEM_PRODUCER_NAME "/sample_mutex_producer_name" #define SEM_CONSUMER_NAME "/sample_mutex_consumer_name" #define QUEUE_BUFFER_SIZE 10 void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { int fdshm; sem_t *sem_producer; sem_t *sem_consumer; void *shmaddr; struct SHARED_OBJECT *so; int val; srand(time(NULL)); if ((fdshm = shm_open(SHARED_MEM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) { perror("ftruncate"); goto EXIT1; } if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) { perror("mmap"); goto EXIT2; } if ((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, QUEUE_BUFFER_SIZE)) == NULL) { perror("sem_open"); goto EXIT3; } if ((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0)) == NULL) { perror("sem_open"); goto EXIT4; } so = (struct SHARED_OBJECT *)shmaddr; so->head = 0; so->tail = 0; val = 0; for (;;) { usleep(rand() % 300000); if (sem_wait(sem_producer) == -1) { perror("sem_wait"); goto EXIT5; } so->qbuf[so->tail++] = val; if (sem_post(sem_consumer) == -1) { perror("sem_wait"); goto EXIT5; } so->tail = so->tail % QUEUE_BUFFER_SIZE; if (val == 99) break; ++val; } EXIT5: sem_destroy(sem_consumer); if (sem_unlink(SEM_CONSUMER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT4: sem_destroy(sem_producer); if (sem_unlink(SEM_PRODUCER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT3: if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); EXIT2: close(fdshm); EXIT1: if (shm_unlink(SHARED_MEM_NAME) == -1 && errno != ENOENT) 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.c */ #include #include #include #include #include #include #include #include #include #include #include #define SHARED_MEM_NAME "/sample_shared_memory_name" #define SEM_PRODUCER_NAME "/sample_mutex_producer_name" #define SEM_CONSUMER_NAME "/sample_mutex_consumer_name" #define QUEUE_BUFFER_SIZE 10 void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { int fdshm; sem_t *sem_producer; sem_t *sem_consumer; void *shmaddr; struct SHARED_OBJECT *so; int val; srand(time(NULL)); if ((fdshm = shm_open(SHARED_MEM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) { perror("ftruncate"); goto EXIT1; } if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) { perror("mmap"); goto EXIT2; } if ((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, QUEUE_BUFFER_SIZE)) == NULL) { perror("sem_open"); goto EXIT3; } if ((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0)) == NULL) { perror("sem_open"); goto EXIT4; } so = (struct SHARED_OBJECT *)shmaddr; for (;;) { if (sem_wait(sem_consumer) == -1) { perror("sem_wait"); goto EXIT5; } val = so->qbuf[so->head++]; if (sem_post(sem_producer) == -1) { perror("sem_wait"); goto EXIT5; } so->head = so->head % QUEUE_BUFFER_SIZE; usleep(rand() % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } putchar('\n'); EXIT5: sem_destroy(sem_consumer); if (sem_unlink(SEM_CONSUMER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT4: sem_destroy(sem_producer); if (sem_unlink(SEM_PRODUCER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT3: if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); EXIT2: close(fdshm); EXIT1: if (shm_unlink(SHARED_MEM_NAME) == -1 && errno != ENOENT) 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); } UNIX/Linux sistemlerinde de okuma-yazma kilitlerinin temel kullanım biçimi benzerdir. İşlemler şöyle yürütülmektedir: -> Okuma yazma kilitleri pthread_rwlock_t türüyle temsil edilmektedir. Programcı bu türden global bir nesne tanımlar. Nesneye ilkdeğer vermenin iki yolu vardır. Birincisi statik düzeyde PTHREAD_RWLOCK_INITIALIZER isimli maroyu kullanmaktadır. Örneğin: #include pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; İkincisi ise pthread_rwlock_init fonksiyonunu kullanmaktır. Örneğin: pthread_rwlock_t g_rwlock; ... int pthread_rwlock_init(&g_rwlock); pthread_rwlock_init fonksiyonunun prototipi şöyledir: int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); Fonksiyonun birinci parametresi ilkdeğer verilecek pthread_rwlock_t nesnesinin adresini almaktadır. İkinci parametre reader-writer lock nesnesinin özelliklerinin belirtildiği pthread_rwlockattr_t türünden nesnenin adresini almaktadır. İkinci parametre NULL geçilebilir. Bu durumda nesne default özelliklerle yaratılmaktadır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. -> Okuma amaçlı kritik kod şöyle oluşturulmaktadır: pthread_rwlock_rdlock(&g_rwlock); ... ... ... pthread_rwlock_unlock(&g_rwlock); Yazma amaçlı kritik kod ise şöyle oluşturulmaktadır: pthread_rwlock_wrlock(&g_rwlock); ... ... ... pthread_rwlock_unlock(&g_rwlock); Fonksiyonların prototipleri şöyledir: #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 pthread_rwlock_t türünden nesnenin adresini almaktadır. Genel çalışma biçimi yukarıda Windows sistemlerinde açıkladığımız gibidir. Burada Windows sistemlerinden farklı olarak unlock işlemi için tek fonksiyon bulunmaktadır. Yani kilit okuma amaçlı da alınsa, yazma amaçlı da alınsa kilidin bırakılması pthread_rwlock_unlock fonksiyonu ile yapılmaktadır. Fonksiyonlar başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. -> Programcı reader-writer lock nesnesi ile işini bitirsikten sonra pthread_rwlock_destroy fonksiyonu ile nesneyi yok edilmelidir. Fonksiyonun prototipi şöyledir: #include int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); Fonksiyon yine pthread_rwlock_t nesnesinin adresini parametre olarak alır, başarı durumunda sıfır değerine başrısızlık durumunda ise errno değerine geri döner. Aşağıda UNIX/Linux sistemlerinde reader-writer lock nesnelerinin kullanımına örnek verilmiştir. Bu örnek aslında yukarıda Windows sistemeri için yaptiğimız örneğin UNIX/linux versiyonu gibidir. * Örnek 1, #include #include #include #include #include #include #define NTHREADS 10 void *thread_proc(void *param); void read_proc(const char *ptname); void write_proc(const char *ptname); void exit_sys_errno(const char *msg, int eno); pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; int main(void) { pthread_t tids[NTHREADS]; char tname[32], *ptname; int result; srand(time(NULL)); for (int i = 0; i < NTHREADS; ++i) { sprintf(tname, "Thread-%d", i + 1); if ((ptname = strdup(tname)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if ((result = pthread_create(&tids[i], NULL, thread_proc, ptname)) != 0) exit_sys_errno("pthread_create", result); } for (int i = 0; i < NTHREADS; ++i) pthread_join(tids[i], NULL); if ((result = pthread_rwlock_destroy(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_destroy", result); return 0; } void *thread_proc(void *param) { const char *ptname = (const char *)param; for (int i = 0; i < 10; ++i) { usleep(rand() % 100000); if (rand() % 2 == 0) read_proc(ptname); else write_proc(ptname); } return NULL; } void read_proc(const char *ptname) { int result; if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("%s thread begins reading...\n", ptname); usleep(rand() % 300000); printf("%s thread ends reading...\n", ptname); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_unlock", result); } void write_proc(const char *ptname) { int result; if ((result = pthread_rwlock_wrlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_wrlock", result); printf("%s thread begins writing...\n", ptname); usleep(rand() % 300000); printf("%s thread ends writing...\n", ptname); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_unlock", result); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde ismine "koşul değişkenleri (condition variables)" denilen önemli bir senkronizasyon nesnesi vardır. Koşul değişkenleri eskiden Windows sistemlerinde yoktu. Ancak belli bir zamandan sonra Windows sistemlerine de sokuldu. Windows'ta event nesneleri olduğu için koşul değişkenleri yerine bu nesneler onların görevlerini tam olmasa da yaklaşık olarak yerine getirebiliyordu Ancak yukarıda da belirttiğimiz gibi daha sonraları Windows sistemlerine de koşul değişkenleri eklenmiştir. Koşul değişkenlerinin amacı bir koşul sağlanmadığı sürece thread'i blokede bekletmek, koşul sağlanınca thread'in blokesini çözmektir. Ancak koşul değişkenlerinin kullanılması ve çalışma biçiminin anlaşılması şimdiye kadar gördüğümüz senkronizasyon nesnelerine göre daha zordur. UNIX/Linux sistemlerinde koşul değişkenleri tipik olarak aşağıdaki adımlardan geçilerek kullanılmaktadır: -> UNIX/Linux sistemlerinde koşul değişkenleri tek başlarına kullanılmamaktadır. Mutex nesneleri ile birlikte kullanılmaktadır. Koşul değişkenleri pthread_cond_t türüyle temsil edilmektedir. Programcı koşul değişkenlerini yine global düzeyde bir mutex ile birlikte tanımlar. Koşul değişkenlerine ilkdeğer verilmesi statik düzeyde PTHREAD_COND_INITIALIZER makrosuyle yapılabilir. Örneğin: pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; Kooşul değişkenlerine ilkdeğer vermek için pthread_cond_init fonksiyonu da kullanılabilmektedir. Fonksiyonun prototipi şöyledir: #include int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); Fonksiyonun birinci parametresi pthread_cond_t nesnesini adresini, ikinci parametresi koşul değişkeninin özelliklerin belirtildiği pthread_condattr_t nesnesinin adresini almaktadır. Biz bu kursta koşul değişkenlerinin özelliklerini ele almayacağız. Bu ikinci parametreyi NULL olarak geçebilirsiniz. Bu durumda koşul değişkenleri default özelliklerle yaratılmaktadır. Zaten PTHREAD_COND_INITIALIZER makrosu da koşul değişkenlerini default özelliklerle yaratmaktadır. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda errno değerine geri dönmektedir. -> Koşul değişkenleri ile koşul sağlanana kadar bekleme yapmak için pthread_cond_wait isimli fonksiyon kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); Fonksiyonun birinci parametresi koşul değişkeninin adresini, ikinci parametresi mutex nesnesinin adresini almaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_cond_wait(&g_cond, &g_mutex); Ancak koşul sağlanana kadar bekleme böyle yapılmamaktadır. Belli bir kalıba uygun biçimde bu fonksiyon çağrılmalıdır. Koşul değişkenini bekleme kalıbı aşağıdaki gibidir: pthread_mutex_lock(&g_mutex); while (koşul_sağlanmadığı_sürece) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada dikkat edilmesi gereken durumlar şunlardır: pthread_cond_wait bir döngü içerisinde çağrılmalıdır. Döngünün aşağıdaki gibi oluşturulduğuna dikkat ediniz: while (koşul_sağlanmadığı_sürece) pthread_cond_wait(&g_cond, &g_mutex); Burada döngünün içerisindeki koşul "koşulun sağlanmadığı sürece uykuda kalınmasına ilişkin" koşuldur. Yani bu koşul sağlanmadığı zaman thread uyandırılacaktır. Örneğin: while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); Burada g_flag değişkeni sıfır olduğu sürece thread blokede bekleyecektir. Yani blokenin çözülmesi için g_flag değişkeninin 0'dan farkllı bir değerde olması gerekmektedir. Yukarıdaki kalıptaki diğer önemli bir nokta da koşul döngüsüne girmeden mutex nesnesinin sahipliğinin alındığı ve çıkışta da sahipliğin bırakıldığıdır. Yukarıdaki kalıbın ne anlama geldiği izleyen paragraflarda açıklanacaktır. -> Koşul değişkenini bekleyen thread'i uyandırmak için iki POSIX fonksiyonu bulunmaktadır: pthread_cond_signal ve pthread_cond_broadcast. pthread_cond_signal fonksiyonu koşul değişkeninde bekleyen tek bir thread'i uyandırma amacındadır. pthread_cond_broadcast ise koşul değişkeninde bekleyen tüm thread'leri uyandırma amacındadır. pthread_cond_signal fonksiyonu aslında tek bir thread'i uyandırmak istemektedir. Ancak işletim sistemlerinde bu durum her zaman mümkün olamadığı için istemeden birden fazla thread de uyandırılabilmektedir. Bu duruma "yanlış uyanma (spurious wakeup)" denilmektedir. Bu iki uyandırma biçimini şu örneğe benzetebiliriz: Bir odada 10 kişi uyuyor olsun. Siz de yalnızca Ahmet'i uyandırmak isteyin. Ancak yaptığınız gürültüden istemeden Mehmet ve Selami de uyanmış olabilir. İşte Mehmet ve Selami'nin uyanması "yanlış uyanma (spurious wakeup)" durumudur. Ancak siz bir megafonla bağırarak odadaki herkesi de uyandırabilirsiniz. Bu işlem pthread_cond_broadcast işlemine benzemektedir. Şimdi siz "yanlışlıkla uyandırma (spurious wakeup)" gibi bir sürecin nasıl olup da yüksek teknoloji gereken işletim sistemlerinde söz konusu olabildiğini merak edebilirsiniz. İşte işletim sistemlerinin etkin tasarımında mecburen böylesi durumlar oluşabilmektedir. pthread_cond_signal fonksiyonunun koşul değişkeninde bekleyen bir thread'i değil "en az bir thread'i" uyandırdığına dikkat ediniz. Ayrıca yanlış uyandırmanın yalnızca pthread_cond_signal yoluyla değil başka nedenlerden dolayı da oluşabileceğini belirtmek istiyoruz. Koşul değişkeninde bekleyen bir thread'in pthread_cond_signal ya da pthread_cond_broadcast fonksiyonu ile uyandırılması koşulun sağlandığı anlamına gelmemektedir. Uyanan thread'ler koşulu test etmeli, koşul sağlanmıyorsa yeniden uykuya dalacak biçimde hareket etmelidir. O halde pthread_cond_signal ya da pthread_cond_broadcast uygulanmadan önce bu fonksiyonları uygulayan ki tarafın koşulun sağlanmasına yol açması gerekir. Aşağıdaki beklemeye dikkat ediniz: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada diğer bir thread g_flag değişkenini değiştrmeden pthread_cond_signal ya da pthread_cond_broadcast uygularsa thread uyandırılıp pthread_cond_wait fonksiyonundan çıksa bile koşul sağlanmadığından dolayı yeniden uykuya dalacaktır. Tipik olarak programcı pthread_cond_signal ya da pthread_cond_broadcast uygulamadan önce mutex nesnesinin sahipliğini almalı koşulu kritik kod içerisinde oluşturmalı, bu fonksiyonları çağırdıktan sonra mutex nesnesinin sahipliğini bırakmalıdır. Yani tipik uygulama şöyle olmalıdır: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_cond_broadcast(&g_cond); pthread_mutex_unlock(&g_mutex); pthread_cond_signal ve pthread_cond_broadcast fonksiyonlarının prototipleri şöyledir: #include int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); Fonksiyonlar koşul değişken nesnesinin adresini paranetre olarak almakta, başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedirler. -> Koşul değişkeninin ve mutex nesnesinin kullanımı bittiğinde bunlar pthread_cond_destroy ve pthread_mutex_destroy fonksiyonuyla yok edilmelidir. pthread_cond_destroy fonksiyonunun prototipi şöyledir: #include int pthread_cond_destroy(pthread_cond_t *cond); Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. Aslında genel olarak pthread_cond_destroy ve pthread_mutex_destroy fonksiyonları pek çok kütüphanede bir şey yapmamaktadır. Ancak yine de bu fonksiyonların uyumluluk bakımından çağrılması gerekir. Aşağıda koşul değişkenlerinin kullanımına tipik bir örnek verilmiştir. Örneğimizde iki thread g_flag değişkeninin sıfır dışı bir değer olmasını beklemektedir. Ana thread'te g_flag değişkeni 1 değerine set edilip pthread_broadcast işlemi uygulandığında bekleyen iki thread de uyanmaktadır. * Örnek 1, #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); 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_flag = 0; int main(void) { 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); printf("press ENTER to continue...\n"); getchar(); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); g_flag = 1; if ((result = pthread_cond_broadcast(&g_cond)) != 0) exit_sys_errno("pthread_cond_broadcast", result); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lunock", 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_cond_destroy(&g_cond)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); return 0; } void *thread_proc1(void *param) { int result; printf("thread-1 is waiting at the condition variable..\n"); 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, &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); printf("ok, thread-1 resumes...\n"); return NULL; } void *thread_proc2(void *param) { int result; printf("thread-2 is waiting at the condition variable..\n"); 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, &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); printf("ok, thread-2 resumes...\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi de koşul değişkenlerini kullanmak için belirttiğimiz kalıpların ne anlam ifade ettiği üzerinde duracağız. Bekleme işleminin mutex nesnesinin sahipliği alınarak yapılması gerektiğini belirtmiştik. Örneğin: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada önce mutex nesnesinin sahipliğinin alındığını görüyorsunuz. pthread_cond_wait fonksiyonu uykuya dalmadan önce atomik bir biçimde mutex nesnesinin sahipliğini bırakmaktadır. Şimdi aşağıdaki koda bakalım: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_mutex_unlock(&g_mutex); pthread_cond_broadcast(&g_cond); Burada pthread_cond_broadcast uygulandığında bu koşul değişkeninde bekleyen tüm thread'ler uyanacaktır. İşte pthread_cond_wait fonksiyonu uykuya dalmadan önce mutex nesnesinin sahipliğini nasıl bırakıyorsa uyanırken de sahipliği alarak uyanmaktadır. Böylece thread uyandığında koşul eğer sağlanıyorsa döngüden çıkıp yine mutex'i sahipliği bırakacaktır. Şimdi yukarıdaki kodda g_flag değerinin zaten 1 olduğunu düşünelim. Bu durumda mutex nesnesinin sahipliği alınacak "g_flag == 1" olduğu için de döngüye girilmeyecektir. Böylece mutex nesnesinin sahipliği bırakılacaktır. Pekiyi neden yukarıdaki kalıpta bir döngü kullanılmıştır. Neden döngü yerine if deyimi kullanılmamıştır? İşte bunun nedeni "yalancı uyanma (spurious wakeup)" yüzündendir. Döngü yerine aşağıdaki gibi bir if deyiminin olduğunu varsayalım: pthread_mutex_lock(&g_mutex); if (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada yanlış uyandırma oluştuğunda eğer koşul sağlanmıyorsa thread'in yeniden uykuya dalması gerekir. Bunun için de bir döngünün kullanılması gerekmektedir. Şimdi programcının koşul değişkeninde bekleyen thread'lerden yalnızca birini çözmek istediğini düşünelim. Aşağıdaki kalıp bunu yapamayacaktır: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada diğer tarafın pthread_cond_signal uyguladığını kabul edelim. Bu durumda 2 thread'in de uyandırıldığını düşünelim. Bu iki thread de mutex nesnesinin sahipliğini almaya çalışacak ancak yalnızca biri alacaktır. Dolayısıyla diğeri bu mutex nesnesini bekleyecektir. Ancak mutex nesnesinin sahipliğini almış olan thread onu bıraktığında bu kez diğer thread mutex nesnesinin sahipliğini alarak koşul değişkeninden çıkacaktır. O halde programcının mutex kontrolünde yeniden koşul değişkenini eski haline getirmesi gerekmektedir. Örneğin: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); g_flag = 0; pthread_mutex_unlock(&g_mutex); pthread_cond_signal ya da pthread_cond_broadcast uygulayan tarafın koşul değişkenlerini aşağıdaki gibi mutex kontrolü içerisinde değiştirmesi aslında mutlak anlamda gerekmemektedir: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_cond_broadcast(&g_cond); pthread_mutex_unlock(&g_mutex); Ancak koşulların birden fazla ğeğişkene bağlı olduğu durumda önce mutex kontrolü ile kritik kod içerisinde koşulların ayarlanması gerekebilmektedir. C'deki aslında basit bir atama işlemi bile çok thread'li ortamda senkronizasyon sorunlarına yol açabilmektedir. UNIX/Linux sistemlerinde durum değişkenleri daha çok aynı prosesin thread'leri arasında kullanılıyor olsa da aslında prosesler arasında da kullanılabilmektedir. Tabii bu durumda koşul değişkeninin ve mutex nesnesinin paylaşılan bellek alanı üzerinde yaratılması ve yaratım sırasında da attribute nesneleri ile prosesler arası kullanım durumunun set edilmesi gerekmektedir. Bu işlem mutex nesnelerindekine benzer biçimde yapılmaktadır: pthread_cond_t g_cond; ... pthread_condattr_t attr; pthread_condattr_init(&attr); pthread_condattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_cond_init(&g_cond, &attr); pthread_condattr_destroy(&attr); Bir proses sonlandığında prosesin bütün thread'leri de sonlandırılmaktadır. Örneğin biz exit fonksiyonunu çağırdığımızda exit fonksiyonu tüm prosesi sonlandıracağı için bütün thread'lerde sonlanacaktır. C' de main fonksiyonu bittiğinde exit fonksiyonu ile prosesin sonlandırıldığını anımsayınız. Bu durumda main fonksiyonu biterse prosesin tüm thread'leri de sonlanacaktır. Thread'ler konusuna yeni başlayan kişiler bu hatayı çok sık yapmaktadır. Aşağıdaki örnekte main fonksiyonunda bir thread yaratılmış ancak main fonksiyonu hemen sonlanmıştır. Bu durumda yaratılmış olan thread de tüm program da sonlanacaktır. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); /* dikkat! main bitince yaratılmış olan thread de sonlandırılacaktır */ return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); if (i == 5) pthread_exit(NULL); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi de "thread" ler ile ilgili diğer noktalara değinelim: >> Bir thread'in çalışmasına ara verilmesi preemptive bir biçimde donanım kesmeleri yoluyla yapılmaktadır. İşlemci bir makine komutunu çalıştırırken kesme oluşsa bile makine komutunun çalışması bitmeden kesme konumuna geçmez. Yani iki komut arasında kesmeleri işleme sokmaktadır. Thread'ler dünyasında bir işlemin "atomik (atomic)" yapılması "kesilmeden yapılması" anlamına gelmektedir. O halde makine komutları atomiktir. Aşağıdaki işleme bakınız: ++g_count; Böyle bir kod genellikle derleyiciler tarafından tek bir makine komutuyla değil birkaç makine komutuyla yapılmaktadır. Örneğin Intel işlemcilerinde bu işlem için derleyici aşağıdaki gibi üç makine komutu üretebilmektedir: MOV EAX, g_count INC EAX MOV g_count, EAX İşte komutlar arasında thread'ler arası geçiş (context switch) oluşursa daha önce ele aldığımız senkronizasyon sorunları oluşacaktır. Aslında ++g_count işlemi bazı işlemcilerde tek bir makine komutuyla da yapılabilmektedir. Örneğin bu işlem Intel işlemcilerinde aşağıdaki gibi tek bir makine komutuyla da yapılabilmektedir: INC g_count Makine komutları atomik olduğuna göre bu işlem çok thread'li ortamlarda tamamen güvenli midir? İşte makine komutları arasında thread'ler arası geçiş oluşmamakla birlikte çok işlemcili ya da çok çekirdekli sistemlerde başka bir sorun da ortaya çıkmaktadır. İşlemcilerin bazılarında birden fazla çekirdek aynı bellek adresine erişirken oradaki bilgiyi bozabilmektedir. Bunun donanımsal olarak nasıl gerçekleştiği üzerinde burada durmayacağız. Böyle bir olasılık düşük olsa da maalesef yine de bulunmaktadır. Yine bir çekirdek bir bellek adresine bir değer yazarken diğer bir çekirdek oradan değer okumak istediğinde okuyan taraf önceki ya da sonraki değeri değil bozuk bir değeri de okuyabilmektedir. Bu durumda maalesef tek bir değişken bile iki thread arasında anlık kullanılacak olsa yine de bu işlemin senkronize edilmesi gerekmektedir. Ancak bir tek değişkene bir değer atamak için mutex gibi bir senkronizasyon nesnesini kullanmak performansı düşürmektedir. Aslında işlemcileri tasarlayanlar bunun çaresini de düşünmüşlerdir. Bir makine komutunun diğer çekirdekleri ya da işlemcileri o komutluk durdurarak yalnızca o komutu çalıştıran işlemci ya da çekirdeğin belleğe erişmesi sağlanabilmektedir. Örneğin Intel işlemcilerinde bir makine komutunun başına LOCK öneki getirilirse işlemci o komutu diğer işlemcileri durdurarak çalıştırmaktadır: LOCK INC g_count Bu işlem artık Intel işlemcilerinde çok çekirdekli sistemlerde de sorunu çözecektir. Tabii buradaki LOCK öneki performansı da düşürmektedir. Derleyicinin her komutta bu öneki kullanması kodun yavaş çalışmasına yol açacaktır. Pekyi biz C programcısı olarak derleyicinin böyle bir kod üretmesini nasıl sağlayabiliriz? Windows sistemlerinde diğer işlemcileri bir komutluk durdurarak bellek işlemi yapabilmek için tasarlanmış başı Interlocked ile başlayan InterlockedXXX biçiminde API fonksiyonları vardır. Tabii bu fonksiyonların içi sembolik makine dilinde yazılmıştır. Önemli birkaç Interlocked fonksiyonlarının prototiplerini aşağıda veriyoruz: LONG InterlockedIncrement(LONG volatile *Addend); LONG InterlockedAdd(LONG volatile *Addend, LONG Value); LONG InterlockedDecrement(LONG volatile *Addend); LONG InterlockedExchange(LONG volatile *Target, LONG Value); Aslında çok fazla Interlocked fonksiyon bulunmaktadır. Bunların listesi için aşağıdaki MSDN dokümanlarına başvurabilirsiniz: https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-functions Yukarıdaki fonksiyonlar long türden nesnelerin adreslerini parametre olarak almaktadır. Interlocked fonksiyonlarının genellikle tek bir makine komutuyla yapılabilecek işlemler için bulundurulduğuna dikkat ediniz. Aşağıdaki örnekte iki thread de aynı global değişkeni bir milyon kez artırmıştır. Ancak bu işlemi yukarıda görmüş olduğumuz InterllockedIncrement fonksiyonuyla yapmıştır. Bu fonksiyon bir nesneyi atomik olarak tek bir makine komutuyla diğer işlemcileri durdurarak artırmaktadır. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); long g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); printf("%ld\n", g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) InterlockedIncrement(&g_count); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) InterlockedIncrement(&g_count); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >> "built in" ve "__atomic_xxx" Fonksiyonlar : Derleyiciler tarafından doğrudan tanınan çağrılması için prototipe gereksinim duyulmayan ve genellikle derleyicilerin CALL makine komutu yerine inline fonksiyon gibi açım yaptığı özel fonksiyonlara "built-in" ya da "intrinsic" fonksiyon denilmektedir. C derleyicilerinin bir bölümü bazı standart C fonksiyonlarını ve kendilerine özgü bazı eklenti (extension) fonksiyonları bu biçimde ele almaktadır. Microsoft C derleyicilerinde, gcc ve clang derleyicilerinde "built-in" ya da "intrinsic" fonksiyonlar bulunmaktadır. Microsoft C derleyicilerinin "intrinsic" fonksiyon listesine aşağıdaki bağlantıdan ulaşabilirsiniz: https://learn.microsoft.com/en-us/cpp/intrinsics/compiler-intrinsics?view=msvc-170 gcc Derleyicilerinin built-in fonksiyonlarına da aşağıdaki bağlantılardan ulaşabilirsiniz: https://gcc.gnu.org/onlinedocs/gcc/Target-Builtins.html https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html gcc'de bazı built-in fonksiyonlar atomik işlemler için bulundurulmuştur. Bunları Microsoft'un InterlockedXXX fonksiyonlarına benzetebilirsiniz. Ancak gcc'de önceleri bu atomic built-in fonksiyonlar __sync_xxx biçiminde isimlendirilmişti. Sonra C++'a kütüphanesi eklenince C++ kütüphanesi de kullanabilsin diye bu eski __sync_xxx isimli fonksiyonlar yerine bunların "memory model" parametresi alan __atomic_xxx versiyonları oluşturuldu. Artık yeni programların __atomic_xxx biçimindeki bu yeni fonksiyonları kullanması tavsiye edilmektedir. Her iki fonksiyon grubunun dokümanlarına ilişkin bağlantıları aşağıda veriyoruz: https://gcc.gnu.org/onlinedocs/gcc-4.9.0/gcc/_005f_005fatomic-Builtins.html https://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html __atomic_xxx fonksiyonlarındaki "memory model" parametresi __ATOMIC_SEQ_CST olarak girilebilir. Biz burada bu memory model parametresinin ne anlam ifade ettiği üzerinde durmayacağız. Örneğin bir nesneyi atomic bir biçimde 1 artırmak isteyelim. Hiç mutex kullanmadan bunu gcc derleyicilerinde şöyle yapabiliriz: __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); Bir değişkene yalnızca değer atamak için aşağıdaki atomic fonksiyon kullanılabilir: void __atomic_store_n(type *ptr, type val, int memmodel); void __atomic_store(type *ptr, type *val, int memmodel); Benzer biçimde bir nesnenin içerisindeki değeri almak için de aşağıdaki atomic fonksiyonlar kullanılabilir: void __atomic_load_n (type *ptr, int memmodel); void __atomic_load (type *ptr, type *ret, int memmodel); Aşağıdaki örnekte iki thread aynı global değişkeni artırmaktadır. Biz daha önce bu örneği çeşitli senkronizasyon nesneleriyle zaten yapmıştık. Burada hiç senkronizasyon nesnesi kullanmadan gcc'nin atomic built-in fonksiyonlarıyla aynı şeyi yapıyoruz. * Örnek 1, /* atomic.c */ #include #include #include #include #define MAX_COUNT 100000000 void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); int g_count; int main(void) { 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); printf("%d\n", g_count); return 0; } void *thread_proc1(void *param) { for (int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void *thread_proc2(void *param) { for (int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } C11 ile birlikte C'ye isteğe bağlı olarak _Atomic biçiminde bir tür belirleyicisi ve niteleyicisi de eklenmiştir. _Atomic ile tanımlanan nesneler için derleyiciler atomik işlem yapacak biçimde kod üretmektedir. Tabii bu _Atomic belirleyicisi atomic yapılamayack işlemlerde bir etki göstermemektedir. Bir C11 derleyicisinin _Atomic belirleyicisini destekleyip desteklemediği __STDC_NO_ATOMICS__ makrosuna bakılarak belirlenebilir. Microsoft C derleyicileri henüz _Atomic belirleyicisini desteklememektedir. Ancak gcc derleyicileri belli bir sürümden sonra bu belirleyiciyi desteklemektedir. Ayrıca C11 ile birlikte başlık dosyasında aşağıdaki gibi tür makroları da bulundurulmuştur: atomic_bool _Atomic _Bool atomic_char _Atomic char atomic_schar _Atomic signed char atomic_uchar _Atomic unsigned char atomic_short _Atomic short atomic_ushort _Atomic unsigned short atomic_int _Atomic int atomic_uint _Atomic unsigned int atomic_long _Atomic long atomic_ulong _Atomic unsigned long atomic_llong _Atomic long long ... Bunların tam listesi için standartlara başvurabilirsiniz. Yine C++11 ile birlikte atomik işlem yapan fonksiyonlar da isteğe bağlı olarak standart hale getirilmiştir. Bunların bazılarını aşağıda veriyoruz: atomic_store atomic_store_explicit atomic_load atomic_load_explicit atomic_exchange atomic_exchange_explicit atomic_compare_exchange_strong atomic_compare_exchange_strong_explicit atomic_compare_exchange_weak atomic_compare_exchange_weak_explicit atomic_fetch_add atomic_fetch_add_explicit atomic_fetch_sub atomic_fetch_sub_explicit Aşağıdaki örnekte g_count global değişkeni _Atomic ile tanımlandığı için zaten ++g_count işlemi derleyici tarafından atomik bir biçimde yapılacaktır. Başka bir deyişle derleyici bu değişken işleme sokulurken onu tek bir makine komutuyla ve lock işlemi ile işleme sokmaktadır. * Örnek 1, #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); _Atomic int g_count; int main(void) { pthread_t tid1, tid2; int result; srand(time(NULL)); 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); printf("%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_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } C++'ta C'nin _Atomic belirleyicisi yoktur. (Genel olarak C++ Programlama Dili C Programlama Dilini kapsıyor olsa da bu kapsama mükemmel düzeyde değildir.) C++'ta atomik işlemler başlık dosyasında bildirilen std::atomic isimli sınıf şabşlonuyla yapılmaktadır. Bu sınıfın operatör fonksiyonları gerçekten çok işlemcili sistemler için atomik işlemler yapmaktadır. Dolayısıyla yukarıdaki işlemlerin C++'taki eşdeğerleri aşağıdaki gibi oluşturulabilir. * Örnek 1, #include #include #include #include using namespace std; void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); atomic g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); printf("%ld\n", (int)g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Daha önceden de belirttiğimiz gibi Windows sistemlerindeki ReadFile ve WriteFile API fonksiyonları, UNIX/Linux sistemlerindeki read ve write POSIX fonksiyonları işletim sisteminin sistem fonksiyonlarını doğrudan çağırmaktadır. İşletim sisteminin sistem fonksiyonaları da okuma ve yazma bağlamında sistem genelinde atomiktir. Bunun anlamı şudur: Biz iki farklı thread'ten ya da prosesten aynı dosyaya, dosyanın aynı yerine aynı anda yazma ya da okuma yapsak bile iç içe geçme asla oluşmaz. Bu durum kernel tarafından senkronize edilmektedir. Örneğin thread ya da proseslerden biri tam bir dosyanın belli bir offset'ine WriteFile ya da write fonksiyonuyla 100 byte yazıyor olsun. Tam o sırada da aynı offset'ten başka bir thread ya da proses 100 byte okuyaacak olsun. Bu durumda okuyan taraf ya 100 yazılmadan önceki 100 byte'ı okur ya da diğerinin yazdığı 100 byte'ı okur. Ancak asla yarısı eski 100 byte'tan yarısı diğerinin yazdığı 100 byte'tan oluşan bir 100 byte okumaz. Benzer biçimde iki thread ya da proses WriteFile ya da write ile aynı dosyanın aynı offset'ine yazıyor olsalar bile iç içe geçme oluşmamaktadır. Nihai durumda ya birinin ya da diğerinin tam yazdığı şeyler dosyada gözükür. Tabii birden fazla read ya da write işlemi sırasında bu işlemler iç içe geçebilir. Örneğin: thread-1 -------- write(...) write(...) thread-2 --------- write(...) write(...) Burada write çapırmaları atomşktir. Ancak thread'in iki write çağrısı arasına thread2'nin write çağrıları gerebilir. >> "Thread Safety" : Bir fonksiyon farklı thread'lerden aynı anda çağrıldığında herhangi bir sorun oluşmuyorsa o fonksiyon thread güvenlidir. Yani "thread güvenlilik (thread safety)" birden fazla thread tarafından aynı anda çağrılan fonksiyonların sorun çıkartmaması anlamına gelmektedir. Pekiyi thread güvenliliği bozan faktörler nelerdir? İşte eğer fonksiyon bir "statik data" kullanıyorsa (yani global bir nesneyi ya da static yerel bir nesneyi kullanıyorsa) o fonksiyon thread güvenli olamaz. Çünkü global değişkenlerin ve static yerel değişkenlerin toplamda tek bir kopyası vardır. Thread akışları aynı fonksiyonda ilerlerken aynı kopya üzerinde işlem yaptıklarından dolayı bir anomali oluşabilecektir. Örneğin: void foo(void) { static int a = 0; ++a; ... ++a ... ++a; ... } Burada bu foo fonksiyonu farklı thread'lerden aynı anda çağrıldığında bu farklı thread'ler static nesnenin aynı kopyasını kullanacağından dolayı bir bozulma oluşacaktır. static yerel nesnelerin stack'te değil "data" ya da "bss" alanlarında yaratıldığını anımsayınız. Aynı durum global nesne kullanan fonksiyonlar için de geçerlidir. Yukarıda da belirttiğimiz gibi "thread güvenlilik (thread safety)" bir fonksiyonun farklı thread'ler tarafından aynı anda çağrıldığında sorun oluşmaması anlamına gelmektedir. Thread güvenliliği bozan en faktör "static data" kullanımıdır. Yani programın static yerel ya da global nesneleri kullanmasıdır. Örneğin: char *myitoa(int a) { static char buf[32]; sprintf(buf, "%d", a); return buf; } Burada myitoa thread güvenli değildir. Çünkü bu fonksiyon birden fazla thread tarafından aynı anda (iç içe geçecek biçimde) çağrılırsa sorun oluşacaktır. Çünkü iki çağrının kullandığı yerel nesne aynı nesnedir. Pekiyi fonksiyonu "thread güvensiz" yapan şey nedir? Eğer bir fonksiyon statik veri kullanıyorsa (yani static yerel nesneler ya da global nesneler) fonksiyon thread güvenli olmaz. Ortak kaynakları kullanan fonksiyonlar thread güvenli değildir. Mademki fonksiyonu thread güvenli olmaktan çıkartan faktör fonksiyonun statik veri kullanmasıdır. O halde biz fonksiyonu static data kullanmaktan çıkartırsak thread güvenli hale getirmiş oluruz. Örneğin: char *myitoa_tsafe(char *buf, int a) { sprintf(str, "%d", a); return buf; } Burada myitoa fonksiyonu artık static yerel bir dizinin adresiyle geri dönmemektedir. Ona geçirilen adresle geri dönmektedir. Örnek bir kullanım şöyle olabilir: char s[100]; myitoa_tsafe(s, 123456) Burada myitoa_tsafe fonksiyonu artık farklı thread'ler tarafından aynı anda çağrılsa bile bir sorun oluşmayacaktır. (Tabii burada s dizisinin yerel bir dizi olduğunu kabul ediyoruz.) C'nin de bazı standartc fonksiyonları static yerel nesne ya da dizi kullanmaktadır. Dolayısıyla bu fonksiyonlar thread güvenli değildir. Örneğin localtime fonksiyonu thread güvenli değildir. local time fonksiyonunun parametrik yapısını anımsayınız: struct tm *localtime(const time_t *timep); Burada localtime fonksiyonu struct tm türünden static yerel bir nesnenin adresiyle geri dönmektedir. struct tm *localtime(const time_t *timep) { static struct tm lt; .... return < } Görüldüğü gibi fonksiyon aslında bize bir adres vermektedir, ancak bu adres static yerel bir nesnenin adresidir. Dolayısıyla onun tek bir kopyası vardır. Biz bu fonksiyonu farklı thread'lerden aynı anda çağırırsak aslında aynı nesne üzerinde işlem yapılacağı için bu nesnenin içeriği bozulacaktır. Benzer biçimde ctime fonksiyonu da static yerel bir dizinin adresiyle dönmektedir. Bu fonksiyon da thread güvenli değildir. Benzer biçimde rassal sayı üretmekte kullanılan rand fonksiyonu da global bir nesne (tohum nesnesi) kullanmaktadır. Dolayısıyla bu fonksiyon da thread güvenli değildir. Aşağıda C'de thread güvenli olmama potansiyelinde olan standart C fonksiyonlarının listesini veriyoruz: strtok strerror ctime gmtime localtime asctime rand srand tmpnam tempnam setlocal Pekiyi thread güvenli olmayan bir fonksiyonu nasıl thread güvenli hale getiririz? İlk yapılacak şey şüphesiz fonksiyonun static data (statik yerel ya da global nesneleri kastediyoruz) kullanmasını engellemektir. Static data kullanan programlar thread güvenli olamazlar. Bu durumda fonksiyonu thread güvenli hale getirmek için yapılacak şey onun static data kullanmasını engellemektir. Pekiyi C'nin standart kütüphanesindeki bazı fonksiyonlar thread güvenli doğaya sahip değilse ne yapabiliriz? Bu fonksiyonlar izleyen paragraflarda ele alacağımız gibi "thread'e özgü global alanlar" oluşturularak kütüphaleri yazanlar tarafından thread güvenli hale getirilebilirler. Çalıştığınız C derleyicisinin standart kütüphanesinin thread güvenli olup olmadığını öğrenmelisiniz. C'nin 2011 versiyonuna kadar (C11) C standartlarında thread lafı edilmemişti. Ancak ilk kez C11'de thread kavramı standartlara sokuldu ve isteğe bağlı (optional) bir thread kütüphanesi de standartla eklendir. Ancak bu standartlar da yukarıda belirttiğimiz fonksiyonların thread güvenli olup olmadığı konusunda bir açıklama yapmamıştır. Yani özet olarak bir C derleyicisindeki yukarıdakine benzer standart C fonksiyonları thread güvenli olabilir ya da olmayabilir. Microsoft 2005 yılına kadar standart C kütüphanesinin thread güvenli olan ve thread güvenli olmayan iki farklı versiyonunu bulundurmaktaydı. Ancak 2005'ten itibaren tek bir standart C kütüphanesi bulundurmaya başlamıştır. O da thread güvenli kütüphanedir. POSIX sistemlerinde static data kullanan sorunlu standart C fonksiyonlarının hepsinin xxxxx_r isimli thread güvenli bir versiyonu da bulundurulmuştur. Bu versiyonlar genel olarak static data kullanmak yerine ekstra bir parametre ile static data için kullanılacak alanı fonksiyonu çağırandan istemeketdir. Örneğin localtime ve localtime_r fonksiyonlarının prototipleri şöyledir: struct tm *localtime(const time_t *timep); struct tm *localtime_r(const time_t *timep, struct tm *result); localtime_r fonksiyonu static yerel nesne kullanmamakta parametre olarak alınan struct tm nesnesine yerleştirme yapmaktadır. Tabii fonksiyon parametresiyle aldığı nesnenin adresine geri dönmektedir. Örneğin, rand ve rand_r fonksiyonlarının prototipleri şöyledir: int rand(void); int rand_r(unsigned int *seedp); rand_r fonksiyonunun global tohum değişkeni kullanmadığına tohum değişkenini parametre olarak aldığına dikkat ediniz. Aşağıdaki örnekte ctime fonksiyonun thread güvenli versiyonu olan ctime_r fonksiyonu kullanılmıştır. * Örnek 1, #include #include #include #include #include void *thread_proc1(void* param); void *thread_proc2(void* param); void foo(void); void exit_errno(const char* msg, int result); int main(void) { int result; pthread_t tid1, tid2; if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_errno("pthread_create", result); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; } void foo(void) { time_t t; char s[64]; t = time(NULL); ctime_r(&t, s); printf("%s", s); } void *thread_proc1(void *param) { foo(); return NULL; } void *thread_proc2(void *param) { foo(); return NULL; } void exit_errno(const char *msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); } C'nin standart FILE stream işlemlerinin thread güvenli olup olmadığı hakkında da bir şey söylenmemiştir. Ancak hem Microsoft C kütüphanesi hem de GNU C kütüphanesi FILE stream işlemlerini thread güveli biçimde yapmaktadır. Yani iki ayrı thread global bir stream nesnesi üzerinde işlem yapıyorsa kullanılan tampon bu fonskiyonlar tarafından kritik kodlarla zaten korunmuş durumdadır. İç içe geçme durumu oluşmamaktadır. Benzer biçimde C++'ın kürüphanesi de aynı dosya üzerinde işlem yapılırken bile thread güvenlidir. Ancak standartlar bunu garanti etmemktedir. Aşağıda Windows sistemlerinde iki thread eş zamanlı olarak aynı dosyaya yazma yapaktadır. Dosya tamponunun her açılan dosya için ayrı bir biçimde oluşturulduğunu anımsayınız. Eğer Windows'ta dosya nesneleri thread güvenli olmasaydı bu yazma işlemlerinde iç içe geçme olabilirdi. Buradaki örnekte "test.txt" dosyasının içini inceleyiniz. İç içe geçmenin olmadığını göreceksiniz. Örneğin: ... thread-1 Thread-1 Thread-1 Thread-1 Thread-1 Thread-2 Thread-2 Thread-2 Thread-2 Thread-2 Thread-2 Thread-1 Thread-1 Thread-1 Thread-1 Thread-1 Thread-1 Thread-1 ... Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); FILE *g_f; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_f = fopen("test.txt", "w")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) fprintf(g_f, "Thread-1\n"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) fprintf(g_f, "Thread-2\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Yukarıda da belirttiğimiz gibi GNU C Kütüphanesindeki stream işlemleri de thread güvenlidir. Yukarıda vermiş olduğumuz örneğin UNIX/Linux karşılığı da aşağıdaki gibidir. #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); FILE *g_f; int main(void) { pthread_t tid1, tid2; int result; if ((g_f = fopen("test.txt", "w")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } 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; for (i = 0; i < 10000; ++i) fprintf(g_f, "Thread-1\n"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10000; ++i) fprintf(g_f, "Thread-2\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, C++'ın iostream kütüphanesi de genel olarak Microsoft ve UNIX/Linux sistemlerinde thread güvenli yazılmıştır. Ancak standartlar bağlamında bunun için bir garanti bulunmamaktadır. #include #include #include #include using namespace std; fstream g_f; void thread_proc1() { for (int i = 0; i < 1000; ++i) g_f << "thread-1 running...\n"; } void thread_proc2() { for (int i = 0; i < 1000; ++i) g_f << "Thread-2 running...\n"; } int main(void) { g_f.open("test.txt", ios::out); thread t1(thread_proc1); thread t2(thread_proc2); t1.join(); t2.join(); g_f.close(); return 0; } >> C++'da C++11 ile birlikte standart bir thread kütüphanesi oluşturulmuştur. Tabii aslında bu kütüphane Windows sistemlerinde Windows API fonksiyonlarını, UNIX/Linux sistemlerinde POSIX'in pthread fonksiyonalrını kullanmaktadır. Yalnızca arayüz standart hale getirilmiştir. Aşağıda C++'da thread yaratımına bir örnek verilmiştir. * Örnek 1, #include #include #include using namespace std; class Sample { public: Sample(const char *name) : m_name(name) {} void operator()() { cout << m_name + "\n"; } private: string m_name; }; void thread_proc1() { cout << "thread-1 running...\n"; } void thread_proc2() { cout << "thread-2 running...\n";; } int main(void) { Sample s1("thread-1"), s2("thread-2"); thread t1(s1); thread t2(s2); thread t3(thread_proc1); thread t4(thread_proc1); t1.join(); t2.join(); t3.join(); t4.join(); return 0; } Genel olarak C++'ın sınıfları aynı nesne üzerinde okuma konusunda thread güvenli ancak yazma konusunda thread güvenli değildir. Fakat farklı nesneler üzerinde okuma ve yazma işlemleri thread güvenlidir. Bunların standartlarda bu anlamda thread güvenli versionları yoktur. >> Thread'e özgü global değişkenler olabilir mi? Yani örneğin iki thread akışı bir global değişkeni kullanırken aslında bunlar farklı global değişkenler olabilir mi? İşte işletim sistemleri uzun süredir bunu sağlamak için mekanizmalar bulundurmaktadır. Windows sistemlerinde thread'e özgü global değişken oluşturma mekanizmasına "Thread Local Storage (TLS)", UNIX/Linux sistemlerinde ise "Thread Specific Data (TSD)" denilmektedir. Hatta C11 ile birlikte C standartlarına _Thread_local isimli, C++11 ile C++ standartlarına thread_local isimli yer belirleyici (storage class specifier) anahtar sözcükler sokulmuştur. Yani artık C ve C++'ta işletim sisteminin API fonksiyonlarını ve POSIX fonksiyonlarını kullanmadan da thread'e özgü global dğeişkenler kullanılabilmektedir. Örneğin: _Thread_local int g_tl; Burada aslında bir g_tl nesnesi yoktur. Yaratılmış olan ve yaratılacak olan tüm thread'lerin birbirinden ayrı birer g_tl nesneleri vardır. Aşağıdaki örnekte C11 ile C'e sokulan _Thread_local belirleyicisi kullanılarak bir global değişken oluşturulmuştur. Bu global değişkenin her thread için ayrı bir kopyası bulunmaktadır. Aşağıdaki örnekte bu durum görülmektedir. * Örnek 1, #include #include #include #include void Foo(const char *str); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); _Thread_local int g_tl; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); Foo("Main Thread"); /* Main Thread: 0 */ return 0; } void Foo(const char *str) { printf("%s: %d\n", str, g_tl); } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { g_tl = 10; Sleep(5000); Foo("Thread1"); /* THread: 10 */ return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { g_tl = 20; Foo("Thread2"); /* Thread2: 20 */ return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Yukarıda da belirttiğimiz gibi aslında thread'e özgü global alanlar işletim sisteminin desteğiyle oluşturulmaktadır. C ve C++ derleyicileri de arka planda aslında izleyen paragraflarda ele alacağımız gibi işletim sisteminin bu mekanizmalarını kullanarak thread'e özgü global değişkenler oluşturabilmektedir. >> "Thread Specific Data" : >>> Windows Sistemlerinde : Windows'ta her thread için işletim sistemi TLS adı altında slotlardan oluşan bir dizi ayırmaktadır. Slotların indeksleri DWORD türüyle temsil edilmektedir. Belli bir slot indeksi her thread'te ayrı bir alan belirtir. Windows'ta TLS (Thread Local Storage) kullanımı şu adımlardan geçilerek sağlanmaktadır: -> Önce henüz thread'ler yaratılmadan TlsAlloc API fonksiyonuyla bir TLS slotu yaratılır. TlsAlloc fonksiyonunun prototipi şöyledir: DWORD TlsAlloc(void); Fonksiyon parametre almamaktadır. Geri dönüş değeri olarak TLS alanında bir slot indeksi vermektedir. Bu slot indeksi yaratılmış olan ve yaratılacak olan her thread için kullanılabilir ve farklı bir alan belirtmektedir. Programcı tipik olarak bu slot indeksini global nesnede saklar. Fonksiyon başarısızlık durumunda TLS_OUT_OF_INDEXES özel değerine geri dönmektedir. Windows sistemlerinde toplam 1086 slot bulunmaktadır. Örneğin: DWORD g_slot; ... if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES) ExitSys("TlsAlloc"); -> Artık her thread aynı slot indeksini kullanarak slota bir adres yerleştirebilir. Slot indeksleri aynı olsa da aslında bu slotlar farklı thread'lerde olduğu için her thread kendi slotunu kullanıyor olacaktır. Slota adres yerleştiren TlsSetValue fonksiyonunun prototipi şöyledir: BOOL TlsSetValue( DWORD dwTlsIndex, LPVOID lpTlsValue ); Fonksiyonun birinci parametresi TLS slot indeksini, ikinci parametresi ise slota yerleştirilecek adres belirtmektedir. Fonksiyon başarı durumunda sıfır dışı bir değer başarısızlık durumunda 0 değerine geri dönmektedir. Biz aslında malloc ile tahsisat yapıp slota tahsis ettiğimiz alanın adresini yerleştirebiliriz. Böylece tek bir slot ile istediğimiz kadar thread'e özgü global nesne oluşturmuş oluruz. Örneğin: struct THREAD_LOCAL_DATA { int a; int b; ibt c; }; struct THREAD_LOCAL_DATA *tld; ... if ((tld = (struct THREAD_LOCAL_DATA *)malloc(sizeof(struct THREAD_LOCAL_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } tld->a = 10; tld->b = 20; tld->c = 30; if (!TlsSetValue(g_slot, tld)) ExitSys("TlsSetValue"); -> Değer TLS slotundan geri almak için TLSGetValue fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: LPVOID TlsGetValue( DWORD dwTlsIndex ); Fonsiyon TLS slot indeksini parametre olarak alır ve oraya set edilmiş adresi geri dönüş değeri olarak verir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. Tabii yaratılmamış bir slot adresi geçmedikten sonra fonksiyonun başarısız olma olasılığı yoktur. Dolayısyla fonksiyonun geri dönüş değerini kontrol etmeyebilirsiniz. Örneğin: struct THREAD_LOCAL_DATA *tld; ... if ((tld = (struct THREAD_LOCAL_DATA *)TlsGetValue(g_slot)) == NULL) ExitSys("TlsGetValue"); Ancak gerçekten solota NULL adres de yerleştirilebilir. Bu durumda fonksiyon NULL adrese geri döndüğünde GetLastError değerine bakılmalıdır. Eğer GetLastError ERROR_SUCESS değerindeyse işlem başarılıdır ve gerçekten slota yerleştirilen NULL adres geri alınmıştır. Ancak GetLastError başka bir değer geri döndürürse işlemin başarısız olduğu sonucu çıkartılmalıdır. -> Slot kullanımı bittikten sonra slotun TlsFree fonksiyonu ile serbest hale getirilmesi gerekir. TlsFree fonksiyonunun prototipi şöyledir: BOOL TlsFree( DWORD dwTlsIndex ); Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Tabii her şey doğrıu yapılmışsa fonksiyonun başarısız olma olsaılığı da yoktur. Aşağıda bir örnek verilmiştir. * Örnek 1, #include #include #include #include void Foo(const char *str); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); DWORD g_slot; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES) ExitSys("TlsAlloc"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); Foo("Main Thread"); TlsFree(g_slot); return 0; } void Foo(const char *str) { int *pVal; if ((pVal = (int *)TlsGetValue(g_slot)) == NULL && GetLastError() != ERROR_SUCCESS) ExitSys("TlsGetValue"); printf("%d\n", (int)pVal); } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { if (!TlsSetValue(g_slot, (LPVOID)10)) /* g_tl = 10 */ ExitSys("TlsSetValue"); Sleep(5000); Foo("Thread1"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { if (!TlsSetValue(g_slot, (LPVOID)20)) /* g_tl = 20 */ ExitSys("TlsSetValue"); Foo("Thread2"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Her ne kadar TlsSetValue ile slota yalnızca bir adres yerleştirebiliyorsak da aslında dinamik tahsisat yapıp bu alanın adresini slota yerleştirebiliriz. Böylece istediğimiz kadar çok bilgiyi thread'e özgü biçimde kullanabiliriz. Örneğin: struct THREAD_DATA { int a; int b; int c; }; ... struct THREAD_DATA *td; td = (struct THREAD_DATA *)malloc(sizeof(struct THREAD_DATA)); td->a = 10; td->b = 20; td->c = 30; TlsSetValue(g_slot, td); Aşağıda bu yöntemle birden fazla bilginin slota yerleştirilmesine ilişkin bir örnek verilmiştir. * Örnek 1, #include #include #include #include void Foo(const char *str); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); DWORD g_slot; struct THREAD_DATA { int a; int b; int c; }; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES) ExitSys("TlsAlloc"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); Foo("Main Thread"); TlsFree(g_slot); return 0; } void Foo(const char *str) { struct THREAD_DATA* td; if ((td = (struct THREAD_DATA *)TlsGetValue(g_slot)) == NULL && GetLastError() != ERROR_SUCCESS) ExitSys("TlsGetValue"); printf("%s: %d, %d, %d\n", str, td->a, td->b, td->c); } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { struct THREAD_DATA* td; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 10; td->b = 20; td->c = 30; if (!TlsSetValue(g_slot, td)) ExitSys("TlsSetValue"); Sleep(5000); Foo("Thread1"); free(td); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { struct THREAD_DATA* td; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 100; td->b = 200; td->c = 300; if (!TlsSetValue(g_slot, td)) ExitSys("TlsSetValue"); Foo("Thread2"); free(td); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Standart C kütüphanesi fonksiyonların parametrik yapılarını değiştirmeden nasıl thread güvenli hale getirilir? İşte böyle bir faaliyette bazı fonksiyonlar statik nesneleri kullanmak yerine TLS içerisindeki nesneleri kullanmalıdır. Aşağıdaki örnekte bu işlemin mantıksal olarak nasıl yapılabileceği gösterilmiştir. Bu örnekte thread güvenli kütüphane için programın başında ve sonunda, her thread'in başında ve sonunda init ve exit fonksiyonlarının çağrılması gerekmektedir. Aslında bu çağrılar programcıdan gizlenebilir. Şöyle ki: Eğer dinamik kütüphane söz konusuysa dinamik kütüphanenin bazı fonksiyonları bu tür durumlarda otomatik olarak çağrılmaktadır. İşte programcı da bu kodları aslında kendi kütüphanesinin içerine alabilmektedir. Ancak statik kütüphanelerde böyle bir callback mekanizması yoktur. Microsoft bunun için standart C kütüphanesinin statik versiyonunu yazarken mecburen sarma thread fonksiyonları kullanmıştır. _beginthreadex fonksiyonu statik standart C kütüphanesi kullanılacaksa thread yaratmak için tercih edilmelidir. Bu fonksiyon aslında arka planda CreateThread API fonksiyonunu zaten çağırmaktadır. Ancak thread a yaratılmadan önce ve thread sonlanırken aşağıdaki kodda bulunan init ve exit gibi fonksiyonlar bu sarma fonksiyon tarafından çağrılır. Benzer biçimde eğer Microsoft sistemlerinde statik kütüphane kullanılıyorsa thread sonlanırken _endthreadex fonksiyonu çağrılmalıdır. Dinamik kütüphanelerde bu sarma fonksiyonların kullanılmasına gerek yoktur. Ayrıca gcc derleyicilerinde standart C fonksiyonlarının thread güvenli olmadığını bunların thread güvenli versiyonlarının farklı parametrik yapılarla statik nesne kullanmayack biçimde xxxxx_r ismiyle yazıldığını anımsayınız. * Örnek 1, /* cstdlib.h */ #ifndef CSTDLIB_H_ #define CSTDLIB_H_ #include /* Type Definitions */ typedef struct tagCSTDLIB_STATIC_DATA { char *strtok_pos; size_t rand_next; } CSTDLIB_STATIC_DATA; /* Function Prototypes */ int init_cstdlib(void); void exit_cstdlib(void); int init_csdlib_thread(void); int exit_csdlib_thread(void); char *csd_strtok(char *str, const char *delim); void csd_srand(size_t seed); int csd_rand(void); #endif /* cstdlib.c */ #include #include #include #include "cstdlib.h" static DWORD g_cstdSlot; int init_cstdlib(void) { CSTDLIB_STATIC_DATA *clib; if ((g_cstdSlot = TlsAlloc()) == TLS_OUT_OF_INDEXES) return 0; return 1; } int init_csdlib_thread(void) { CSTDLIB_STATIC_DATA *clib; if ((clib = (CSTDLIB_STATIC_DATA *)malloc(sizeof(CSTDLIB_STATIC_DATA))) == NULL) { TlsFree(g_cstdSlot); return 0; } clib->rand_next = 1; if (!TlsSetValue(g_cstdSlot, clib)) return 0; } int exit_csdlib_thread(void) { CSTDLIB_STATIC_DATA *clib; if ((clib = (CSTDLIB_STATIC_DATA *)malloc(sizeof(CSTDLIB_STATIC_DATA))) == NULL) { TlsFree(g_cstdSlot); return 0; } free(clib); } void exit_cstdlib(void) { TlsFree(g_cstdSlot); } char *csd_strtok(char *str, const char *delim) { CSTDLIB_STATIC_DATA *clib; char *beg; if ((clib = TlsGetValue(g_cstdSlot)) == NULL) return NULL; if (str != NULL) clib->strtok_pos = str; while (*clib->strtok_pos != '\0' && strchr(delim, *clib->strtok_pos) != NULL) ++clib->strtok_pos; if (*clib->strtok_pos == '\0') return NULL; beg = clib->strtok_pos; while (*clib->strtok_pos != '\0' && strchr(delim, *clib->strtok_pos) == NULL) ++clib->strtok_pos; if (*clib->strtok_pos != '\0') *clib->strtok_pos++ = '\0'; return beg; } void csd_srand(size_t seed) { CSTDLIB_STATIC_DATA *clib; if ((clib = TlsGetValue(g_cstdSlot)) == NULL) return NULL; clib->rand_next = seed; } int csd_rand(void) { CSTDLIB_STATIC_DATA *clib; if ((clib = TlsGetValue(g_cstdSlot)) == NULL) return NULL; clib->rand_next = clib->rand_next * 1103515245 + 12345; return (unsigned int)(clib->rand_next / 65536) % 32768; } /* Sample.c */ #include #include #include #include #include "cstdlib.h" void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if (!init_cstdlib()) { fprintf(stderr, "cannot initialize CSD Standard C Library!..\n"); exit(EXIT_FAILURE); } if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); exit_cstdlib(); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { char s[] = "ali, veli, selami, ayşe, fatma"; char *str; int i, val; if (!init_csdlib_thread()) { fprintf(stderr, "CSDLib initialization failed!..\n"); exit(EXIT_FAILURE); } for (str = csd_strtok(s, ", "); str != NULL; str = csd_strtok(NULL, ", ")) printf("threadproc1 --> %s\n", str); for (i = 0; i < 10; ++i) { val = csd_rand(); printf("threadproc1_rand --> %d\n", val); } exit_csdlib_thread(); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { char s[] = "adana, izmir, balikesir, muğla"; char *str; int i, val; if (!init_csdlib_thread()) { fprintf(stderr, "CSDLib initialization failed!..\n"); exit(EXIT_FAILURE); } for (str = csd_strtok(s, ", "); str != NULL; str = csd_strtok(NULL, ", ")) printf("threadproc2 --> %s\n", str); for (i = 0; i < 10; ++i) { val = csd_rand(); printf("threadproc2_rand --> %d\n", val); } exit_csdlib_thread(); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> UNIX/Linux Sistemlerinde : UNIX/Linux sistemlerinde thread'e özgü statik alanlara "Thread Specific Data (TSD)" denilmektedir. Genel kullanım biçimi Windows sistemlerindekilere oldukça benzemektedir. Sırasıyla şunlar yapılmalıdır: -> Önce pthread_key_create fonksiyonu ile TSD için bir slot yaratılır. pthread_key_create fonksiyonunu işlev olarak TlsAlloc fonksiyonuna benzetebiliriz. Fonksiyonun prototipi şöyledir: #include int pthread_key_create(pthread_key_t *key, void (*destructor)(void *)); Fonksiyonun birinci parametresi slot anahtarının yerleştirileceği pthread_key_t türünden nesnenin adresini almaktadır. Fonksiyon slot anahtar bilgisini bu nesnenin içerisine yerleştirmektedir. Tipik olarak programcı bu türden global bir nesne tanımlar. Onun adresini fonksiyona geçirir. Fonksiyonun ikinci parametresi thread sonlandığında çağrılacak callback fonksiyonun adresini almaktadır. Bu parametre NULL geçilebilir. Bu durumda sonlanma sırasında fonksiyon çağrılmaz. Fonksiyon başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir. -> TSD slotuna yerleştirme yapmak için pthread_setspecific slottan değer almak için ise pthread_getspecific fonksiyonları kullanılmaktadır. Fonksiyonların prototipleri şöyledir: #include int pthread_setspecific(pthread_key_t key, const void *value); void *pthread_getspecific(pthread_key_t key); pthread_setspecific fonksiyonunun birinci parametresi slotu belirten pthread_key_t türünden nesneyi ikinci parametresi ise o slota yerleştirilecek adresi almaktadır. pthread_getspecific fonksiyonu da slota yerleştirilmiş olan adres değerini vermektedir.Bu fonksiyonları Windows'taki TlsSetValue ve TlsGetValue fonksiyonlarına benzetebiliriz. pthread_setspecific fonksiyonu başarı durumunda 0 değerine başarısızlık durumunda ise errno değerine geri dönmektedir. pthread_getspecific fonksiyonu başarısızlık durumunda NULL adrese geri dönmektedir. -> Kullanım bittikten sonra elde edilen slot pthread_key_delete fonksiyonu ile isteme iade edilir. Fonksiyonun prototipi şöyledir: #include int pthread_key_delete(pthread_key_t key); Fonksiyon slota ilişkin pthread_key_t türünden nesneyi parametre olarak alır. Başarı durumunda sıfır değerine, başarısızlık durumunda errno değerine geri döner. pthread_key_create fonksiyonunun ikinci parametresine geçirilecek destructor fonksiyonunun parametrik yapısı şöyle olmalıdır: void destructor(void *value); Fonksiyona TSD alanına yerleştirilmiş olan slottaki adres parametre olarak geirilmektedir. Tipik olarak programcılar bu fonksiyonda dinamik tahsis edilen alanları free hale getirirler. Aşağıda UNIX/Linux sistemlerinde TSD kullanımına ilişkin bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include void foo(const char *str); void destructor(void *value); void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); struct THREAD_DATA { int a; int b; int c; }; pthread_key_t g_tsdkey; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_key_create(&g_tsdkey, destructor)) != 0) exit_sys_errno("pthread_key_create", 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_key_delete(g_tsdkey)) != 0) exit_sys_errno("pthread_key_delete", result); return 0; } void foo(const char *str) { struct THREAD_DATA *td; if ((td = (struct THREAD_DATA *)pthread_getspecific(g_tsdkey)) == NULL) { fprintf(stderr, "cannot get key value!..\n"); exit(EXIT_FAILURE); } printf("%s: %d, %d, %d\n", str, td->a, td->b, td->c); } void destructor(void *value) { free(value); } void *thread_proc1(void *param) { struct THREAD_DATA* td; int result; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 10; td->b = 20; td->c = 30; if ((result = pthread_setspecific(g_tsdkey, td)) != 0) exit_sys_errno("pthread_setspecific", result); sleep(5); foo("Thread1"); return NULL; } void *thread_proc2(void *param) { struct THREAD_DATA* td; int result; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 100; td->b = 200; td->c = 300; if ((result = pthread_setspecific(g_tsdkey, td)) != 0) exit_sys_errno("pthread_setspecific", result); foo("Thread2"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >> İşletim Sistemlerinde Kullanılan Çizelgeleme Teknikleri : Bugün işletim sistemlerinde en çok kullanılan çizelgeleme tekniği öncelik dereceleri dikkate alınarak uygulanan "döngüsel çizelgeleme (round robin scheduling)" denilen tekniktir. Döngüsel çizelgelemede her thread sırasıyla çalıştırılmaktadır. Ancak bu sistemin işletim sistemine özgü ayrıntıları vardır. Döngüsel çizelgelemede işletim sistemi thread'lere öncelik derecelerini de dikkate alarak quanta süreleri atar. Quanta süresini dolduran thread'lerin quanta süreleri yeniden doldurulmadan önce çalışma kuyruğundaki tüm thread'lerin quanta sürelerinin bitmesi beklenmektedir. Tabii bir thread çalışmaya başladığında bloke olabileceği için quanta süresini sonuna kadar kullanamayabilmektedir. İşletim sistemi her thread'in quanta süresinden harcağı zamanı tutmaktadır. Örneğin işletim sisteminin her thread'e 60 ms. quanta süresi verdiğini düşünelim. O anda da çalışma kuyruğunda(run queue) aşağıdaki thread'ler bulunuyor olsun: T1 (40 ms) T2 (0 ms) ==> quanta süresi bitti T3 (10 ms) T4 (25 ms) Burada T2 thread'inin ona atanan 60 ms'lik quanta süresinin dolduğunu varsayalım. İşletim sistemi T2 thread'ine hemen 60 ms quanta doldurmayacaktır. Önce çalışma kuyruğundaki tüm thread'lerin kullandığı quanta sürelerinin 0'a düşmesini bekleyecek sonra hepsini birlikte 60 ms ile dolduracaktır. Pekiyi bu durumda bloke olup bekleme kuyruğunda beklemekte olan thread'lerin quantda süreleri ne olacaktır. Örneğin T5 thread'inin bloke beklediğini ve kalan quanta süresinin 30 ms. olduğunu varsayalım. Çalışma kuyruğundaki tüm thread'lerin quanta süreleri sıfıra geldiğinde onlara 60 ms. quanta doldurulurken blokede bekleyen threa'2lere doldurma yapılacak mıdır? İşte işletim sistemleri genellikle blokede bekleyen thread'lere de doldurma yapmaktadır. Çünkü onlar uyandıklarında diğerleriyle eşit hakka sahip olacak biçimde daha fazla çalıştırılmalıdırlar. Ancak tabii blokede uzun süre bekleyen thread'lerin quanta'larının sürekli doldurulması da anomali yaratabilmektedir. Bunun için işletim sistemleri blokede bekleyen thread'ler için maksimum bir üst sınır da belirleyebilmektedir. Pekiyi çalışma kuyruğundaki tüm thread'lerin quanta süreleri 0'a düştüğünde tüm thread'ler aynı quanta değeri ile mi doldurulmaktadır? İşte bu konuda işletim sistemleri arasında farklılıklar vardır. Windows'ta genel olarak aynı quanta süresi doldurulmaktadır. Ancak Linux sistemlerinde SCHED_OTHER çizelgeleme politikasında her thread'e o threadin önceliği ile orantılı quanta süreleri doldurulmaktadır. Yani Linux sistemlerinde bir thread'in önceliğini artırdığımızda onun diğerlerine göre daha fazla quanta süresi kullanmaısnı sağlayabiliriz. Thread'li işletim sistemlerinde thread'lerin zaman paylaşımlı biçimde çizelgelendiğini belirtmiştik. Ancak bu sistemlerde sisteme bağlı olarak thread'lere öncelik dereceleri atanabilmektedir. Böylece yksek öncelikli thread'lerin CPU'dan daha fazla zaman alması sağlanabilmektedir. Thread öncelikleri konusu yukarıda da belirttiğimiz gibi işletim sistemine özgü farklılıklar içermektedir. Biz burada önce Windows sistemlerindeki durumu sonra da UNIX/Linux sistemlerindeki durumu kabaca ele alacağız. >>> Windows Sistemlerinde : Windows sistemlerinin kullandığı thread izelgeeleme algoritmasına "öncelik sınıflarıyla döngüsel çizelgeleme (priority class based round robin scheduling )" denilmektedir. Windows'ta her thread'in [0, 31] arasında bir öncelik derecesi vardır. Şimdiye kadar yaratmış olduğumuz thread'lerin default öncelik dereceleri 8'dir. Windows'ta thread'ler şöyle çizelgelenmektedir: Önce en yüksek önceliğe sahip thread'lerden bir grup oluşturulur. Sanki diğer thread'ler hiç yokmuş gibi yalnızca bu thread'ler zaman paylaşımlı biçimde çalıştırılır. Bu thread'ler sonlanırsa ya da bloke olursa bu kez daha düşük öncelikli en yüksek grup aynı biçimde kendi aralarında izelgelenir. Bu yöntemde düşük öncelikli thread'lerin çalışabilmesi için yüksek öncelikli thread'lerin sonlanması ya da bloke olması gerekmektedir. Yüksek öncelikli bir thread'in blokesi çözüldüğünde işletim sistemi düşük öncelikli thread'in çalışmasına ara vererek yeniden bu yüksek öncelikli thread grubunu çizelgelemektedir. Örneğin sistemde aşağıdaki öncelikte thread'ler bulunuyor olsun: T1 -> 12 T2 -> 12 T3 -> 10 T4 -> 9 T5 -> 9 T6 -> 8 T7 -> 8 T8 -> 8 Burada sistem sanki diğer thread'ler yokmuş gibi T1 ve T2 thread'lerini kendi aralarında zaman paylaşımlı olarak çalıştırmaktadır. Bu thread'ler sonlanırsa ya da bloke olursa T3 thread'i çalışma imkanı bulacaktır. T3 thread'i de sonlanırsa ya da bloke olursa bu durumda T4 thread'i, o da sonlanırsa ya da bloke olursa T5, T6, T7 ve T8 thread'leri kendi aralarında zaman paylaşımlı olarak çizelgelenecektir. Görüldüğü gibi bu sistemde düşük öncelikli bir thread'in çalışanilmesi için yüksek öncelikli thread'lerin bloke olması ya da sonlanması gerekmektedir. Windows'un çizelgeleme algoritması kabaca yukarıdaki gibi olsa da aslında oludukça ayrıntılar içermektedir. Çok işlemcili ya da çok çekirdekli sistemlerde eeğer boşta işlemci ya da çekirdek varsa işletim sistemi daha düşük öncelikli sınıfları da bu işlemci ya da çekirdeklere atayabilmektedir. Windows'ta bir thread'in [0, 31] arasındaki öncelik derecesi iki değerin toplamıyla elde edilmektedir: Prosesin Öncelik Sınıfı + Thread'in Göreli Öncelik Derecesi. Prosesin öncelik sınıfı bir taban değer belirtir. Thread'in göreli önceliği de bu taban değere toplanır. Prosesin öncelik sınıflarının taban değerleri şöyledir: NORMAL_PRIORITY_CLASS (8 default) ABOVE_NORMAL_PRIORITY_CLASS (10) BELOW_NORMAL_PRIORITY_CLASS (6) HIGH_PRIORITY_CLASS (13) REALTIME_PRIORITY_CLASS (24) IDLE_PRIORITY_CLASS (4) Thread'in göreli öncelik dereceleri de şöyledir: THREAD_PRIORITY_NORMAL (0 default) THREAD_PRIORITY_IDLE (Öncelik sınıfına göre değişmektedir) THREAD_PRIORITY_LOWEST (-2) THREAD_PRIORITY_BELOW_NORMAL (-1) THREAD_PRIORITY_ABOVE_NORMAL (+1) THREAD_PRIORITY_HIGHEST (+2) THREAD_PRIORITY_TIME_CRITICAL (Öncelik sınıfına göre değişmektedir) Herhangi bir öncelik oluşturmak için bu ikşi ayarlamadın da yapıması gerekir. Tabii aynı değeri veren birden fazla kombinasyon olabilir. Default durumda prosesin öncelik sınıfı NORMAL_PRIORITY_CLASS, thread'in göreli önceliği THREAD_PRIORITY_NORMAL biçimdedir. Bu durumda thread'in öncelik derecesi 8 + 0 = 8 biçimindedir. Örneğin thread önceliğini 15 yapmak isteyelim. Bunun birkaç yolu olabilir: IDLE_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL BELOW_NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL ABOVE_NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL HIGH_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL HIGH_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL Thread'in önceliğini 31'e çekmek isteyelim. Bunun tek yolu şöyleidr: REALTIME_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL Thread önceliklerinin belirlenmesine ilişkin Microsoft dokğmanlarına aşağıdaki bağlantıdan erişebilirsiniz: https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities Prosesin öncelik sınıfı GetPriorityClass API fonksiyonuyla alınıp SetPriorityClass API fınksiyonuyla set edilebilir. Fonksiyonarın prototipleri şöyledir: DWORD GetPriorityClass( HANDLE hProcess ); BOOL SetPriorityClass( HANDLE hProcess, DWORD dwPriorityClass ); Fonksiyonların birinci parametreleri öncelik sınıfı değiştirilecek prosesin HANDLE değerini belirtmektedir. GetPriorityClass API fonksiyonu prosesin öncelik sınıfına geri dönmektedir. SetPriorityClass API fonksiyonu ise prosesin öncelik sınıfını ikinci parametresiyle belirtilen sınıf haline getirmektedir. GetPriorityClass fonksiyonu başarısız olamamaktadır. SetPriorityClass fonksiyonu ise başarı durumunda sıfır dışı bir değere başarsızlık durumunda sıfır değerine geri dönmektedir. Anımsanacağı gibi o anda çalışmakta olan prosesin handle değeri GetCurrentProcess API fonksiyonuyla elde edilmektedir. Biz bir prosesin öncelik sınıfını istediğimiz gibi değiştirebilir miyiz? Windows'ta karmaşık bir güvenlik mekanizması vardır. Bu kursta bu konuya girmeyeceğiz. Ancak bu tür uygulamalarda programı "Run As Administrator" seçeneği ile çalıştırmalısınız. Aşağıdaki örnekte prosesin öncelik değiştirilmiş ve yazdırılmıştır. * Örnek 1, #include #include #include LPCSTR GetPriorityClassName(DWORD dwPriority); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwPriorityClass; if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); if (!SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS)) ExitSys("SetPriorityClass"); if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); return 0; } LPCSTR GetPriorityClassName(DWORD dwPriority) { const char *pszName = "NONE"; switch (dwPriority) { case ABOVE_NORMAL_PRIORITY_CLASS: pszName = "ABOVE_NORMAL_PRIORITY_CLASS"; break; case BELOW_NORMAL_PRIORITY_CLASS: pszName = "BELOW_NORMAL_PRIORITY_CLASS"; break; case HIGH_PRIORITY_CLASS: pszName = "HIGH_PRIORITY_CLASS"; break; case IDLE_PRIORITY_CLASS: pszName = "IDLE_PRIORITY_CLASS"; break; case NORMAL_PRIORITY_CLASS: pszName = "NORMAL_PRIORITY_CLASS"; break; case REALTIME_PRIORITY_CLASS: pszName = "REALTIME_PRIORITY_CLASS"; break; } return pszName; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Thread'in göreli önceliğini almak ve set etmek için GetThreadPriorty ve SetThreadPriority API fonksiyonları kullanılmaktadır. Fonksiyonların prototipleri şöyledir: int GetThreadPriority( HANDLE hThread ); BOOL SetThreadPriority( HANDLE hThread, int nPriority ); Fonksiyonların birinci parametreleri göreli önceliği alınacak ya da dğeiştirilecek thread'in HANDLE değerini almaktadır. SetThreadPriority fonksiyonunun ikinci parametresi thread'in göreli önceliğini belirtmektedir. GetThreadPriority fonksiyonu başarısız olamaz, thread'in göreli önceliği ile geri dönmektedir. SetThreadPriority fonksiyonu başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine geri dönmektedir. O anda çalışmakta olan thread'in HANDLE değeri GetCurrentThread API fonksiyonu ile elde edilebilmektedir. Aşağıda thread'in göreli öncelik derecesi alınıp set edilmiştir. * Örnek 1, #include #include #include LPCSTR GetThreadPriorityName(int threadPriority); void ExitSys(LPCSTR lpszMsg); int main(void) { int threadPriority; if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)) ExitSys("SetThreadPriority"); if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); return 0; } LPCSTR GetThreadPriorityName(int threadPriority) { const char *pszName = "NONE"; switch (threadPriority) { case THREAD_PRIORITY_ABOVE_NORMAL: pszName = "THREAD_PRIORITY_ABOVE_NORMAL"; break; case THREAD_PRIORITY_BELOW_NORMAL: pszName = "THREAD_PRIORITY_BELOW_NORMAL"; break; case THREAD_PRIORITY_HIGHEST: pszName = "THREAD_PRIORITY_HIGHEST"; break; case THREAD_PRIORITY_IDLE: pszName = "THREAD_PRIORITY_IDLE"; break; case THREAD_PRIORITY_LOWEST: pszName = "THREAD_PRIORITY_LOWEST"; break; case THREAD_PRIORITY_NORMAL: pszName = "THREAD_PRIORITY_NORMAL"; break; case THREAD_PRIORITY_TIME_CRITICAL: pszName = "THREAD_PRIORITY_TIME_CRITICAL"; break; } return pszName; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aşağıcdaki örnekte proesin ana thread'i maksimum öncelik olan 31 önceliğe çekilmek istenmiştir. Programı "Run As Administrator" ile çalıştırınız. #include #include #include LPCSTR GetPriorityClassName(DWORD dwPriority); LPCSTR GetThreadPriorityName(int threadPriority); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwPriorityClass; int threadPriority; if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); printf("-----------------------\n"); if (!SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS)) ExitSys("SetPriorityClass"); if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)) ExitSys("SetThreadPriority"); if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); getchar();PCSTR GetPriorityClassName(DWORD dwPriority) const char *pszName = "NONE"; switch (dwPriority) { case ABOVE_NORMAL_PRIORITY_CLASS: pszName = "ABOVE_NORMAL_PRIORITY_CLASS"; brezName = "BELOW_NORMAL_PRIORITY_CLASS"; break; case HIGH_PRIORITY_CLASS: pszName = "HIGH_PRIORITY_CLASS"; break; case IDLE_PRIORITY_CLASS: pszName = "IDLE_PRIORITY_CLASS"; break; case NORMAL_PRIORITY_CLASS: pszName = "NORMAL_PRIORITY_CLASS"; break; case REALTIME_PRIORITY_CLASS: pszName = "REALTIME_PRIORITY_CLASS"; break; } return pszName; } LPCSTR GetThreadPriorityName(int threadPriority) { const char *pszName = "NONE"; switch (threadPriority) { case THREAD_PRIORITY_ABOVE_NORMAL: pszName = "THREAD_PRIORITY_ABOVE_NORMAL"; break; case THREAD_PRIORITY_BELOW_NORMAL: pszName = "THREAD_PRIORITY_BELOW_NORMAL"; break; case THREAD_PRIORITY_HIGHEST: pszName = "THREAD_PRIORITY_HIGHEST"; break; case THREAD_PRIORITY_IDLE: pszName = "THREAD_PRIORITY_IDLE"; break; case THREAD_PRIORITY_LOWEST: pszName = "THREAD_PRIORITY_LOWEST"; break; case THREAD_PRIORITY_NORMAL: pszName = "THREAD_PRIORITY_NORMAL"; break; case THREAD_PRIORITY_TIME_CRITICAL: pszName = "THREAD_PRIORITY_TIME_CRITICAL"; break; } return pszName; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> UNIX/Linux Sistemlerinde : UNIX/Linux sistemlerinde her thread'in belirlenmiş olan bir "çizelgeleme politikası (scheduling policy)" vardır. POSIX standartlarına göre şu çizelgeleme politikaları bulunmaktadır: SCHED_FIFO SCHED_RR SCHED_OTHER SCHED_SPORADIC Bu politikalardan, -> SCHED_SPORADIC politikasının desteklenesi isteğe bağlı bırakılmıştır. -> SCHED_FIFO ve SCHED_RR politikalarıba "gerçek zamanlı (real time)" çizelgeleme politikaları denilmektedir. -> SCHED_OTHER politikasının ayrıntılarına girilmemiş ve bu belirleme işletim sistemini yazanların isteğine bırakılmıştır. Default çizelgeleme politikasının ne olacağı POSIX standartlarında açıkça belirtilmemiştir. O da işletim sistemini yazanların isteğine bırakılmıştır. Linux ve pek çok UNIX türevi sistemdeki default çizelgeleme politikası SCHED_OTHER biçimindedir. POSIX standartlarına göre SCHED_OTHER thread'lerin bir dinamik öncelik derecesi vardır. Dinamik öncelik [0, 39] arasındadır. Default dinamik öncelik 20'dir. Dinamik öncelikte yüksek değer düşük öncelik, düşük değer yüksek öncelik belirtmektedir. Linux sistemlerinde thread'lere quanta doldurulurken her thread'e o thread'in dinamik önceliği ile orantılı bir quanta süresi atanmaktadır. Yani dinamik önceliği yüksek olan thread'lere daha fazla quanta süresi, dinamik önceliği düşük thread'lere daha az quanta süresi atanmaktadır. Böylece programcı thread'in diğer thread'lere göre daha fazla CPU zamanı almasını istiyorsa thread'inin dinamik önceliğini yükseltmektedir. Dinamik öncelikle quanta süresi arasındaki ilişki sistemden sisteme hatta aynı sistemde versiyona bile değişiklik österebilmektedir. Örneğin eskiden Linux sistemlerinde dinamik önceliğin etkisi nispeten azdı daha sonra bu etki yükseltilmiştir. Eskiden UNIX/Linux sistemlerinde thread kavramı yoktu. O zamanlarda thread'ler yerine proseslerin dinamik önceliği vardı. Sonra thread'ler sistemlere eklenince thread'lerin de dinamik öncelikleri oluşturuldu. Ancak eski POSIX fonksiyonları da muhafaze edildi. POSIX standartlarına göre bir prosesin dinamik önceliğ değiştirildiğinde prosesin tüm thread'lerinin dinamik önceliği değiştirilmiş olmalıdır. Ancak Linux sistemleri bu kurala uymamaktadır. Linux sistemlerinde prosesin dinamik önceliği değiştirildiğinde bundan yalnızca prosesin ana thread'i etkilenmektedir. (Halbuki POSIX standartlarında o anda yaratılmış olan ve daha sonra yaratılacak olan tüm thread'lerin dinamik önceliğinin değişmesi gerekmektedir.) Dinamik önceliği değiştirmede en çok kullanılan fonksiyon nice isimli POSIX fonksiyonudur. nice fonksiyonu prosesin dinamik önceliğini değiştirmektedir. (Linux sistemlerinde bu durum tüm thread'lerin değil ana thread'in dinamik önceliğinin değiştirilmesine yol açmaktadır.) nice fonksiyonun protoripi şöyledir: #include int nice(int inc); nice fonksiyonu prosesin dinamik önceliğini o anki dinamik öncelikten parametresi belirtilen miktarda yükseltir ya da alçaltır. Ancak parametrenin anlamı terstir. Yani pozitif değerler "düşürme", negatif değerler "yükseltme" anlamına gelmektedir. Fonksiyon parametre olarak [-20, +19] arasında bir değer almaktadır. Böylelikle biz bu fonksiyona argüman olarak 19 değerini verirsek dinamik öncelik en düşük değeri belirten 39 olur, -20 verirsek dinamik öncelik en yükske değeri belirten 0 olur. Fonksiyon başarı durumunda yeni nice değerine başarısızlık durumunda -1 değerine geri dönmektedir. nice fonksiyonu ile sıradan prosesler önceliklerini düşürebilirler (pozitif parametre) ancak yükseltemezler (negatif parametre). Yükseltme işlemi için prosesin uygun önceliğe sahip olması (örneğin root olması, yani sudo ile çalıştırılması) gerekmektedir. Prosesin nice değeri getpriority POSIX fonksiyonyla elde edilebilmektedir. nice fonksiyonun prosesin dinamik önceliğini değiştirdiğini belirtmiştik. Yalnızca belli bir thread'in dinamik önceliğini değiştirmek için pthread_schedsetparam fonksiyonu kullanılmaktadır. Bu kodudaki ayrıntılar UNIX/Linux sistem programlama kurslarında ele alınmaktadır. Yukarıda da belirtildiği gibi SCHED_FIFO ve SCHED_RR çizelgeleme politikalarına "gerçek zamanlı çizelgeleme politikaları" denilmektedir. UNIX/Linux sistemlerinde çalışma kuyruğunda (yani bloke olmamış) SCHED_FIFO ya da SCHED_RR thread'ler varsa hiçbir zaman SCHED_OTHER thread çizelgelenmemektedir. Tabii çok işlemcili ya da çekirdekli sistemlerde SCHED_FIFO ya da SCHED_RR thread'ler CPU'ya atabndıktan sonra çalışma kuyruğunda artık hiç SCHED_FIFO ya da SCHED_RR thread yoksa SCHED_OTHER thread'ler diğer işlemci ya da çekirdeklerde çizelgelenebilmektedir. Yani SCHED_FIFO ve SCHED_RR thread'lerin SCHED_OTHER thread'lere tam bir üstünlüğü vardır. Pekiyi SCHED_FIFO ve SCHED_RR thread'lerin kendi aralarındaki durum nasıldır? SCHED_FIFO ve ve SCHED_RR thread'lerin de birer önceliği vardır. (Bu öncelik SCHED_OTHER thread'lerin dinamik önceliğinden farklıdır.) Linux sistemlerinde SCHED_FIFO ve SCHED_RR thread'lerin öncelik derecesi 1 ile 99 arasındadır. Burada Yüksek değer yüksek öncelik belirtmektedir. Ancak POSIX standartları bu 1 ve 99 değerlerinin sistemden sisteme değişebileceğini o sistemdeki değerlerin sched_get_priority_min ve sched_get_priority_max fonksiyonlarıyla elde edilmesi gerektiğini belirtmektedir. Çalışma kuyruğunda yalnızca SCHED_FIFO ve SCHED_RR thread'lerin olduğunu varsayalım. (Zaten SCHED_OTHER thread'ler olsa bile çizelgelenmeyecektir.) Çizelgeleme algoritması şöyledir: -> Çizelgeleyici kuyruktaki en yüksek önceliğe sahip olanlar arasında önde bulunan SCHED_FIFO ya da SCHED_RR thread'i CPU'ya atar. Eğer atanan thread SCHED_FIFO ise bloke olana kadar sürekli çalıştrılır. Yani SCHED_FIFO bir thread bloke olmadıktan sonra CPU'yu bırakmamaktadır. Ancak atanan CPU'ya atanan thread SCHED_RR ise bir quanta çalıştırılıp kuyruğun sonuna alınır. -> SCHED_FIFO thread bloke olursa bu durumda kuyrukta en yüksek öncelikte ve önde olan ilk SCHED_FIFO ya da SCHED_RR thread CPU'ya atanır. -> Bloke olmuş olan bir thread'in blokesi çözüldüğünde eğer o anda çalışmakta olan thread'ten daha yüksek öncelikliyse o anda çalışmakta olan thread'in çalışması kesilir ve blokesi çözülen thred çalıştırılır. Tabii bu yeni thread SCHED_FIFO ise sürekli çalıştırılacak, SCHED_RR ise bir quanta çalıştırılıp sona alınacaktır. Eğer blokesi çözülmüş olan thread o anda çalışmakta olan thread'le eşit öncelikli ya da ondan daha düşük öncelikli ise kuyruğun sonuna alınmaktadır. Blokesi çözülmüş yüksek öncelikli thread tarafından kesilen thread eğer SCHED_FIFO thread'se kuyruğun önüne yerleştirilir. SCHED_RR thread'se kuyruğun sonuna alınır. Bu çizelgeleme sisteminde şu durumlara dikkat ediniz. -> Bu sistemde çalışma kuyruğundaki bütün geçek zamanlı zamanlı thread'lerin SCHED_RR olduğunu varsayalım. Bu durumdaki çizelgeleme Windows'taki çizelgelemeye çok benzer durumdadır Yani en yüksek öncelikteki thread'ler kendi aralarında döngülsel çizelgelenecektir. -> CPU'ya atanmış olan SCHED_FIFO thread'in CPU'yu bırakması ancak daha yüksek öncelikli SCHED_FIFO ya da SCHED_RR thread'in uykudan uyanmasıyla ya da yüksek öncelikli yeni bir thread'in yaratılmasıyla ya da o thread'ın bloke olmasıyla mümkündür. SCHED_FIFO politikası bu sistemlerde "sürekli çalışan tüm CPU'yu tek başına kullanan thread'lerin oluşturulması" amacıyla bulundurulmuştur. Yüksek öncelikli SCHED_FIFO thread'ler diğer thread'lerin çalışmasını engelleyebilmektedir. Aşağıdaki örnekte gerçek zamanlı çizelgeleme politikalarına ilişkin öncelik derecelerinin en düşük ve en yüksek değeri sched_get_priority_min ve sched_get_priority_max fonksiyonları ile elde edilip yazdırılmıştır. SCHED_OTHER politikası için bu fonksiyonlar 0 değerini geri döndürmektedir. * Örnek 1, #include #include #include #include #include #include #include #define QUEUE_SIZE 10 void *thread_producer(void *param); void *thread_consumer(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_queue[QUEUE_SIZE]; size_t g_head; size_t g_tail; int main(void) { pthread_t tid1, tid2; int result; if (sem_init(&g_sem_producer, 0, QUEUE_SIZE) == -1) exit_sys("sem_init"); if (sem_init(&g_sem_consumer, 0, 0) == -1) exit_sys("sem_init"); 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) { int val; unsigned seed; seed = time(NULL) + 123; val = 0; for (;;) { usleep(rand_r(&seed) % 300000); if (sem_wait(&g_sem_producer) == -1) exit_sys("sem_wait"); g_queue[g_tail] = val; g_tail = (g_tail + 1) % QUEUE_SIZE; if (sem_post(&g_sem_consumer) == -1) exit_sys("sem_post"); if (val == 99) break; ++val; } return NULL; } void *thread_consumer(void *param) { int val; unsigned seed; seed = time(NULL) + 456; for (;;) { if (sem_wait(&g_sem_consumer) == -1) exit_sys("sem_wait"); val = g_queue[g_head]; g_head = (g_head + 1) % QUEUE_SIZE; if (sem_post(&g_sem_producer) == -1) exit_sys("sem_post"); usleep(rand_r(&seed) % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } printf("\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 bir prosesin ya da thread'in çizelgeleme politikası ve gerçek zamanlı thread'lerin öncelikleri nasıl belirlenmektedir? Bir prosesin çizelgeleme politikası sched_setscheduler fonksiyonuyla set edilip sched_getcheduler fonksiyonu ile alınabilmektedir. Bu POSIX fonksiyonları Linux sistemlerinde doğrudan ilgili sistem fonksiyonlarını çağırmaktadır. Fonksiyonların prototipileri şöyledir: #include int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param); int sched_getscheduler(pid_t pid); Fonksiyonların birinci parametreleri prosesin id değerini belirtmektedir. Bu parametre 0 girilirse fonksiyonları çağıran proses için işlem yapılmaktadır. sched_setscheduler fonksiyonun ikinci parametresi çizelgeleme politikasını üçüncü paarametresi ise gerçek zamanlı çizelgeleme politikalarına ilişkin öncelik derecesini belirtmektedir. Vu üçüncü parametrede SCHED_OTHER için öncelik belirtilememektedir. Bu parametre yalnızca SCHED_FIFO ve SCHED_RR için anlamlıdır. Fonksiyon başarı durumunda eski çizelgeleme politikasına başarısızlık durumunda -1 değerine geri dönmektedir. sched_getscheduler fonksiyonu da başarı durumunda çizelgeleme politikasına, başarısızlık durumunda -1 değerine geri dönmektedir. Prosesin çizelgeleme politikasını değiştirebilmek için sched_setscheduler fonksiyonunu çağıran prosesin uygun önceliğe sahip olması gerekir. Aşağıdaki programda proses kendi çizelgeleme politikasını SCHED_FIFO yapıp önceliğini de Linux'taki maksimum öncelik olan 99 yapmıştır. Tabii programı sudo ile çalıştırmalısınız. * Örnek 1, #include #include #include #include void* thread_proc(void* param); void exit_errno(const char* msg, int result); int main(void) { int result; pthread_t tid;; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_errno("pthread_create", result); pthread_join(tid, NULL); return 0; } void* thread_proc(void* param) { struct sched_param sparam; int result; long i; sparam.sched_priority = 99; if ((result = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sparam)) != 0) exit_errno("pthread_setschedparam", result); for (i = 0; i < 1000000000; ++i) ; return NULL; } void exit_errno(const char* msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); } Yukarıda da belirttiğimiz gibi POSIX standartlarına göre aslında prosesin çizelgeleme politikası ya da öncelik dereceleri değiştirildiğinde bundan prosesin tüm thread'leri etkilenmektedir. Ancak Linux'ta bundan yalnızca prosesin ana thread'i etkilenmektedir. Ancak biz belli bir thread'imizin de diğerlerin bağımsız olarak çizelgeleme politikasını değiştirip elde edebiliriz. Bunlar için pthread_setschedparam ve pthread_getschedparam fonksiyonları kullanılmaktadır: #include int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); int pthread_getschedparam(pthread_t thread, int *policy, struct sched_param *param); Fonksiyonlar sched_setscheduler ve sched_getscheduler fonksiyonları gibi çalışmaktadır. Yalnızca proses id yerine thread id değerini parametre olarak almaktadır. Diğer thread fonksiyonlarında olduğu gibi bu fonksiyonlar da başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönemektedir. Aşağıdaki örnekte bir thread'in çizelgeleme politikası ve önceliği değiştirilmektedir. * Örnek 1, #include #include #include #include void* thread_proc(void* param); void exit_errno(const char* msg, int result); int main(void) { int result; pthread_t tid;; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_errno("pthread_create", result); pthread_join(tid, NULL); return 0; } void* thread_proc(void* param) { struct sched_param sparam; int result; sparam.sched_priority = 99; if ((result = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sparam)) != 0) exit_errno("pthread_setschedparam", result); for (long i = 0; i < 1000000000; ++i) ; return NULL; } void exit_errno(const char* msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); }