Derinlemesine Threading..(2)
// 20 Temmuz 2009 // Delphi, İşletim Sistemi, Programlama, Win32
Bir önceki makalemizde thread’ler konusuna giriş yapmış, işletim sisteminin thread’leri nasıl yönettiğini ve thread’lerin senkronizasyon mekanizmalarını anlatmaya çalışmıştık. Bu bağlamda Critical Section, Mutex, Semaphore senkronizasyon mekanizmalarını izah etmiştik. İlk makalemizde değinmediğimiz 2 adet senkronizasyon mekanizmasına da bu makalede temas etmeye çalışacağız. Event ve Waitable Timer adı verilen bu mekanizmalar da tıpkı diğer senkronizasyon mekanizmaları gibi çalışırlar. Bu iki sekonronizasyon mekanizması da WaitForSingleObject yada WaitForMultipleObjects vasıtası ile thread’lerimizin belirli kod bloklarında belirli bir şart sağlanana kadar beklemesi için vardırlar.
Hatırlayacağımız üzere, critical section’lar belirli bir kritik kod bloğuna aynı anda birden fazla thread’in girmesine müsaade etmiyordu. Mutex’ler ise critical section’lara son derece benzemelerine rağmen birden fazla uygulamanın(process) thread’lerinin de aynı kod bloklarına girişlerini senkronize ediyordu. Ardından temas ettiğimiz semaphore’ler ise biraz daha farklı bir yaklaşım ile kritik bir kod bloğuna bizim belirlediğimiz sayıda thread’in girmesini sağlıyordu.
Event mekanizmaları da yukarıda sayılan thread senkronizasyon mekanizmaları gibi çalışırlar. Ancak elbette kendine özgü tarafları da vardır. Programlarımızda hangi thread senkronizasyon mekanizmasını kullanacağımız tamamen ihtiyaçlarımız ile doğru orantılıdır. Birinin bir diğerine üstünlüğü gibi bir şey söz konusu değildir. Tüm bu bahsedilen mekanizmaların asıl amacı, işletim sistemindeki thread geçişlerinin 20 ms. olduğu bir ortamda veriye hatasız bir şekilde erişmek ve kullanabilmektir.
Event mekanizmaları, birden fazla thread’in ortaklaşa çalışması söz konusu olduğunda anlamlı olurlar. Bir thread’in bir diğer thread’i beklemesi gerektiğinde, kısaca bir ekip ruhunun gerektiği noktalarda bu mekanizma son derece kullanışlıdır. Eğer kodlarınızın bir ekip ruhu ile çalışması gerekiyor ise, aralarında bir imece söz konusu ise o halde bu mekanizmayı bilmeniz faydalı olacaktır.
Event mekanizmasını anlatabilmek adına, mübarek miraç kandili vesilesi ve ramazan ayının yaklaşıyor olması münasebeti ile Ramazan ayına has bir örnek tercih ettim. Bu örneğimiz; iftar saatine yakın bir zaman diliminde evimizde geçen olaylara ilişkin bir senaryoyu içeriyor.
Evde; anne, baba, çocuk ve bir misafirimiz var. Senaryomuz gereği; iftara yarım saat kala anne yemekleri ısıtmaya başlar. Yemeklerin ısınmasına başlanılması ile baba sofra kurma hazırlıklarına girişir. İftara 5 dk kala, evin küçük çocuğu pide almak üzere evden dışarı çıkar. Ezan’ın okunması ile tüm ev ahalisi iftarlarını açarlar.
Bu küçük ama ramazan ayında hemen hemen hepimizin yaşadığı senaryonun bilgisayara aktarılması sırasında thread’ler ve event senkronizasyon mekanizması kullanacağız. İftar saati gelene kadar tüm ev ahalisinin sohbet ettiğini yada televizyon seyrettiğini varsayıyoruz. İftar’a yarım saat kala anne thread bir event vasıtası ile tetikleniyor ve yemekleri ısıtmaya başlıyor. Ardından baba thread, anne thread tarafından sofrayı kurması için uyarılıyor. Anne ve baba thread’ler üstlerine düşen vazifeleri yaparlarken, diğer ev ahalisi hâla sohbet ve televizyon izleme ile iştigal ediyorlar.
İftara 5 dakika kala, çocuk thread’imiz evden pide almak üzere ayrılıyor. Misafirimiz bu arada televizyon izlemek ile meşgul. Çocuk pide almaktan gelince, baba sofrayı kurmuş, anne yemekleri ısıtmış durumda. Ve hep birlikte ezanın okunmasını bekliyorlar. Ezanın okunmasına müteakip; tüm ev ahalisi oruçlarını açıyorlar.
Senaryomuz hakkında özet bir bilgi verdikten sonra, event senkronizasyon mekanizmasında kullanılan metodların tanımlarına bir bakalım isterseniz;
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName ); HANDLE OpenEvent( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName ); BOOL SetEvent( HANDLE hEvent ); BOOL ResetEvent( HANDLE hEvent );
ve Delphi tanımlarımız:
function CreateEvent(lpEventAttributes: PSecurityAttributes; bManualReset, bInitialState: BOOL; lpName: PChar): THandle; stdcall; function OpenEvent(dwDesiredAccess: DWORD; bInheritHandle: BOOL; lpName: PChar): THandle; stdcall; function SetEvent(hEvent: THandle): BOOL; stdcall; function ResetEvent(hEvent: THandle): BOOL; stdcall;
Tanımlarından gördüğünüz gibi daha önce anlattığımız senkronizasyon nesneleri ile bir farklılıkları görülmüyor. Hatırladığınız üzere senkronizasyon nesnelerini bir kapıya benzetmiştik. Kapının açık yada kapalı olması durumları gibi senkronizasyon nesneleri de açık yada kapalı durumlarına sahiptirler. CreateEvent API fonksiyonunun ikinci parametresi olan bManualReset kapının açılma kapanma işinin otomatik mi yoksa bize mi bırakılacağının kararını içeriyor. Bu parametreye true geçilmesi, SetEvent metodu ile açık duruma geçen kapının ResetEvent ile kapatılmasını bizim kontrol etmemiz gerektiğini ifade ediyor. Bu parametreye false geçilmesi durumunda, kapının açık olduğunu görüp ilgili kod bloğunu geçen thread kapıyı ardından otomatikman kapatacak ve başka thread’ler kapının önünde beklemeye devam edeceklerdir. Biz örneğimizde bu parametrenin her iki durumunuda kullanacağız.
Tanımları verdiğimize ve bu tanımların diğer senkronizasyon mekanizmalarındaki tanımlarla büyük benzerlikler içerdiğini söylediğimize göre, artık kod örneğimize geçebiliriz:
(* Ramazan ayı senaryomuz ------------------------------------------------ Anne yarım saat kala yemekleri ısıtmaya başlasın Baba yarım saat kala sofrayı kurmaya yardım etsin Çocuk 5 dk kala pide almaya gitsin Misafir yan gelip yatsın Ezan okunsun Herkes orucunu açsın Anneye teşekkür edilsin(En önemli kısım bu, bunu yapmazsanız anne sizi Terminate eder.) *) TEvAhalisiThread = class(TThread) public constructor Create; procedure TvSeyret; procedure SohbetEt; procedure OrucunuAc; end; TAnneThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure YemekleriIsit; procedure Elhamdulillah; end; TBabaThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure SofrayiKur; procedure Elhamdulillah; end; TCocukThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure PideAl; procedure Elhamdulillah; end; TMisafirlerThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure Elhamdulillah; end; TZamanThread = class(TThread) private fIftarSaati : TTime; bAnneYemekleriIsit : Boolean; bPideAlmayaGit : Boolean; bEzanOkundu : Boolean; procedure EzanOkundu; protected procedure Execute; override; public constructor Create(const IftarSaati : TTime); end; var Form1: TForm1; Event_AnneYemekleriIsit, Event_CocukPideAlmayaGit, Event_BabaSofrayiKur, Event_EzanOkundu : THandle; ... ...
Yukarıdaki kod tanımlarımızda TAnneThread, TBabaThread, TCocukThread, TMisafirlerThread ve TZamanThread isimli sınıfları görüyorsunuz. Bu sınıflar birbirleri ile etkileşimde olan sınıflardır. Ve herbirisi TZamanThread’in vereceği komutlar ile kendilerine has görevlerini yapacaklardır. Şimdi arzu ederseniz kodumuza devam edelim, bakalım nasıl bir iletişim halindeler;
procedure TForm1.FormCreate(Sender: TObject); var Error : DWord; begin Event_AnneYemekleriIsit := OpenEvent(EVENT_ALL_ACCESS, false, PAnsiChar('Event Anne Yemekleri Isit')); if Event_AnneYemekleriIsit = 0 then begin Memo1.Lines.Add('Event bulunamadı, oluşturulacak.'); Event_AnneYemekleriIsit := CreateEvent(nil, false, false, PAnsiChar('Event Anne Yemekleri Isit')); Error := GetLastError(); if Error = ERROR_INVALID_HANDLE then Memo1.Lines.Add('Event Anne Yemekleri Isit ismi daha önce event harici başka bir senkronizasyon nesnesinde kullanılmış.!'); end else Memo1.Lines.Add('Event bulundu ve OpenEvent ile açıldı.'); Event_BabaSofrayiKur := OpenEvent(EVENT_ALL_ACCESS, false, PAnsiChar('Event Baba Sofrayı Kur')); if Event_BabaSofrayiKur = 0 then begin Memo1.Lines.Add('Event bulunamadı, oluşturulacak.'); Event_BabaSofrayiKur := CreateEvent(nil, false, false, PAnsiChar('Event Baba Sofrayı Kur')); Error := GetLastError(); if Error = ERROR_INVALID_HANDLE then Memo1.Lines.Add('Event Baba Sofrayı Kur ismi daha önce event harici başka bir senkronizasyon nesnesinde kullanılmış.!'); end else Memo1.Lines.Add('Event bulundu ve OpenEvent ile açıldı.'); Event_CocukPideAlmayaGit := OpenEvent(EVENT_ALL_ACCESS, false, PAnsiChar('Event Cocuk Pide Almaya Git')); if Event_CocukPideAlmayaGit = 0 then begin Memo1.Lines.Add('Event bulunamadı, oluşturulacak.'); Event_CocukPideAlmayaGit := CreateEvent(nil, false, false, PAnsiChar('Event Cocuk Pide Almaya Git')); Error := GetLastError(); if Error = ERROR_INVALID_HANDLE then Memo1.Lines.Add('Event Cocuk Pide Almaya Git ismi daha önce event harici başka bir senkronizasyon nesnesinde kullanılmış.!'); end else Memo1.Lines.Add('Event bulundu ve OpenEvent ile açıldı.'); Event_EzanOkundu := OpenEvent(EVENT_ALL_ACCESS, false, PAnsiChar('Event Ezan Okundu')); if Event_EzanOkundu = 0 then begin Memo1.Lines.Add('Event bulunamadı, oluşturulacak.'); Event_EzanOkundu := CreateEvent(nil, true, false, PAnsiChar('Event Ezan Okundu')); // Buraya dikkat, ikinci parametre True.! Error := GetLastError(); if Error = ERROR_INVALID_HANDLE then Memo1.Lines.Add('Event Ezan Okundu ismi daha önce event harici başka bir senkronizasyon nesnesinde kullanılmış.!'); end else Memo1.Lines.Add('Event bulundu ve OpenEvent ile açıldı.'); end; procedure TForm1.FormDestroy(Sender: TObject); begin CloseHandle(Event_AnneYemekleriIsit); CloseHandle(Event_BabaSofrayiKur); CloseHandle(Event_CocukPideAlmayaGit); CloseHandle(Event_EzanOkundu); end; { TEvAhalisiThread } constructor TEvAhalisiThread.Create; begin inherited Create(true); FreeOnTerminate := true; Resume; end; procedure TEvAhalisiThread.OrucunuAc; begin // Orucumuzu açalım.. end; procedure TEvAhalisiThread.SohbetEt; begin // Ahali ile sohbet et.. end; procedure TEvAhalisiThread.TvSeyret; begin // Arada TV seyret.. end; { TAnneThread } procedure TAnneThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TAnneThread.Elhamdulillah; begin fMessage := 'Anne: Afiyet olsun.'; Synchronize(Soyle); end; procedure TAnneThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Event_AnneYemekleriIsit, INFINITE); YemekleriIsit; WaitForSingleObject(Event_EzanOkundu, INFINITE); OrucunuAc; Sleep(400); Elhamdulillah; Terminate; end; end; procedure TAnneThread.YemekleriIsit; begin fMessage := 'Anne: Yemekleri ısıtmaya başladım.:' + TimeToStr(Time); Synchronize(Soyle); SetEvent(Event_BabaSofrayiKur); // Gerçek hayatta yemekleri ısıtmaya başlamış olan anne, babaya da sofrayı kurması yönünde ricada bulunur. end; { TBabaThread } procedure TBabaThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TBabaThread.Elhamdulillah; begin fMessage := 'Baba: Hanım eline sağlık.'; Synchronize(Soyle); end; procedure TBabaThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Event_BabaSofrayiKur, INFINITE); SofrayiKur; WaitForSingleObject(Event_EzanOkundu, INFINITE); OrucunuAc; Sleep(100); Elhamdulillah; Terminate; end; end; procedure TBabaThread.SofrayiKur; begin fMessage := 'Baba: Bende sofrayı kurmaya başladım:' + TimeToStr(Time); Synchronize(Soyle); end; { TCocukThread } procedure TCocukThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TCocukThread.Elhamdulillah; begin fMessage := 'Çocuk: Anneciğim ellerine sağlık'; Synchronize(Soyle); end; procedure TCocukThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Event_CocukPideAlmayaGit, INFINITE); PideAl; WaitForSingleObject(Event_EzanOkundu, INFINITE); OrucunuAc; Sleep(200); Elhamdulillah; Terminate; end; end; procedure TCocukThread.PideAl; begin fMessage := 'Çocuk: Anne ben pide almaya gidiyorum.:' + TimeToStr(Time); Synchronize(Soyle); end; { TMisafirlerThread } procedure TMisafirlerThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TMisafirlerThread.Elhamdulillah; begin fMessage := 'Misafir: Yenge ellerine sağlık nefis olmuş'; Synchronize(Soyle); end; procedure TMisafirlerThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Event_EzanOkundu, INFINITE); OrucunuAc; Sleep(300); Elhamdulillah; Terminate; end; end; { TZamanThread } procedure TZamanThread.EzanOkundu; begin form1.Memo1.Lines.Add('Ezan okundu:' + TimeToStr(Time)); end; constructor TZamanThread.Create(const IftarSaati: TTime); begin inherited Create(true); // Bekler vaziyette oluştur.. FreeOnTerminate := true; fIftarSaati := IftarSaati; bAnneYemekleriIsit := false; bPideAlmayaGit := false; bEzanOkundu := false; Resume; end; procedure TZamanThread.Execute; var CurrentTime : TTime; begin inherited; while not Terminated do begin CurrentTime := Time; if (IncMinute(CurrentTime, 30) >= fIftarSaati) and (not bAnneYemekleriIsit) then // O anki zamana 30 dk eklenince iftar zamanını geçiyor ise.. begin SetEvent(Event_AnneYemekleriIsit); bAnneYemekleriIsit := true; end; If (IncMinute(CurrentTime, 5) >= fIftarSaati) and (not bPideAlmayaGit) then begin SetEvent(Event_CocukPideAlmayaGit); bPideAlmayaGit := true; end; if CurrentTime >= fIftarSaati then begin SetEvent(Event_EzanOkundu); bEzanOkundu := true; Synchronize(EzanOkundu); end; if not bEzanOkundu then Sleep(1000) // 1 saniyede bir kontrol edelim.. else Terminate; end; end; procedure TForm1.btnEventClick(Sender: TObject); var thrdZaman : TZamanThread; thrdAnne : TAnneThread; thrdBaba : TBabaThread; thrdCocuk : TCocukThread; thrdMisafir : TMisafirlerThread; begin // edtIftarSaati TEdit türünde bir component'tir ve içine girilen değer; 18:30:00 gibidir.. thrdZaman := TZamanThread.Create(StrToTime(edtIftarSaati.Text)); thrdAnne := TAnneThread.Create; thrdBaba := TBabaThread.Create; thrdCocuk := TCocukThread.Create; thrdMisafir := TMisafirlerThread.Create; end;
Yukarıdaki örneğimizde oluşturulan 5 ayrı thread gözlemliyorsunuz. Bu thread’lerden TZamanThread herşeyi kontrol eden thread’imizdir. Diğer thread’ler tanımlı olan event senkronizasyon nesnelerini bekleyerek kendilerine has işlemleri gerçekleştirirler. TZamanThread sınıfımızın Execute metoduna baktığımızda, kendisine geçilen iftar saati bilgisini sürekli kontrol ettiğini görürürüz. İftar saatine yarım saat kalması durumunda, SetEvent API’si yardımı ile “Event_AnneYemekleriIsit” event değişkenimizi tetiklemiş olur(Kapıyı açar).
Bu tetiklemeye kadar TAnneThread, Execute metodu içindeki WaitForSingleObject(Event_AnneYemekleriIsit, INFINITE); satırı vasıtası ile bekler durumdadır. Event nesnesinin set edilmesine müteakip, bu satırın altına geçebilmiştir(Yemekleri ısıtmaya başlamıştır). Yemekleri ısıtmasının bitmesinden sonra WaitForSingleObject(Event_EzanOkundu, INFINITE); kodu ile ezanın okunmasını beklemeye başlamıştır.
TBabaThread ise Execute metodunda, “Event_BabaSofrayiKur” isimli senkronizasyon nesnesini beklemektedir. TAnneThread.YemekleriIsit metodu içindeki SetEvent ile baba thread’de beklemeyi bırakmış ve sofrayı kurma görevine başlamıştır. Ardından o da tıpkı anne thread’de olduğu gibi ezanın okunmasını bekleyecektir.
TZamanThread iftar’a 5 dakika kala, “Event_CocukPideAlmayaGit” isimli senkronizasyon nesnesini tetikleyecektir. Bu sayede TCocukThread beklemekte olduğu emri alır ve pide almak üzere yola koyulur. Pideleri alıp geri geldiğinde o da diğerleri gibi ezanın okunması için beklemeye başlar.
Bu arada TMisafirThread’imizin Execute metodunda kendisine biçilmiş herhangi bir görevi olmadığını, sadece ezanın okunmasını beklediğini görebilirsiniz. Biz Türk milletinin hasletinde olduğu gibi misafirperverliğimiz sanal dünyada thread’ler içinde dahi devam etmelidir
Nihayet ezan vakti geldiğinde TZamanThread’imiz SetEvent(Event_EzanOkundu); API’si yardımı ile event nesnesini tetikleyecek ve bu event’i bekleyen anne, baba, çocuk ve misafir thread’ler bir sonraki kod bloğuna yani OrucunuAc‘a geçebileceklerdir. Ardından zaman thread’imiz Terminate kodunu çağırarak TThread sınıfının Terminated property’sinin true olmasını sağlayacak ve sonsuz döngüden çıkacak, yani threadimiz sonlanacaktır.
Yukarıdaki kod örneğimizde “Event_EzanOkundu” olayının CreateEvent ile oluşturulması koduna bir dikkat ! uyarısı yazmıştım. Hatırlarsanız CreateEvent’in ikinci parametresi bizim event senkronizasyon nesnesinin açık/kapalı olma durumunu otomatik’mi yoksa manuel’mi yapacağımıza karar veren parametre idi. Biz ezan okunması için manuel ayarı seçtik. Bunun sebebi, bu event senkronizasyon nesnesini bekleyen birden fazla thread’in olması ve bizim bu thread’lerin hepsinin birden bekler durumdan kurtulmasını istememizdi.
Eğer parametremizi true değilde false olarak geçse idik, SetEvent ile tetiklenen ezan senkronizasyon nesnesi, tetiklenir tetiklenmez anında tekrar kapalı duruma geçecekti ve dolayısı ile sadece bir thread bekler durumdan kurtulabilecek diğerleri hâla bekler durumda kalacaktı. Halk dili ile, sadece bir kişi orucunu açacak, diğerleri ona bakacak ve kıyamet bundan kopacaktı
Bizde kıyametin kopmasını istemediğimiz için event senkronizasyon nesnesinin açık/kapalı olması durumunu otomatiğe bağlamadık, biz kontrol edeceğiz dedik. Ve açık duruma geldikten sonra, herhangi bir yerde ResetEvent metodunu çağırmadığımız için durum açık olarak kaldı.
Örneğimizi denediğinizde, göreceğiniz çıktı;
gibi olacaktır. Gördüğünüz gibi, Event senkronizasyon mekanizması belirli bir olayı beklemeye dayalıdır. Olayın gerçeklenme koşulunu sizler yönetirsiniz. Kimi zaman bu bir tuşa basma ile gerçekleşir, kimi zaman bir button yada check box’a bastırırsınız yada bizim örneğimizde olduğu gibi bir TZamanThread ile olayları kontrol edersiniz. Bu tamamen sizin ihtiyaçlarınız ile doğru orantılıdır.
Event senkronizasyon mekanizmasını az çok anlattıktan sonra, bir diğer mekanizmamız olan Waitable Timer kavramına göz gezdirebiliriz. Adından da anlaşılabileceği üzere bu bir timer senkronizasyon nesnedir. Ancak, normal timer’lar gibi değildir. Normal timer olaylarında, işletim sisteminin bir pencereye ihtiyacı vardır. Bu pencereye belirli periotlarda WM_TIMER mesajları gönderilir. Oysa bu mekanizma için herhangi bir penceresel denetime ihtiyaç yoktur ve timer nesnelerinden çok daha hassastır. İşletim sistemindeki timer nesneleri mesaj tabanlı oldukları için bir pencereye ihtiyaç duyarlar ve hassasiyetleri yaklaşık olarak 40-50 milisaniyeler civarındadır. Daha kısa zaman aralıklarına sahip işlemler yapmayı arzu ediyor iseniz o zaman WaitableTimer’ları kullanabilirsiniz. Çünkü Waitable Timer’lar 100 nano saniye hassasiyete sahiptir.
Özet ile diğer senkronizasyon mekanizmalarında olduğu gibi çalışırlar. Sizin vereceğiniz zaman dilimi geçildiğinde senkronizasyon nesnesini açar yada kapatırlar. Hatırlayacağınız üzere yukarıdaki örneğimizde biz bu işi TZamanThread ile yapıyorduk. Şimdi yukarıdaki örneğimizi, Waitable Timer senkronizasyon nesnesine göre yeniden yazacağız. Ancak öncelikle herzamanki gibi tanımlarımıza bir göz gezdirelim:
HANDLE CreateWaitableTimer( LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCTSTR lpTimerName ); HANDLE OpenWaitableTimer( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpTimerName ); BOOL SetWaitableTimer( HANDLE hTimer, const LARGE_INTEGER *pDueTime, LONG lPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, BOOL fResume );
function CreateWaitableTimer(lpTimerAttributes: PSecurityAttributes; bManualReset: BOOL; lpTimerName: PChar): THandle; stdcall; function OpenWaitableTimer(dwDesiredAccess: DWORD; bInheritHandle: BOOL; lpTimerName: PChar): THandle; stdcall; function SetWaitableTimer(hTimer: THandle; var lpDueTime: TLargeInteger; lPeriod: Longint; pfnCompletionRoutine: TFNTimerAPCRoutine; lpArgToCompletionRoutine: Pointer; fResume: BOOL): BOOL; stdcall;
Görüldüğü gibi bu senkronizasyon mekanizmasının metodlarının biraz daha farklı parametreleri var. CreateWaitableTimer yada OpenWaitableTimer metodları diğer senkronizasyon mekanizmalarında olduğu gibi benzerlikler arz ediyor. Lâkin, SetWaitableTimer şimdiye dek gördüklerimizden farklı bir yapıya sahip. Waitable Timer’lar tetikleme zamanı için geçeceğiniz zaman bilgisinin TSystemTime, TFileTime türleri arasındaki geçişleri ile ilgili bazı tür dönüşümleri gerektirirler. Gelin kullanımlarına geçmeden evvel ihtiyaç duyacağımız parametre tiplerine yakından bakalım;
PLargeInteger = ^TLargeInteger; _LARGE_INTEGER = record case Integer of 0: ( LowPart: DWORD; HighPart: Longint); 1: ( QuadPart: LONGLONG); end; TLargeInteger = Int64; LARGE_INTEGER = _LARGE_INTEGER; PSystemTime = ^TSystemTime; _SYSTEMTIME = record wYear: Word; wMonth: Word; wDayOfWeek: Word; wDay: Word; wHour: Word; wMinute: Word; wSecond: Word; wMilliseconds: Word; end; TSystemTime = _SYSTEMTIME; SYSTEMTIME = _SYSTEMTIME; PFileTime = ^TFileTime; _FILETIME = record dwLowDateTime: DWORD; dwHighDateTime: DWORD; end; TFileTime = _FILETIME; FILETIME = _FILETIME;
Bir Waitable Timer ‘a zaman bilgisini geçebilmemiz için öncelikle TSystemTime türündeki record değişkenimizin içerisine gerekli bilgileri yazmalı, ardından SystemTimeToFileTime API’si ile gerekli dönüşümü sağlamalı, LocalFileTimeToFileTime ile bir dönüşüm daha sağladıktan sonra TLargeInteger türündeki parametremizin yüksek ve düşük word’lerine gereken atamaları yapmalıyız. Karmaşık gibi görünen bu anlatım aslında göründüğü kadar karmaşık değil. Bunu yazacağımız kodda sizlerde gözlemleyebileceksiniz. Ancak kodlara geçmeden evvel teorik bilgileri vermeye devam etmekte fayda olduğu kanaatindeyim.
SetWaitableTimer’ın üçüncü parametresi Waitable timer’ın ne kadar zamanda bir çalışacağını ifade eder. Biz bu değere 1000 geçerek 1 saniyede bir kontrol etmesini isteyeceğiz. Dördüncü ve beşinci parametreler, waitable timer senkronizasyon mekanizmasının belirtilen zamana eriştiğinde çağırmasını istediğiniz bir metod var ise anlamlı olacaktır. Dördüncü parametrenin Delphi’deki tanım şekli aşağıdaki gibi olacaktır:
procedure (lpArgToCompletionRoutine: Pointer; dwTimerLowValue, dwTimerHighValue: DWORD); stdcall;
Beşinci parametre ise herhangi bir geçerli pointer adresi olabilir. Ancak biz örneğimizde bu iki parametreyi de nil olarak geçeceğiz. Çünkü APC(Asynchronous Procedure Call) adı verilen tekniğin detaylarına girmek istemiyorum, bunu sizin araştırmalarınıza bırakıyorum. Yalnız bir ipucu olarak, APC çağrılarının uygulama kuyruğuna eklendiğini ve bu çağrıları yakalayabilmek için SetWaitableTimer API’sini çağıran thread’de SleepEx(INFINITE, true) ile beklemeniz gerektiğini söyleyebilirim. Bu beklemeyi yapmadığınız zaman APC çağrılarını yakalayamazsınız.
Örneğimize geçmeden evvel OpenWaitableTimer için gereken bazı sabitlerin Delphi’de tanımlı olmadıklarını hatırlatmak ve bu tanımları sizlerle paylaşmak isterim;
const TIMER_QUERY_STATE = $0001; TIMER_MODIFY_STATE = $0002; TIMER_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED or SYNCHRONIZE or TIMER_QUERY_STATE or TIMER_MODIFY_STATE;
Evet, tüm bu sıkıcı teorisel bilgilerin ardından isterseniz iftar ile ilgili örneğimizi, Waitable Timer senkronizasyon mekanizmasını kullanarak ve ana yapımızı pek de değiştirmeden nasıl kodlayabileceğimize yakinen bakalım:
type TEvAhalisiThread = class(TThread) public constructor Create; procedure TvSeyret; procedure SohbetEt; procedure OrucunuAc; end; TAnneThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure YemekleriIsit; procedure Elhamdulillah; end; TBabaThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure SofrayiKur; procedure Elhamdulillah; end; TCocukThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure PideAl; procedure Elhamdulillah; end; TMisafirlerThread = class(TEvAhalisiThread) private fMessage : String; procedure Soyle; protected procedure Execute; override; public procedure Elhamdulillah; end; var Form1: TForm1; Waitable_AnneYemekleriIsit, Waitable_CocukPideAlmayaGit, Waitable_EzanOkundu : THandle; implementation procedure TForm1.FormCreate(Sender: TObject); var Error : DWord; begin Waitable_AnneYemekleriIsit := OpenWaitableTimer(TIMER_ALL_ACCESS, false, PAnsiChar('Waitable Anne Yemekleri Isit')); if Waitable_AnneYemekleriIsit = 0 then begin Memo1.Lines.Add('Waitable Timer bulunamadı, oluşturulacak.'); Waitable_AnneYemekleriIsit := CreateWaitableTimer(nil, false, PAnsiChar('Waitable Anne Yemekleri Isit')); Error := GetLastError(); if Error = ERROR_INVALID_HANDLE then Memo1.Lines.Add('Waitable Anne Yemekleri Isit ismi daha önce Waitable Timer harici başka bir senkronizasyon nesnesinde kullanılmış.!'); end else Memo1.Lines.Add('Waitable Timer bulundu ve OpenWaitableTimer ile açıldı.'); Waitable_CocukPideAlmayaGit := OpenWaitableTimer(TIMER_ALL_ACCESS, false, PAnsiChar('Waitable Cocuk Pide Almaya Git')); if Waitable_CocukPideAlmayaGit = 0 then begin Memo1.Lines.Add('Waitable Timer bulunamadı, oluşturulacak.'); Waitable_CocukPideAlmayaGit := CreateWaitableTimer(nil, false, PAnsiChar('Waitable Cocuk Pide Almaya Git')); Error := GetLastError(); if Error = ERROR_INVALID_HANDLE then Memo1.Lines.Add('Waitable Cocuk Pide Almaya Git ismi daha önce Waitable Timer harici başka bir senkronizasyon nesnesinde kullanılmış.!'); end else Memo1.Lines.Add('Waitable Timer bulundu ve OpenWaitableTimer ile açıldı.'); Waitable_EzanOkundu := OpenWaitableTimer(TIMER_ALL_ACCESS, false, PAnsiChar('Waitable Ezan Okundu')); if Waitable_EzanOkundu = 0 then begin Memo1.Lines.Add('Waitable Timer bulunamadı, oluşturulacak.'); Waitable_EzanOkundu := CreateWaitableTimer(nil, true, PAnsiChar('Waitable Ezan Okundu')); // İkinci parametreye dikkat, eventlerdeki açıklamamızı hatırlayınız. Error := GetLastError(); if Error = ERROR_INVALID_HANDLE then Memo1.Lines.Add('Waitable Ezan Okundu ismi daha önce Waitable Timer harici başka bir senkronizasyon nesnesinde kullanılmış.!'); end else Memo1.Lines.Add('Waitable Timer bulundu ve OpenWaitableTimer ile açıldı.'); end; procedure TForm1.FormDestroy(Sender: TObject); begin CloseHandle(Waitable_AnneYemekleriIsit); CloseHandle(Waitable_CocukPideAlmayaGit); CloseHandle(Waitable_EzanOkundu); end; { TEvAhalisiThread } constructor TEvAhalisiThread.Create; begin inherited Create(true); FreeOnTerminate := true; Resume; end; procedure TEvAhalisiThread.OrucunuAc; begin // Orucumuzu açalım.. end; procedure TEvAhalisiThread.SohbetEt; begin // Ahali ile sohbet et.. end; procedure TEvAhalisiThread.TvSeyret; begin // Arada TV seyret.. end; { TAnneThread } procedure TAnneThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TAnneThread.Elhamdulillah; begin fMessage := 'Anne: Afiyet olsun.'; Synchronize(Soyle); end; procedure TAnneThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Waitable_AnneYemekleriIsit, INFINITE); YemekleriIsit; WaitForSingleObject(Waitable_EzanOkundu, INFINITE); OrucunuAc; Sleep(400); Elhamdulillah; Terminate; end; end; procedure TAnneThread.YemekleriIsit; begin fMessage := 'Anne: Yemekleri ısıtmaya başladım.:' + TimeToStr(Time); Synchronize(Soyle); end; { TBabaThread } procedure TBabaThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TBabaThread.Elhamdulillah; begin fMessage := 'Baba: Hanım eline sağlık.'; Synchronize(Soyle); end; procedure TBabaThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Waitable_AnneYemekleriIsit, INFINITE); SofrayiKur; WaitForSingleObject(Waitable_EzanOkundu, INFINITE); OrucunuAc; Sleep(100); Elhamdulillah; Terminate; end; end; procedure TBabaThread.SofrayiKur; begin fMessage := 'Baba: Bende sofrayı kurmaya başladım:' + TimeToStr(Time); Synchronize(Soyle); end; { TCocukThread } procedure TCocukThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TCocukThread.Elhamdulillah; begin fMessage := 'Çocuk: Anneciğim ellerine sağlık'; Synchronize(Soyle); end; procedure TCocukThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Waitable_CocukPideAlmayaGit, INFINITE); PideAl; WaitForSingleObject(Waitable_EzanOkundu, INFINITE); OrucunuAc; Sleep(200); Elhamdulillah; Terminate; end; end; procedure TCocukThread.PideAl; begin fMessage := 'Çocuk: Anne ben pide almaya gidiyorum.:' + TimeToStr(Time); Synchronize(Soyle); end; { TMisafirlerThread } procedure TMisafirlerThread.Soyle; begin form1.Memo1.Lines.Add(fMessage); end; procedure TMisafirlerThread.Elhamdulillah; begin fMessage := 'Misafir: Yenge ellerine sağlık nefis olmuş'; Synchronize(Soyle); end; procedure TMisafirlerThread.Execute; begin inherited; while not Terminated do begin TvSeyret; SohbetEt; WaitForSingleObject(Waitable_EzanOkundu, INFINITE); OrucunuAc; Sleep(300); Elhamdulillah; Terminate; end; end; procedure TForm1.btnWaitableTimerClick(Sender: TObject); type TInt64Rec = record Lo : DWord; Hi : DWord; end; var SysTime : TSystemTime; ft, ftResult: TFileTime; li : Int64; IftarZamani : TTime; wGun, wAy, wYil, wSaat, wDakika, wSaniye, wMiliSaniye : Word; thrdAnne : TAnneThread; thrdBaba : TBabaThread; thrdCocuk : TCocukThread; thrdMisafir : TMisafirlerThread; begin IftarZamani := StrToTime(edtIftarSaati.Text); DecodeDate(Date, wYil, wAy, wGun); DecodeTime(IftarZamani, wSaat, wDakika, wSaniye, wMiliSaniye); SysTime.wYear := wYil; SysTime.wMonth := wAy; SysTime.wDayOfWeek := 0; SysTime.wDay := wGun; SysTime.wHour := wSaat; SysTime.wMinute := wDakika; SysTime.wSecond := wSaniye; SysTime.wMilliseconds := 0; SystemTimeToFileTime(SysTime, ft); LocalFileTimeToFileTime(ft, ftResult); TInt64Rec(li).Lo := ftResult.dwLowDateTime; TInt64Rec(li).Hi := ftResult.dwHighDateTime; SetWaitableTimer(Waitable_EzanOkundu, li, 1000, nil, nil, false); DecodeTime(IncMinute(IftarZamani, -30), wSaat, wDakika, wSaniye, wMiliSaniye); // Annenin yemekleri ısıtacağı zaman.. SysTime.wHour := wSaat; SysTime.wMinute := wDakika; SysTime.wSecond := wSaniye; SystemTimeToFileTime(SysTime, ft); LocalFileTimeToFileTime(ft, ftResult); TInt64Rec(li).Lo := ftResult.dwLowDateTime; TInt64Rec(li).Hi := ftResult.dwHighDateTime; SetWaitableTimer(Waitable_AnneYemekleriIsit, li, 1000, nil, nil, false); DecodeTime(IncMinute(IftarZamani, -5), wSaat, wDakika, wSaniye, wMiliSaniye); // Çocuğun pide almaya gideceği zaman.. SysTime.wHour := wSaat; SysTime.wMinute := wDakika; SysTime.wSecond := wSaniye; SystemTimeToFileTime(SysTime, ft); LocalFileTimeToFileTime(ft, ftResult); TInt64Rec(li).Lo := ftResult.dwLowDateTime; TInt64Rec(li).Hi := ftResult.dwHighDateTime; SetWaitableTimer(Waitable_CocukPideAlmayaGit, li, 1000, nil, nil, false); thrdAnne := TAnneThread.Create; thrdBaba := TBabaThread.Create; thrdCocuk := TCocukThread.Create; thrdMisafir := TMisafirlerThread.Create; end;
Evet hepsi bu kadar. Gördüğünüz gibi diğerlerine nazaran tek zorluğu SetWaitableTimer API’sinin parametrelerinde yaşadık. Geri kalan kullanım diğer senkronizasyon nesnelerinde izah ettiğimiz gibi gerçekleşiyor. Bu örneğimizde zamanlamayı kontrol etmek maksadı ile daha evvel kullandığımız TZamanThread sınıfına ihtiyacımız olmadı. Çünkü zamanlama bilgisini Waitable Timer’lar ile sağlıyoruz. Belirttiğimiz zaman geldiğinde, Waitable Timer senkronizasyon nesnesi otomatikman açık duruma geçecek, dolayısı ile bu senkronizasyon nesnelerini WaitForSingleObject yada WaitForMultipleObjects ile bekleyen thread’lerimiz çalışmalarına kaldıkları yerden devam edebilme şansını elde edebileceklerdir.
Bu konularla ilgili herhangi bir görüş, düşünce yada öneriniz var ise yorum olarak paylaşmanızdan bilhassa memnuniyet duyarım. Bundan sonra yazacağım küçük makale bir thread’in TerminateThread kullanılmadan düzgün bir şekilde nasıl kapatılacağına yönelik olacaktır.
Bir sonraki makalede görüşmek ümidi ile hoşçakalın..
Böyle önemli bir konu, böyle güncel bir örnekleme ile ancak bu kadar güzel anlatılabilirdi !
Çok güzel bir kandil hediyesi olmuş
Sağolasın Tuğrul Hocam…
Oldukça öğretici ve espirili bir anlatım olmuş hocam. Benim tek önerim, o kadar “Event” anlatımından sonra “Evet” kelimesini hiç kullanmasaydın olabilir sanırım
Zira evet leri ivınt olarak okumak oldukça komik oluyor 
Ellerine sağlık…
Sizler sağolun arkadaşlar, bu konular anlaşılması biraz zor konular olduğu için anlatması da biraz zor oluyor. Mümkün mertebe anlaşılır örneklere başvurmak icap ediyor. Örneğimi beğendi iseniz yeterince açık anlatabilmişim demektir.
Tugrul Bey Merhaba
Thread lerle ilgili makalelerinizi okudum.Çok teşekkür ederim.Benim için çok faydalı bilgiler içeriyor.
Konu ile ilgili bir sorunumuzu sizinle paylaşmak istiyoruz.
Paralel Çalışan Multithread bir uygulamada threadler içinde new() komutu ile pointerlar oıluşturmamız gerekiyor ancak bu aşamada threadler birbirini bekliyor ve normalin(singlethread) çok altında bir zamanlama ile işlem tamamlanıyor.Bu sorunu nasıl aşabiliriz.
Bir thread içinde yapılan memory allocation’ın bir diğer thread’i bekletmesi yada duraklatması söz konusu değildir. Sorun memory allocation olmamalı kanaatimce. Detaylandırabilirseniz daha fazla yardımcı olabilirim sanırım.
Tuğrul Bey; öncelikle , ilginiz için çok Teşekkür Ediyoruz.
Sorunumuzun delphi kodu aşagıdaki gibidir.
type th=class(tthread)
private
protected
procedure execute;override;
public
constructor create;
end;
type
TForm1 = class(TForm)
SpeedButton1: TSpeedButton;
procedure SpeedButton1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
t1:array of th;
implementation
{$R *.dfm}
procedure th.execute;
var i,j,k:integer;
x:pinteger;
begin
for i:=0 to 1000000000 do begin
j:=round(sqrt(i));
new(x);dispose(x);//SORUNLU SATIR
end;
Terminate;
end;
constructor th.create;
begin
inherited create(true);
end;
procedure TForm1.SpeedButton1Click(Sender: TObject);
var i:integer;
begin
for i:=0 to high(t1) do begin
t1[i]:=th.create
end;
for i:=0 to high(t1) do begin
t1[i].Resume;
end;
for i:=0 to high(t1) do begin
t1[i].WaitFor;
end;
for i:=0 to high(t1) do begin
t1[i].Free;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
var i:integer;
begin
SetLength(t1,4);
end;
Göndermiş olduğunuz kodda bir aksaklık yok gibi görünüyor. Siz 1 milyar kere mem. allocation yapmışsınız. Pointer’ların memory’de 4 byte yer kapladığı düşünülürse bu da 4 milyar byte’lık bir alan rezerve edip boşalttığınız anlamını taşıyor. Ancak memory allocation işlemine müteakip hemen hafızayı boşalttığınız için bu da bir sorun olmaz. Bütün threadlerinizi Resume ile başlattığınız için, WaitFor çağrımının bitmesi 1 adet thread’in bitmesi kadar zaman alacaktır.
Örneğin; threadleriniz içinde 5 sn. gibi bir bekleme yapmış olsa idiniz, tüm threadler aynı anda paralel işletileceği için toplam bekleme zamanınız 5 sn + bir kaç milisaniye cinsinden olurdu. Bu bağlamda; ben hâla memory allocation’da ne gibi spesifik bir sorunla karşı karşıyasınız anlayamadım. Sizin örneğinize benzer bir örnek geliştirmiş olmama rağmen; paralel execution’ın normal bir şekilde gerçekleştiğini ve memory allocation/deallocation bloklarının sağlıklı çalıştığını gözlemledim.
Karşılaştığınız sorun hakkında daha açıklayıcı bir malümat verebilir ve anlamama yardımcı olursanız, mümkün mertebe faydalı olmaya gayret ederim.
Merhaba tuğrul bey;
Göndermiş oldugum örnek kodda new() ;dispose() satırı
iptal edildiginde işlemcilerin performası (quad core ) %100 e ulaşıyor
Sorunlu satır aktif edildiğinde toplam işlemci performası %15 e düşüyor.yani tek çekirdeği bile düzgün kullanamıyor.
Benim Tesbitim bu konuda şu şekilde;
memory allaoction yetkisi sadece Mainthread e verilmiş (windows tarafından) dolayısı ile memory bloku üzerinde talebi olan threadler bu hizmeti mainthreadden alıyor. ve hafızayı alan geri dönüyor.
aynı andaki talepleri ise mainthread in bile kafasını karıştırıyor ve normal tek çekirdek performası bile düşüyor.
Araştırmalarım neticesi bu konunun sadece bizim sorunumuz olmadığını gördüm.
Bu yüzden İntel bu konuda bir yazılım geliştirmiş
Multithread uygulamalar için (ücretli bir yazılım TBB) sanırım onu deneyeceğiz.
Bu Ancak Böyle Anlatılabilirdi. Tebrik ve Teşekkür Ederim.
Tuğrul hocam çok güzel bir makale,elinize,klavyenize sağlık.