Multithread Programlama: Process,Thread,Event





Giriş: 

Şöyle bir düşünün: Oyunu başlatmak için sesleri , imajları, haritaları, modelleri yüklemeniz lazım.
önce sesleri yükleyecek prosedürü çağırır sonra imajları yükleyecek prosedürü çağırır sonra haritaları sonrada modelleri... Oyunu oynamak için bekleyen oyuncu bu ilklemeleri beklerken ağaç olur. Keşke dört koldan bu dört prosedürü aynı zamanda(eşzamanlı) çağırıp işletilebilsek...

Veya şöyle düşünün: Oyununuzdaki karakterin karşısında 4 düşman var ve birini saldırıyor ikinciye saldırıyor üçüncüye saldırıyor dördüncüye saldırıyor ama düşmanın eli armut topluyor. Çünkü Kodlarımız önce 1. düşmana,sonra 2. sonra 3. sonrada 4. için. Oysa diğer düşmanlar 1.düşmana saldırı anında bize saldırmak için vakti var ama bizim kodlarımız 1.düşmana odaklanmış durumda. Keşke Aynı anda tüm düşmanların ve bizim ayrı ayrı zekası(kodları) olsa...

Bu ve benzerlerini yapmak için çözümümüz Multithread(çoklu iş parçaları) programlamadır.

Process:
Process nedir? MSDN'deki tanımı aktarayım:

“Process, hususi virtual adres yüzeyi,kod, data, ve process'e açık sekronize nesneler, hatlar(pipe),dosyalar vb diğer işletim sistemi kaynaklarından oluşan işletilebilir bir uygulamadır”.

Bu tanımdan bir process'in aslında bizim bildiğimiz exe'den(modülden) başka bir şey olmadığını çıkarsamak yanlış olmaz.

Bir process, minimum, işletilebilir modül, hususi adres yüzeyi ve bir thread'dan oluşur.
Windows bir process oluşturmak üzere bir komut aldığında bu process için hususi bellek adres yüzeyi yaratıp sonra bu yüzeye işletilebilir dosyayı mapler. Ondan sonra, bu process için birincil threadı yaratır.


Oyun programlamada çok kullanım alanı bulacağını zannetmesemde , bir process'i programımızdan nasıl çağıracağımızı görelim ilkin(aklıma gelen tek örnek: Oyun bilgisyara kurulduktan sonra kullanıcıya lisans ile ilgili bir yazı göstertebiliriz).



Bunu şu api ile yaparız:

BOOL WINAPI CreateProcess(
__in_opt LPCTSTR lpApplicationName,
__inout_opt LPTSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in_opt LPVOID lpEnvironment,
__in_opt LPCTSTR lpCurrentDirectory,
__in LPSTARTUPINFO lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);




lpApplicationName --> işletmeyi istediğiniz exe dosyanın diziniyle adı. Eğer bu parametre NULL verilirse lpCommandLine parametresinde exe dosyasının ismini belirtmelisiniz.

lpCommandLine --> işletmeyi istediğiniz programa komut satırı argümanları. Eğer lpApplicationName NULL ise bu parametre işletilebilir dosyanın isminide içermek zorunda. Şunun gibi: "notepad.exe readme.txt"

lpProcessAttributes and lpthreadAttributes --> process ve birincil thread için güvenlik özelliklerini belirtir. Eğer NULL olursa varsayılan güvenlik özellikleri kullanılır.

bInheritHandles --> Kendi Processimizden açık tüm handleleri kalıt olarak yeni process'e geçmesini istediğinizi belirten bir flag.

dwCreationFlags --> Çalıştırmayı istediğimiz process yaratıldığında hemen işletilmeye başlamayıp derhal askıya alınması , çalışmaya başlamadan önce modife edebilme , incelenebilme gibi davranışlarını kararlaştıran çeşitli flaglar. Aynı zamanda çalıştırılan processdeki threadın(ların) öncelik sınıfını belirtebilirsiniz. Bu öncelik sınıfı processdeki threadların zaman önceliğini belirlemede kullanılır. Normal şartlarda, NORMAL_PRIORITY_CLASS flagını kullanırız.

lpEnvironment --> Çalıştırmayı istediğimiz process için çeşitli çevre stringlerini içeren çevre bloğuna pointer. Eğer bu parametre NULL ise yeni process ebeveyn process'den çevre bloğunu kalıt alır.

lpCurrentDirectory --> Çalıştırmayı istediğimiz process için mevcut dizini ve sürücüyü belirten stringe pointer.

lpStartupInfo --> Çalıştırmayı istediğimiz processin ana penceresinin görüntüsünün nasıl olacağını belirten STARTUPINFO yapısına pointer.

lpProcessInformation --> Çalıştırmayı istediğimiz process hakkında çeşitli tanımlayıcı bilgileri alan PROCESS_INFORMATION yapısına pointer.


Thread
Windows bir processi ilk yarattığında process başına sadece bir thread yaratır(birincil thread). Bu thread genellikle modüldeki ilk talimattan işletilmeye başlar. Eğer process sonra başka threadlara ihtiyaç duyarsa, onları kolayca yaratabilir. 
Peki Thread nedir? Windows , bir anda birden fazla programın aynı anda çalıştığı multithread bir işletim sistemidir. Aslında bir anda birden fazla program çalışmaz. Windows her programa çalışmak için bir zaman dilimi verir ve diğer programlar bu esnada askıda bekler, bu zaman dilimi dolduğunda diğer programa işletim hakkını geçirilir. Velhasıl bir kuyrukta herkez işletim hakkının kendine geçmesini bekler. Yani bu izahtan thread'ı işletme hakkı için kuyrukta bekleyen process'in kod bölümüdür (iş parçası)şeklinde çıkarsamak yanlış olmaz.


Nihayet Multithread programlamayıda , process'de ayrı işletim haklarına (sürelerine) sahip kod bölümleridir(threadlar) diyebiliriz o zaman.

Threadlar aynı processde çalışır ki global değişkenler, handleler gibi processdeki her kaynağa erişebilirler. Tabi her threadın kendine özgül stack'a sahip olup her threaddaki lokal değişkenler o threada özeldir. Yine her threadın kendi özgül register setleri vardır böylece Windows diğer threadlara geçtiğinde kontrol tekrar kendilerine geçtiğinde son durumlarını hatırlayabilir ve görevlerini kaldığı yerden devam edebilirler . Bu içten Windows tarafından yönetilir.

İki kategoriye threadları bölebiliriz:

Kullanıcı ara yüz threadı: Bu tip thread kendi penceresini yarattığından pencere mesajlarını alabilir. Kendi penceresiyle kullanıcıya yanıt verebilir ismide buradan gelir.

İşçi thread: Bu tip thread bir pencere yaratmaz bundan dolayı herhangi bir pencere mesajı alamaz. Arkada özelikle tayin edilen işi yapmak için vardır. işçi thread ismide buradan gelir.

Multithread yeteneğini kullanırken şu stratejiyi tavsiye ederim: Birincil thread'ı ara yüz işlemlerini diğer threadlar arkada zor işleri yapsın.

Devamdaki sözdizimine sahip CreateThread fonksiyonunu çağırarak bir thread'ı yaratabiliriz:

HANDLE WINAPI CreateThread(
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in_opt LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out_opt LPDWORD lpThreadId
);


CreateThread fonksiyonu oldukça CreateProcess'e benzer.

lpThreadAttributes --> Eğer thread'ın varsayılan güvenlik düzeyine sahip olmasını istiyorsanız NULL kullanabilirsiniz.

dwStackSize --> Thread'ın stack boyutunu belirtir. Birincil thread ile aynı stack boyutunu istiyorsanız, bu paramterede NULL kullanılır.

lpStartAddress--> Thread fonksiyonunun adresi. Bu, threadın işini yapacak fonksiyondur(prosedür). Bu fonksiyon sadece ve sadece bir tane 32-bitlik parametre alır ve döndürür.

lpParameter --> Thread fonksiyonuna(prosedür) geçmeyi istediğiniz parametre.

dwCreationFlags --> 0’ın anlamı thread yaratıldıktan sonra hemen çalışmaya başlar. Aksi CREATE_SUSPENDED flagı.

lpThreadId --> CreateThread fonksiyonu yaratılan thread'ın thread ID'sini bu parametreye döndürür.

Eğer CreateThread çağrısı başarılıysa yaratılan threadın handleyini döndürür. Aksi halde, NULL döndürür.
Eğer dwCreationFlags'ta CREATE_SUSPENDED flagını belirtmediyseniz başarılı CreateThread çağırısından sonra thread fonksiyonu(prosedürü) hemen işlemeye başlar. Bu flagı belirttiyseniz, thread ResumeThread çağrılana kadar askıda kalır.
Thread fonksiyonunda(prosedür) return talimatıyla döndüğünüzde, Windows thread fonksiyonu için içten ExitThread fonksiyonunu çağırır. ExitThread'ı thread fonksiyonunuzda sizde çağırabilirsiniz ama bu çok önemli olmayan küçük bir nüans.
GetExitCodeThread fonksiyonunu çağırarak bir thread'ın exit kodunu elde edebilirsiniz.
Eğer bir threaddan başka bir threadı sonlamak isterseniz TerminateThread fonksiyonunu çağırabilirsiniz. Ama bu fonksiyon thread'a toplanma imkanı vermeden hemen sonladığına ekstrem durumlarda bu fonksiyonu kullanınız.

Şimdi threadlar arası iletişim metotlarına geçelim.

Üç tane var:
*Global değişken kullanımı
*Özel mesaj
*Olay Nesnesi(Event)


Threadlar process'in global değişkenlere ekli kaynaklarını paylaşır bundan dolayı threadlar diğerleriyle iletişimde global değişkenleri kullanabilir. Tabi bu metot dikkatli kullanılmalıdır. Thread sekronizasyonuna dikkat edilmeli. Misal, iki thread 10 üyeli aynı yapıyı kullanıyorsa, biri yapıda güncelleme işi ortasında Windows aniden thread'dan kontrolü aldığında ne olacak? Diğer thread yapıda güncellemenin yarıda kaldığı veriyle kala kalacaktır! Herhangi bir hata yapma, multithread programları götürmek ve debuglamak daha zor. Bu tür hatalarda iz sürmesi oldukça zor olup iş şansa kalır.

Yine threadlar arası iletişim için özel mesajlar kullanabilirsiniz. Eğer threadların hepsi kullanıcı ara yüz threadı ise hiç bir problem yok: Bu metot iki yönlü iletişim olarak kullanılabilir. Topu topu threadlara anlamlı bir veya fazlası özel mesaj tanımlaması yapmaktır. Baz değer olarak WM_USER mesajını kullanarak özel mesaj tanımlarsınız , şöyle onu tanımlayabilirsiniz:

#define WM_MYCUSTOMMSG WM_USER+100h

Windows kendi mesajları için WM_USER üstü hiçbir değeri kullanmaz bu yüzden kendi özel mesaj değerleriniz olarak WM_USER ve üstü değerleri kullanabilirsiniz.
Threadın biri kullanıcı ara yüz threadı ve diğeri işçi thread ise işçi thread kendi penceresine ve dolayısıyla bir mesaj kuyruğuna sahip olmadığından iki yönlü iletişim olarak bu metodu kullanamazsınız. Devamdaki düzeni kullanabilirsiniz:

Kullanıcı Ara yüz Threadı ------> global değişken(ler)----> İşçi thread
İşçi Thread ------> özel mesaj(lar) ----> Kullanıcı Ara yüz Threadı



Bizde örneğimizde bu metodu kullanacağız.


Örnek:

Örnek zip dosyasını indir ve thread1.exe'i çalıştır. "Hesapla" menü öğesini tıkla. Bu, 600,000,000 kere "++i" talimatı programca işlettirecek. Bu esnada ana pencereyle herhangi bir şey yapamayacağınıza dikkat: Onu hareket ettiremez, menüsünü aktif hale getiremezsiniz, çünkü o an hesaplama ile meşgul. Hesaplama tamamlandığında bir mesaj kutusu gösterilicek. Bundan sonra pencere normal olarak komutlarınıza cevap verebilir.
Kullanıcıyı bu sıkıntıdan kurtarmak için thread2.exe 'de ayrı bir işçi threada "hesaplama" rutini taşınıp birincil thread ile kullanıcı ara yüz işleri sürdürülecek. Ana pencere olağana kıyasla daha yavaş yanıt verse de bir şekilde yanıt verdiğini göreceksiniz.

Event
Biraz önce, threadların özel bir mesajla nasıl iletişim kurduğunu gösterdim. Diğer iki metodu atladım: global değişken ve olay nesnesi. Bu derste her ikisini de kullanacağız.
Bir olay nesnesi bir düğme gibidir: sadece iki duruma sahiptir: açık veya kapalı. Olay nesnesi açık konumda “sinyal” durumundadır. Kapalıyken “sinyal yok” durumundadır. Bir olay nesnesini yaratıp olay nesnesinin durumunu izlemek için ilgili threadlara bir kod parçası koyarsınız. Olay nesnesi sinyal yok durumundaysa threadlar uykuda bekler. Threadlar bekleme durumunda çok az CPU zamanı tüketirler.

Takip eden sözdizimine sahip CreateEvent fonksiyon çağrısıyla bir olay nesnesi yaratırsınız:

HANDLE WINAPI CreateEvent(
__in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,
__in BOOL bManualReset,
__in BOOL bInitialState,
__in_opt LPCTSTR lpName
);


lpEventAttribute--> NULL değerini belirtirseniz, olay nesnesi varsayılan güvenlik düzeyi ile yaratılır.

bManualReset--> WaitForSingleObject çağrısından sonra olay nesnesini otomatik sinyal yok durumuna Windows tarafından getirilmesini isterseniz bu parametrede FALSE’i belirtmelisiniz. Aksi halde kendiniz ResetEvent ‘ı çağırarak olay nesnesini resetlemesiniz.

bInitialState--> Olay nesnesini sinyal durumunda yaratılmasını isterseniz, bu parametrede TRUE belirtilir aksi halde olay nesnesi sinyal yok durumunda yaratılacaktır.

lpName --> Olay nesnesinin ismi olan bir ASCIIZ stringe pointer. Bu isim OpenEvent’i çağırmayı istediğinizde kullanılır.

Çağrı başarılıysa, yeni yaratılan olay nesnesinin handleyini döndürür aksi halde NULL döndürür.
İki API çağrısıyla bir olay nesnesinin durumunu değiştirebilirsiniz: SetEvent ve ResetEvent. SetEvent fonksiyonu olay nesnesini sinyal durumuna getirir. ResetEvent tersini yapar.
Olay nesnesi yaratıldığında, olay nesnesinin durumunu izlemek isteyen thread’a WaitForSingleObject çağrısını koymalısınız.

WaitForSingleObject devamdaki sözdizimine sahiptir:

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);



hObject --> Bir senkronize nesneye handle. Olay nesnesi bir senkronize nesnesi türüdür.

dwTimeout --> Bu fonksiyonun nesnenin sinyal durumuna geçmesini bekleyeceği milisaniye cinsinden zamanı belirtir. Belirtilen zaman dolarsa ve olay nesnesi hala sinyal yok durumunda ise WaitForSingleObject çağrıyı yapana döner. Nesneyi sonsuz süreli beklemek isterseniz, bu parametrede INFINITE sabit değerini belirtmelisiniz.


Örnek, kullanıcının menüden bir komut seçmesini bekleyen bir pencere gösterir. Kullanıcı "Thread'ı çalıştır" ‘ı seçerse, thread hesaplama işlemine başlar. Hesaplama bittiğinde, işin bittiğini kullanıcıya bildiren bir mesaj kutusu gösterilir. Thread çalıştığı esnada, kullanıcı thread ‘ı durdurmak için "stop thread" ‘ı seçebilir.


Örnekler ve küçük açıklamalar en kısada sürede eklenecek...