"Programming Applications for Microsoft Windows (4th Edition)" Jeffrey Richter
"Advanced Windows" Jeffrey Richter
"Win32 Systems Programming"


Threadler


Thread kullanımı Win32 sistemleriyle mumkun hale getirilmiştir. Klasik UNIX sistemlerinde thread kavramı yoktur. Ancak son yıllarda UNIX sistemlerine de thread konusu dahil edilmiştir. Aslında thread konusunun kendisi yazılımda yeni bir konudur. Bir process n thread’den oluşur. Her process calışmaya tek bir thread’le başlar. Buna ana thread denir. Process’in calışması ana thread’den başlar. Diğer thread’ler herhangi bir zamanda herhangi bir thread icerisinde CreateThread API fonksiyonuyla yaratılırlar.

Thread bir process’in farklı bir programmış gibi calışabilen parcalarına denir. Win32’nin zaman paylaşımlı calışma sistemi process temelinde değil thread temelinde yapılmaktadır. Orneğin sistemde uc process calışmakta olsun. P1 process’inin uc, P2 process’inin dort, P3 process’inin de iki thread’i olsun. Win32 sistemlerinin quanta suresi 20 ms’dir. Sistem her quanta suresi dolduğunda dokuz thread’den birini bırakacak diğerini alacaktır.


[IMG]http://img220.**************/img220/2342/image006nk4.gif[/IMG]




Bir process’in thread’leri sanki ayrı programlarmış gibi asenkron bir bicimde ele alınıp calıştırılırlar. C programcısı icin herhangi bir fonksiyon thread olarak tanımlanabilir. Bir thread CreateThread API fonksiyonuyla yaratılıp calıştırılmaya başlanır. Aşağıdaki durumlardan bir tanesi oluştuğunda sonlanır.

1. Thread olarak belirlenen fonksiyonun icerisinde ExitThread API fonksiyonunun cağırılması ile,

2. Thread olarak belirlenen fonksiyon ana bloğunu bitirip sonlandığında,

3. Thread olarak belirtilen fonksiyon icerisinde return anahtar sozcuğu kullanılarak fonksiyon sonlandırıldığında.

Aslında thread olarak belirlenen fonksiyon yalnızca thread’in başlangıc ve bitiş noktalarını belirlemekte etkilidir. Yoksa thread bir akış belirtir. Ana thread’in başlangıc noktası derleyicinin başlangıc kodudur. WinMain başlangıc kodu tarafından cağırılır. Tabii WinMain akış bakımından ana thread’i temsil eder. Bir process yaratıldığında ana thread işletim sistemi tarafından yaratılır. Yani WinMain icerisinde akış ana thread uzerindedir. Fakat ana thread’in başlangıc noktası derleyicinin başlangıc kodudur. Orneğin iki thread’in akışı aynı herhangi bir fonksiyonun icerisinde olabilir. Ozetle thread bir akış belirtir. Thread fonksiyonu ise sadece akışın başlangıc fonksiyonudur. Bir thread fonksiyonu bittiğinde akış nereden devam edecektir? Aslında thread fonksiyonu CreateThread API fonksiyonu icerisinden cağırılır. Thread fonksiyonu doğal olarak sonlandığında akış CreateThread icerisinden devam eder. İşte o noktada ExitThread fonksiyonu uygulanır. CreateThread ve ExitThread fonksiyonları mantıksal olarak şunları yapmaktadır:

CreateThread()


ExitThread()



Tabii CreateThread API fonksiyonu cağırıldığında bu fonksiyon yeni bir akışı yaratarak kendi akışına devam eder.

Bir process’in thread’ler arasındaki gecişlerinde sayfa tablosunda hicbir değişiklik yapılmaz. Yani bir process’in butun thread’leri aynı adres alanı icerisinde calışmaktadır. Aynı thread’lerin arasındaki haberleşme normal global değişkenler kullanılarak yapılabilir. Ancak farklı process’lerin thread’leri arasındaki geciş sırasında sayfa tablosu yeniden duzenlenir. Boylece farklı process’lerin thread’leri arasıda adres alanı bakımından bir izolasyon sağlanmış durumundadır. Aynı process’in thread’leri arasındaki geciş zamansal bakımdan farklı process’lerin thread’leri arasındaki gecişten daha hızlıdır.


Thread’lerle Calışmanın Process’lerle Calışmaya Gore Avantajları

Thread sisteminin olmadığı işletim sistemlerinde işlemler process’lerle yapılır. Thread’lerle calışmanın process’lerle calışmaya gore avantajları şunlardır:

1. Thread’ler arasındaki geciş işlemi process’ler arasındaki geciş işleminden daha hızlı yapılır(Tabii farklı process’lerin thread’leri arasındaki geciş soz konusuysa bu daha yavaştır).

2. Coklu thread calışmalarında process’in bloke olma olasılığı daha azdır. Orneğin klavyeden bir girdi beklense tum process bloke edilmez, yalnızca işlemin yapıldığı thread bloke edilir(Process’in bloke edilmesi işletim sisteminin bir process’i dışsal bir olay gercekleşene kadar cizelge dışı bırakması işlemidir).

3. Thread’ler arası haberleşme process’ler arası haberleşmeden cok daha kolaydır. Sadece global değişkenlerle haberleşme sağlanabilir.
Thread’lerle Calışmanın Process’lerle Calışmaya Gore Dezavantajları

1. Thread’ler arasında process’lere gore daha yoğun bir senkronizasyon uygulamak gerekir.
2. Process’ler birbirlerinden izole edildikleri icin cok process’le calışılması daha guvenlidir.
İşlevlerine Gore Thread’lerin Sınıflandırılması

1. Uyuyan Thread’ler(sleepers threads): Bu tur thread’ler bir olay oluşana kadar beklerler. Olay oluşunca bir faaliyet gosterirler. Sonra o olay oluşana kadar yeniden beklerler. Tabii bekleme işi thread bloke edilerek yapılmaktadır.

2. Tek İş Yapan Thread’ler(one shot threads): Bu thread’ler bir olay gercekleşene kadar bekler. Olay gercekleşince faaliyet gosterir ve thread’in calışması biter.

3. Onceden Faaliyet Gosteren Thread’ler(anticipating threads): Burada ileride yapılacak bir işlem onceden yapılır. Eğer akış o işleme gerek kalmayacak bir bicimde gelişiyorsa işlem boşuna yapılmış olur. Eğer akış o işlemin farklı bir bicimde yapılmasını gerektirecek bir şekilde gelişiyorsa o işlem yeniden yapılır.

4. Beraber Faaliyet Gosteren Thread’ler: Burada spesifik bir iş vardır. CPU’dan daha fazla zaman alacak bicimde işlem birden fazla thread tarafından yapılır.

5. Bağımsız Faaliyet Gosteren Thread’ler: Bu thread’ler tamamen farklı amacları gercekleştirmek icin yazılır. Genellikle bir senkronizasyon problemi oluşturmazlar. Tasarım kolaylığı sağlamak amacıyla kullanılırlar.
Thread’lerin Oncelik Derecelendirilmesi

Windows ve UNIX sistemleri cizelgeleme algoritması olarak dongusel cizelgeleme(round robin scheduling) metodu kullanılır. Bu cizelgeleme yonteminde process’ler ya da thread’ler sırasıyla calıştırılır. Liste bittiğinde başa donulerek tekrardan başlanır. Ancak Win32’de oncelikli dongusel cizelgeleme(priority round robin scheduling) denilen bir yontem kullanılır. Bu yontemde her thread’in 0-31 arasında bir oncelik derecesi vardır. Bu yontemde en yuksek oncelikli thread grubu diğerlerine bakılmaksızın kendi aralarında calıştırılır. O grup tamamen bitirilince daha duşuk gruptakiler yine kendi aralarında cizelgelenir ve işlemler boyle devam ettirilir. Orneğin sistemde 8 tane thread şu onceliklerle bulunsun:



[IMG]http://img220.**************/img220/7874/image007za6.gif[/IMG]





İlk once 18 oncelikliler işleme alınır. Bu grup bitirilince 14’lukler kendi aralarında calıştırılır ve bitirilirler. Sonra 8’likler en son da 1’likler kendi aralarında calıştırılıp bitirilirler. Duşuk oncelikli thread’lerin calışma olasılıkları duşuk olmakla birlikte 0 değildir. Ancak duşuk oncelikli thread’ler iki nedenden dolayı calışma fırsatı bulabilirler:

1. Yuksek oncelikli butun thread’lerin bir t anında bloke edilmiş olma olasılığı vardır. Boylelikle duşuk oncelikli thread’ler de calışma fırsatı bulur.

2. Win32 sistemleri ismine dinamik yukseltme(dynamic boosting) uygular. Dinamik yukseltme uygulanan iki durum vardır:

a. Bir thread 3-4 saniye suresince hic calışma fırsatı bulamadıysa Win32 tarafından onceliği 2 quanta suresi kadar 15’e yukseltilir. 2 quantadan sonra tekrar eski derecesine indirilir.

b. Hicbir giriş-cıkış faaliyetine yol acmayan ve hicbir penceresi olmayan process’lere background process denir. Bir penceresi olan ve giriş-cıkış işlemi kullanan process’lere foreground process denir. Giriş-cıkış işlemlerine yol acan sistem fonksiyonları otomatik olarak process’i bir quanta suresi icin 2 derece, sonraki quanta suresi icinse 1 derece yukseltirler. Sonraki quanta’larda eski haline dondurulurler.






Thread Onceliklerinin Belirlenmesi

Programcı istediği bir thread’e 0-31 arası bir oncelik verebilir. Ancak thread oncelikleri iki aşamada belirlenmektedir. Bunlar process’in oncelik sınıfı(process priority class) ve thread’in goreli onceliği(thread relative priority) kavramlarıyla belirlenir. Bir thread’in onceliği process’in oncelik sınıfının değeriyle thread’in goreli onceliğinin toplanması biciminde elde edilir. Bir process’in oncelik sınıfı şunlardır:

ABOVE_NORMAL_PRIORITY_CLASS (NT5.0’da gecerli)

BELOW_NORMAL_PRIORITY_CLASS (NT5.0’da gecerli)

REALTIME_PRIORITY_CLASS (24)

HIGH-PRIORITY_CLASS (13)

NORMAL_PRIORITY_CLASS (7)

IDLE_PRIORITY_CLASS (4)


Process’in oncelik sınıfı 2 bicimde belirlenebilir:

1. CreateProcess fonksiyonunun altıncı parametresinde yukarıda belirtilen sembolik sabitler kullanılarak. Normal olarak altıncı parametrede bu belirleme yapılmazsa NORMAL_PRIORITY_CLASS yazılmış gibi işlem gorur. Windows Explorer Desktop işlemlerinde bu oncelikle process’leri calıştırır.
2. Process’e ilişkin herhangi bir thread icerisinde SetPriorityClass API fonksiyonuyla. Ayrıca GetPriorityClass API fonksiyonuyla da process’in oncelik sınıfının ne olduğu elde edilebilir. Prototipi:

BOOL SetPriorityClass(
HANDLE hProcess,
DWORD dwPriorityClass
);

DWORD GetPriorityClass(
HANDLE hProcess
);

Bir thread’in goreli onceliği şunlar olabilir:

THREAD_PRIORITY_ABOVE_NORMAL (+1)

THREAD_PRIORITY_BELOW_NORMAL (-1)

THREAD_PRIORITY_HIGHEST (+2)

THREAD_PRIORITY_LOWEST (-2)

THREAD_PRIORITY_IDLE Bu REALTIME_PRIORITY_CLASS oncelik sınıfı icin 16, diğer sınıflar icin 1 oncelik anlamına gelir. Yani toplama yapılmaz. 24’u 16, diğerlerini 1 yapar.

THREAD_PRIORITY_NORMAL (0)
THREAD_PRIORITY_TIME_CRITICAL REALTIME_PRIORITY_CLASS sınıf onceliği icin 31, diğerleri icin 15 oncelik oluşturur.

Bir thread’in goreli onceliği de iki bicimde belirlenebilir:

1.CreateThread API fonksiyonuyla bir thread yaratırken beşinci parametrede yukarda belirtilen sembolik sabitlerden biri girilerek,
2.SetThreadPriority API fonksiyonuyla. Prototipi:

BOOL SetThreadPriority(
HANDLE hThread,
int nPriority
);

Tabii thread’in goreli onceliği GetThreadPriority fonksiyonuyla elde edilir.



Thread Oncelik Değerlerinin Değiştirilmesi
Bir process default olarak masa ustunden ya da Windows Explorer’dan calıştırılıyorsa process’in oncelik sınıfı NORMAL_PRIORITY_CLASS(7), thread’in goreli onceliği ise THREAD_PRIORITY_NORMAL bicimindedir. Boylece thread’in gercek oncelik derecesi 8 olur. Orneğin bir process’in ana thread’inin onceliği şu bicimde değiştirilebilir:

SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS);
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST);
Not: GetTickCount API fonksiyonu sistem ilk acıldığında sıfırlanmış olan ve her milisaniyede bir arttırılan global bir zaman sayacının değerini verir. Boylece program icerisinde iki nokta arasında gecen zaman milisaniye cinsinden bulunabilir.

Bir thread’in oncelik derecesini test eden ornek program şoyle yazılabilir:

#include
#include

void main(void)

Thread’lerin Durdurulması ve Calıştırılması

Programcı isterse handle değerini bildiği bir thread’i SuspendThread API fonksiyonuyla bloke edebilir. Bu fonksiyonla thread cizelge dışı bırakılır ve ancak cizelgeye tekrar alınabilmesi icin ResumeThread API fonksiyonu kullanılır. Prototipleri:

DWORD SuspendThread(
HANDLE hThread
);

DWORD ResumeThread(
HANDLE hThread
);

Fonksiyonların parametreleri ilgili thread’in handle değeridir. Bu handle değeri thread yaratılırken elde edilebilir ya da eğer kendi thread’imiz soz konusuysa GetCurrentThread fonksiyonuyla alınabilir. Genellikle bu fonksiyonları thread’i yaratan thread’in kendisi kullanmaktadır. Orneğin:

hThread = CreateThread(...);
...
SuspendThread(hThread);
...
ResumeThread(hThread);

Bir process’i durdurmak istesek bunu nasıl yapabiliriz? Process’in butun thread’lerine SuspendThread uygulamak gerekir. Process’in thread’lerine ilişkin ID değerleri Toolhelp fonksiyonlarıyla alınabilir. ID değerlerini handle değerlerine donuşturmek gerekir. Bunun icin yalnızca Windows 2000’de gecerli olan OpenThread fonksiyonu gecerlidir.

Genel olarak bir kernel nesnesinin handle değeri başka bir process’e gonderilirse o process bu değerden faydalanamaz. Cunku kernel nesnelerinin handle değerleri process icin anlamlı goreli değerlerdir. Bunlar process handle tablosunda bir index belirtirler. Ancak DuplicateHandle adlı bir API fonksiyonu ile kendi process’imize ilişkin bir handle değerini alarak istediğimiz bir process’e ilişkin bir handle değeri haline donuşturur.

Ayrıca UNIX sistemlerinde de olan unlu bir Sleep fonksiyonu vardır. Bu fonksiyon yalnızca fonksiyonun cağırıldığı thread’i belirli bir milisaniye durdurur. Ayrıca bir de SleepEx fonksiyonu vardır.

void Sleep(DWORD dwMiliSecond);

Fonksiyonun parametresi thread’in kac milisaniye durdurulacağıdır. Eğer bu parametre INFINITE olarak girilirse thread suresiz olarak durdurulur. Thread’i calıştırılabilmek icin ResumeThread uygulamak gerekir. Yalnızca Windows’da değil tum multi-processing sistemlerde program akışının bir sure durdurulabilmesi icin boş bir dongude beklemek yerine Sleep fonksiyonunu kullanmak gerekir. Cunku Sleep fonksiyonu thread’i ya da process’i cizelgeleme dışı bırakarak bekletmeyi sağlar. Tabii fonksiyonda belirtilen milisaniye zaman aralığı belli bir yanılgıyla ancak hesaplanabilmektedir. Şuphesiz Sleep fonksiyonuyla cizelgeleme dışı bırakılmasının thread onceliğiyle doğrudan bir ilgisi yoktur. Yani thread cizelgelemeye alınsa bile oncelik derecesinin duşuk olmasından dolayı calıştırılmayabilir. Bu durumda aşağıdaki kodun calıştırılmasında sayıların yaklaşık birer saniye aralıklarla basılmasının hicbir garantisi yoktur:

#include
#include

void main(void)

}

Thread’lerin Durdurma Sayacları(suspend counters)


Her thread’in bir durdurma thread’i vardır. Thread yaratıldığında bu sayac birdir. Bu sayac sıfırdan farklı olduğunda thread cizelgeleme dışı bırakılır. Aslında ResumeThread fonksiyonu yalnızca durdurma sayacını bir eksiltir. SuspendThread fonksiyonu ise sayacı bir arttırır. Sayac sıfır dışı ise thread’i cizelgeleme dışı bırakır. Orneğin thread’i ust uste iki kez SuspendThread fonksiyonu uygulanabilir. Bu durumda durdurma sayacı iki olur. Thread cizelgeleme dışı bırakılmıştır. Thread’in tekrar calıştırılabilmesi icin iki kere ResumeThread fonksiyonu uygulanmalıdır. SuspendThread ve ResumeThread fonksiyonlarının geri donuş değerleri thread’in onceki durdurma sayac değeridir. Orneğin yeni yaratılmış bir thread’e suspend uygulandığında fonksiyonun geri donuş değeri sıfır olur. Bir thread’in durdurma sayacı Toolhelp fonksiyonlarıyla alınabilir. Ancak en pratik yontem thread’e SuspendThread fonksiyonunu uygulayıp ya da ResumeThread fonksiyonunu uygulayıp geri donuş değerine bakmaktır. Durdurma sayacı değeri negatif değere duşemez yani thread yaratıldığında ResumeThread uygulanırsa fonksiyon başarısızlıkla sonuclanır.

CreateThread Fonksiyonu

Bir thread CreateThread API fonksiyonuyla yaratılır. Fonksiyonun prototipi:

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

Fonksiyonun birinci parametresi guvenlik bilgileriyle ilgilidir. Windows 2000 ve NT sistemleri icin onemlidir. Bu parametre kısaca NULL biciminde gecilebilir. Fonksiyonun ikinci parametresi thread stack alanının uzunluğudur.

Stack yerel değişkenlerin gecici olarak yaratıldığı depo alanıdır. Programlamada bir t anındaki toplam yerel değişkenlerin miktarı ayrılmış stack alanından fazla olamaz. Ozellikle kendi kendini cağıran fonksiyonlarda fazlaca yerel değişkenlerin kullanılması probleme yol acabilmektedir. Orneğin DOS’te stack alanı 64KB’tır. Windows’da her thread’in ayrı bir stack alanı vardır. Bu durum thread’lerin sanki farklı programlarmış gibi calıştırılabilmesinin zorunlu bir sonucudur. Yeni yaratılmış olan bir thread’in stack alanı bu parametrede belirtilebilir. Bu parametrede belirtilen tum alan tahsis edilir ve commit edilir. Eğer bu parametreye sıfır girilirse stack alanı olarak default 1 MB tahsis edilir. Microsoft yalnızca 1 sayfayı commit eder. Ancak gerektiğinde diğer sayfalar otomatik commit edilir. Aslında 1 MB değeri PE formatının icerisinden alınmaktadır. Bu değer PE formatı icerisine linker tarafından yazılmaktadır. Visual C++ sisteminde bu değer manuel olarak /STACK:size, commit ile değiştirilebilir.

Fonksiyonun ucuncu parametresi thread akışının başlangıcını belirten fonksiyon gostericisidir. Bu parametreye thread fonksiyonunun adresi gecirilir. LPTHREAD_START_ROUTINE aslında aşağıdaki gibi bir typedef ismidir:

typedef DWORD (WINAPI * LPTHREAD_START_ROUTINE) (LPVOID)

Yani bu parametreye ancak geri donuş değeri DWORD, parametresi void turunden bir gosterici olan ve WINAPI cağırma bicimine sahip olan bir fonksiyonun adresi gecirilebilir. Bu durumda thread fonksiyonu aşağıdaki gibi tanımlanmış olmalıdır:

DWORD WINAPI ThreadProc(LPVOID pvParam)

Fonksiyonun dorduncu parametresi thread fonksiyonuna gecirilecek olan parametredir. Yani thread fonksiyonu CreateThread tarafından bu parametre gecirilerek calıştırılır. Tabii bu parametre programcının istediği bir nesnenin adresi olabilir. Ya da programcı boyle bir parametreye gereksinim duymayabilir. O zaman bu parametreye NULL gecirilebilir. Fonksiyonun beşinci parametresi thread yaratıldığında thread fonksiyonunun yaratılır yaratılmaz calıştırılıp calıştırılmayacağını belirlemekte kullanılır. Eğer bu parametre 0 girilirse thread fonksiyonu thread yaratılır yaratılmaz calıştırılır. Yok eğer bu parametre CREATE_SUSPENDED olarak girilirse thread yaratılır yaratılmaz calışmaz, calıştırmak icin ResumeThread fonksiyonu uygulanmalıdır. Fonksiyonun son parametresi thread’in ID değerini almakta kullanılır. Fonksiyon thread’in ID değerini parametresiyle aldığı bu adrese yerleştirir. Bazı fonksiyonlar thread ID değerini parametre olarak alır. Oysa thread’in handle değeri process handle tablosunda bir index belirtmektedir.





Thread’in Sonlandırılması

Bir thread akış belirtir. Bu akış şu durumlarda sonlanmaktadır:

1.Thread fonksiyonu sonlandığında. Bu durumda akış CreateThread fonksiyonundan devam eder. Tabii aslında şunlar gercekleşmektedir:

CreateThread yeni bir akışı oluşturur ve yeni akış aslında calışmaya ilk kez yine CreateThread icerisinden devam eder. Yani yeni akış aslında calışmasına thread fonksiyonundan başlamaz. O halde CreateThread aşağıdaki gibi yazılmıştır:

CreateThread(....)

return hThread;
}

Boylece aslında thread fonksiyonunun calışması bittiğinde yeni akış ExitThread fonksiyonunu gorerek bitirilmektedir.

2.Thread akışı icerisinde doğrudan ExitThread API fonksiyonunun cağırılmasıyla. ExitThread şuphesiz başka bir thread tarafından değil, thread akışı uzerinde cağırılmalıdır. Yani ExitThread her zaman kendi thread’ini sonlandırır. Prototipi:

void ExitThread(DWORD dwExitCode);

Exit thread fonksiyonunun başarısız olma olasılığı yoktur. Parametresi thread’in exit kodudur. Bu exit kodu başka bir API fonksiyonuyla alınabilir.

3.Başka bir thread’in TerminateThread fonksiyonunu cağırması sonucunda.Prototipi:

BOOL TerminateThread(
HANDLE hThread,
DWORD dwExitCode
);

Fonksiyonun birinci parametresi sonlandırılacak thread’in handle değeri, ikinci parametresiyle thread’in exit kodudur.

4. Process’in ana thread’i sonlandığında. GUI uygulamalarında WinMain, console uygulamalarında main ana thread akışını temsil eder. Bu fonksiyonlar bittiğinde akış derleyicinin başlangıc kodundan devam edecektir(startup module). Burada da ExitProcess cağırılmaktadır. Sonuc olarak ExitProcess API fonksiyonu butun thread’leri sonlandırmaktadır.

Thread’ler ve Mesaj Sistemi


Bir thread GUI uygulamalarından hicbir pencere yaratmamışsa boyle thread’lere “Worker Thread” denir. Eğer thread en az bir pencere yaratmışsa bu tur thread’lere “User Interface Thread” denir. Win32’de her thread’in ayrı bir mesaj kuyruğu vardır. Yani bir thread icerisinde bir pencere yaratmışsak derhal WinMain’de yaptığımız gibi bir mesaj dongusu oluşturmalıyız. Aslında GetMessage fonksiyonu her zaman o anda calışmakta olan thread’in mesaj kuyruğundan mesajı alır.






Bu konu uzmanlık gerektiren bir konu ve ileri aşamlaı konulardandır...

"Programming Applications for Microsoft Windows (4th Edition)" Jeffrey Richter
"Advanced Windows" Jeffrey Richter
"Win32 Systems Programming"
__________________