Derinlemesine Threading..(1)

// 5 Temmuz 2009 // Delphi, İşletim Sistemi, Win32

Hâla programlama aleminde threading uzak kalınası bir mesele gibi gözlemleniyor. Aslında belli başlı kurallar bilindiğinde bu konunun korkulacak bir konu değil ancak dikkatle ele alınması gereken bir konu olduğu anlaşılacaktır. Genellikle thread dediğimiz mekanizmayı yazdığımız kodların paralel çalışabilmesi için kullanırız. Ancak kullandığımız işletim sistemleri gerçek mânada multi-threaded olmadıkları için hepimizin başına bir ton sorun gelmiştir ve thread’lere lanetler okumuşuzdur. Dolayısı ile onlardan uzak kalmaya mümkün mertebe gayret etmişizdir. Bu makalede amacım, siz değerli okuyucularımı bu mekanizmaya yaklaştırmak, varsa çekincelerinizi bilgim dahilinde gidermeye çalışmak olacaktır.

Thread’lerle düzgün şekilde çalışabilmek için öncelikle çok derin olmasa da orta seviyede işletim sistemi çalışma mekanizmasına aşina olmalıyız. Bu bağlamda işletim sistemleri üzerinde nispeten az bilinen hususlara değinmeye çalışacağım.

İyi bir programcı, üzerinde çalıştığı işletim sistemini tanıyan, onun neleri yapabileceğini ve neleri yapamayacağını bilen programcıdır. Bu öngörü, her zaman düşük seviyeli işletim sistemi bilgilerine müracaat etmeniz gerektiği anlamına gelmez. Yüksek seviye bir dil de kullanabiliyor olabilirsiniz, ancak her zaman olayın arka planında neler olduğunu bilen bir programcı diğerlerine oranla basamaklarca önde olacaktır. Tercih tamamen sizlere aittir. “Çok derin bilgilere girmem, bildiklerim bana yeter, ekmeğimi kazanırım keyfime bakarım” düşüncesinde iseniz, bildiklerinizin size yetmeyeceği gerçeği ile birgün karşılaşacağınızı üzülerek ifade etmek zorunda kalacağım. Her geçen gün sürekli değişen bir bilişim dünyasında kalıcı olabilmeniz; temel seviye bilgilerinizin yüksekliği ile doğrudan orantılıdır.

Yüzyüze karşılaştığım, yada bir şekilde derinlemesine programlama alemi ile ilgili muhabbete girdiğim arkadaşlarıma verdiğim tavsiyeler hep bu yönde olmuştur. Benim kişisel kanım, iyi bir programcının her zaman üzerinde çalıştığı sistem hakkında bilgiye haiz olması üzerinedir. Hemen hemen her programcı, günümüz popüler dillerinde nesneye yönelik programlamayı, pointer’ları ve daha pek çok low level teknolojiyi farkında olarak yada olmayarak kullanmaktadır. Dileğim odur ki, sizler farkında olanların safında olun. Bu farkındalık, sadece şahsınıza değil; şahsınız nezdinde pek çok insana ve hatta ülkenize de faydanız olacağı mânasına gelir.

Blog’umuz genel mânada Delphi üzerine olduğu için vereceğim örnekler Delphi üzerine olacaktır ama bu söylemler programlama ile uğraşan herkesi ilgilendirir. Delphi’ye yeni başlayan birisi dahi olsanız, farkında olmadan pointer’ları kullanıyor, nesneye yönelik programlama dünyasının avantajlarından sonuna kadar istifade ediyor, design pattern denilen popüler programlama konseptlerini programlarınızda kullanıyorsunuz demektir. Delphi, bu derin bilgi gerektiren hususların kullanım zorluklarını, kullanıcılarından gizleme konusunda son derece maharetlidir. Makalemde ilerlemeden evvel, her programcı arkadaşıma Delphi’nin kaynak kodlarında dolaşmasını öneriyorum.

Evet, low level bilgi sahibi olmanın önemini dilimiz döndüğünce izah etttikten sonra, kaldığımız yerden devam edebiliriz. Threading, korkulan ama bir zaman geldiğinde kaçılamayan bir konudur. Her programcı hayatının bir döneminde bu konu ile yüzleşecektir. Yüzleştiği zaman diliminde alnınızın akı ile sorunları bertaraf edebilmeniz için sizlere yardımcı olmaya çalışacağım.

Windows işletim sistemleri; multi-threaded işletim sistemleri değillerdir. Sadece bu hususta taklit yaparlar. İşletim sistemi üzerinde çalışan uygulamalara process adı verdildiğini biliyorsunuz. Her bir process, en az bir thread’e sahiptir. Sistem üzerinde çalışan process’ler Task Manager(Görev Yöneticisi) vasıtası ile gözlemlenebilir. Bu konu ile ilgili yazdığım diğer makaleleri de inceleyebilirsiniz.

İşletim sistemimiz içinde çalışan bir çok process ve en az process sayısı kadar thread olduğuna göre, işletim sistemi bu thread’lerin çalışma mekanizmasını nasıl sağlamaktadır ? İşte threading de en az bilinen ama en önemli hususda bu noktadadır. İşletim sistemi çalışan thread’leri belirli bir algoritmaya göre sıra ile çalıştırmaktadır. Yani belli bir t zaman diliminde, işletim sistemi üzerinde yalnızca bir tek thread çalışıyor olacaktır.

Bu durumda diğer thread’ler beklemeye alınmış durumdadırlar. Thread işletim sisteminin belirlediği bir zaman dilimi kadar çalıştırıldıktan sonra, sıradaki diğer thread’e geçilecektir. Bu çizelgeleme işlemine round robin scheduling adı verilir.Peki işletim sisteminin bu mekanizması nasıldır ? Biraz da bu hususa yakından bakmakta fayda var.

İşletim sistemi thread çalıştırmada zaman paylaşımlı bir yapıya sahiptir. Thread’leri belirli zaman dilimlerinde çalıştırır ve bir thread’e ayrılan zaman dolduğunda çalışma sırasındaki diğer thread’e geçilir. İşte bu thread’ler arasındaki geçiş zamanına quanta süresi adı verilir. Ve eğer değişmedi ise Windows işletim sistemlerinde quanta süresi 20 ms. dir. Yani bir thread işletim sisteminde en fazla 20 milisaniye çalışabilir. Bu 20 milisaniyenin dolmasından sonra, sıradaki diğer bir thread çalışacaktır. İşletim sistemi thread’ler arasında son derece hızla geçerek multi-threaded gibi görünmektedir. Bir thread’in maksimum çalışabileceği süre anlık olarak 20 ms’dir ibaresini tekrarlamak istiyorum. Bunu hiçbir zaman unutmayın, çünkü ilerleyen aşamalarda thread programlamada karşılaştığınız tüm problemlerin ve bu problemlerin çözümüne yönelik teknolojilerin ana sebebinin bu olduğunu anlayacaksınız.

Burada aklımıza bir soru geliyor. İşletim sistemi üzerinde o anda çalışan uygulama sayısının 100 olduğunu varsayalım. Bu durumda en azından 100 çalışan thread’imiz var demektir. İşletim sistemi thread’ler arasında 20 ms’lik zaman dilimlerinde geçiş yapıyorsa eğer, en son thread’e 2000 ms. sonra yani 2 sn. sonra mı uğrayacaktır ?
Böyle bir durumda uygulamamız 20 ms. çalışıp 2 sn. beklemek zorunda mı kalacaktır ?

Elbette ki hayır. Hernekadar işletim sistemi dediğimiz gibi thread’ler arasında 20 ms. gibi bir zaman diliminde geçiş yapıyorsa da; bu tarz uzun bekleme problemlerinin engellenmesi adına bir çözüme de sahiptir. Bu çözüme priority(öncelik) adı verilmiştir. İşletim sisteminin thread’leri belli zaman dilimlerinde çalıştırması round robin algoritmasına göredir demiştik; işin içine öncelikler girince bu algoritma priority round robin adını alır.

Bu yöntemde her bir thread belirli bir öncelik seviyesine sahiptir. Öncelik seviyesi yüksek olan thread’ler işletim sistemince daha ön sıralara alınırlar. Aynı öncelik seviyesine sahip olan thread’ler öncelik seviyesi yüksek olandan düşük olana doğru sırası ile işletilirler ve öncelik seviyesi yüksek olan grup tamamlandıktan sonra işletim sistemi diğer grubu çalıştırma cihetine gider. Thread öncelikleri 0-31 aralığında bir sayısal değer ile ifade edilirler.(Bknz.)

Bu durumda, thread’lerin belirli bir öncelik seviyesine göre gruplandığını ve öncelik seviyesi yüksek olanların düşük olanlardan daha önce çalıştırıldığını ve yüksek öncelikli thread’lerin çalışmasının bitmesine müteakip diğer öncelik gurubuna geçildiğini öğrenmiş bulunuyoruz. Örneğin; işletim sisteminde o anda çalışmakta olan thread’lerimizin öncelik seviyeleri aşağıda görüldüğü gibi olsun:

  • Thread-1, Öncelik: 20
  • Thread-2, Öncelik: 19
  • Thread-3, Öncelik: 05
  • Thread-4, Öncelik: 20
  • Thread-5, Öncelik: 19
  • Thread-6, Öncelik: 01
  • Thread-7, Öncelik: 15
  • Thread-8, Öncelik: 15
  • Thread-9, Öncelik: 05
  • Yukarıdaki gibi bir tabloda işletim sistemi öncelikle Thread-1 ve Thread-4 arasında 20 ms.’lik periyotlar ile geçişler yapacak ve her iki thread’in de bitmesine müteakip öncelik seviyesi 19 olan Thread-2 ve Thread-5′e geçecektir. Sırası ile öncelik seviyesi yüksek olan thread’ler işletilecek ve düşük seviyeli gruba geçiş yapılacaktır.

    Yukarıdaki tabloya göre çalıştırılma sırası aşağıdaki gibi olacaktır:

  • 1-Thread-1, Thread-4 (Öncelik 20)
  • 2-Thread-2, Thread-5 (Öncelik 19)
  • 3-Thread-7, Thread-8 (Öncelik 15)
  • 4-Thread-3, Thread-9 (Öncelik 5)
  • 5-Thread-6
  • Görüldüğü gibi Thread-6 diğer thread’lere göre çalışmak için çok az zaman bulabilecektir. Ancak işletim sistemi, öncelik seviyesi düşük olan thread’lerin tamamen ölü görünmemesi adına da bazı mekanizmalara sahiptir. Öncelik sistemi düşük olan thread’lerin çalışabilmesi için iki koşul vardır.

    Birincisi kendilerinden yüksek önceliğe sahip olan tüm thread’lerin bitmiş olması yada bir nedenden bloke olmuş olmalarıdır. Öncelik seviyesi yüksek olan bir thread SuspendThread ile duraksatılmış olabilir, yada thread içinde bir deadlock oluşmasından ötürü kararsız duruma gelmiş olabilir. Bu durumda öncelik seviyesi daha düşük olan thread’ler çalışma fırsatını yakalayabilirler.

    İkinci koşulumuz ise; işletim sisteminin düşük öncelikli thread’lere verdiği bir çalışma şansı ile ilgilidir. Bu şansa dynamic boosting adı verilir. Eğer bir thread, belirli bir zaman boyunca hiç çalışma şansını elde edememiş ise o zaman işletim sistemi bu thread üzerinde dynamic boosting işlemi uygular ve thread’in önceliğini 15 seviyesine yükseltir. Ancak bu yükseltme bir kaç quanta(1 quanta=20 ms.) süresi kadardır. Kendisine işletim sistemi tarafından kıyak geçilen thread, daha sonra tekrar orjinal öncelik seviyesine geri çekilir.

    Gördüğünüz gibi, threading’in arka planında son derece hassas mekanizmalar bulunmaktadır. İşletim sistemi gerçek multi-threaded’ı sağlamak adına thread’ler arasında öncelik kavramını yoğun bir şekilde kullanarak aslında bizleri kandırmaktadır :) İşletim sistemi thread’ler arasında o kadar hızlı geçer ki, biz thread’lerimizin kesintiye uğradığının farkına dahi varmayız.

    Peki, biz bu öncelik seviyelerini nasıl kullanacağız ? Öncelik seviyelerinin ne olduğunu az çok anlattık. Ama makalenin amacı gereği daha derinlere inmemiz gerekiyor :)

    Yazdığımız uygulamalarda öncelik seviyelerini istediğimiz gibi ayarlayabiliriz. Öncelik belirleme işlemimiz, process önceliği ve thread önceliği olarak iki kısımda incelenir. İşletim sistemi için bir thread’in önceliği; o thread’e sahip olan process’in önceliği ile thread’in önceliğinin toplamıdır. Öncelik değerlerini MSDN‘den gözlemleyebilirsiniz.

    Bir thread’in öncelik değerleri Delphi’de aşağıdaki şekilde tanımlanmıştır:

      THREAD_BASE_PRIORITY_LOWRT = 15;
      THREAD_BASE_PRIORITY_MAX = 2;
      THREAD_BASE_PRIORITY_MIN = -2;
      THREAD_BASE_PRIORITY_IDLE = -15;
    
      THREAD_PRIORITY_LOWEST              = THREAD_BASE_PRIORITY_MIN;
      THREAD_PRIORITY_BELOW_NORMAL   = THREAD_PRIORITY_LOWEST + 1;
      THREAD_PRIORITY_NORMAL              = 0;
      THREAD_PRIORITY_HIGHEST             = THREAD_BASE_PRIORITY_MAX;
      THREAD_PRIORITY_ABOVE_NORMAL   = THREAD_PRIORITY_HIGHEST - 1;
      THREAD_PRIORITY_ERROR_RETURN    = MAXLONG;
    
      THREAD_PRIORITY_TIME_CRITICAL    = THREAD_BASE_PRIORITY_LOWRT;
      THREAD_PRIORITY_IDLE                  = THREAD_BASE_PRIORITY_IDLE;
    

    Bu listede özel durumlu iki üye vardır.THREAD_PRIORITY_IDLE ve THREAD_PRIORITY_TIME_CRITICAL.

    Thread’ler için bu öncelik seviyelerinin kullanılması; process ve thread öncelik seviye değerlerinin toplanması yerine sabit değere eşitleme yoluna gidilmesine neden olur. THREAD_PRIORITY_IDLE öncelik seviyesinde, işletim sistemi öncelik değerini; thread’in ait olduğu process’in öncelik seviyesinin REALTIME_PRIORITY_CLASS olması durumunda 16 diğer durumlarda 1 olarak ayarlayacaktır.

    THREAD_PRIORITY_TIME_CRITICAL öncelik seviyesinde ise işletim sistemi öncelik değerini; thread’in ait olduğu process’in öncelik seviyesinin REALTIME_PRIORITY_CLASS olması durumunda 31 diğer durumlarda 15 olarak ayarlayacaktır.

    Bu karmaşık cümleyi, aşağıda pseudo kod ile göstermeye çalışmak sanırım daha kolay anlaşılmasına vesile olacaktır;

    function GetOSPriority(const Thread : TThread) : Longint;
    begin
      if Thread.Priority = THREAD_PRIORITY_IDLE then
        if Thread.Process.Priority = REALTIME_PRIORITY_CLASS
        then Result := 16
        else Result := 1;
    
      if Thread.Priority = THREAD_PRIORITY_TIME_CRITICAL then
        if Thread.Process.Priority = REALTIME_PRIORITY_CLASS
        then Result := 31
        else Result := 15;
    end;
    

    Buradan gözlemleyebildiğimiz kadarı ile işletim sisteminin thread’leri çalıştırması iki ayrı öncelik seviyesine dayanmaktadır. Process öncelik seviyesi ve thread öncelik seviyesi. Process’ler için öncelik seviyeleri aşağıdaki gibidir;

  • NORMAL_PRIORITY_CLASS
  • IDLE_PRIORITY_CLASS
  • HIGH_PRIORITY_CLASS
  • REALTIME_PRIORITY_CLASS
  • Yazdığımız uygulamalarda herhangi bir öncelik seviyesi belirtmediğimizde process öncelik seviyesi NORMAL_PRIORITY_CLASS‘a ayarlanır. Thread’lerde ise THREAD_PRIORITY_NORMAL değerine ayarlanmaktadır. Çalışma anında bu değerleri istediğimiz gibi değiştirebiliriz. Bir process’in öncelik seviyesini SetPriorityClass API’si yardımı ile değiştirebileceğimiz gibi, eğer process’i biz oluşturuyor isek CreateProcess API’sine parametre olarak da geçebiliriz. Mevcut bir thread’in öncelik seviyesini ise SetThreadPriority API’sini kullanarak değiştirebileceğimiz gibi, eğer TThread Delphi sınıfını kullanıyorsak bu sınıfın Priority özelliğini değiştirerek de yapabiliriz.

    Thread önceliklerinin hayati öneme sahip olduğunu sanıyorum artık anladık. Gerçekten kritik işlemleriniz var ise, process ve thread öncelik seviyelerinizi yüksek değerlerle kullanarak bu işlemlerin diğer thread’lerden daha önce tamamlanmasını sağlayabilirsiniz. Ancak, kantarın topuzunu fazla da kaçırmamak gerekir bu konularda. Her yazdığınız thread’i real time thread olarak yazarsanız, işletim sistemini kararsız duruma sokabilir, yada beklenmeyen yavaşlıklara neden olabilirsiniz.

    Şu aşamaya geldiğimizde thread’lerin işletim sistemince nasıl yönetildiğini ve çalışma mekanizmaları hakkında bir nebze de olsa bilgi sahibi olmuş bulunuyoruz. Hâla aklımızda olan ve asla unutmayacağımız 20 ms. lik süreler içinde thread’ler arasında gezilmesi bize halihazırda ne gibi sorunlar getirir konusuna henüz değinmedik. Biraz da bu konuya değinelim arzu ederseniz.

    Daha önce de sıklıkla ifade ettiğimiz gibi; işletim sistemi thread’ler arasında her 20 ms. de bir geçişler yapıyordu. Şimdi bir senaryo uyduralım. Bu senaryoda, bir thread programımız içinde tanımlı olan global bir değişkene erişecek ve bu değişkenin değerini kontrol edecek yada değiştirecek.

    Bu senaryoda ne gibi bir sorun olabilir sizce ?

    Bir sorun olmaz gibi görünüyor, ama eminim sizlerde gerek okuduğunuz bazı kaynaklarda; gerekse de kulaktan dolma bilgilerle thread’ler içinden dış dünyaya erişimlerde dikkatli olmak gerektiğini biliyorsunuzdur. Peki neden dikkatli olmamız gerekiyor , neden bir thread içinden dış dünyaya erişmemiz sorunlara sebep oluyor ? İşte bu soruya yanıt pek bilinmiyor. Maksadımız bu sorunun yanıtını açıklamak ve dolayısı ile thread senkronizasyon mekanizmalarının neden var olduklarını izah etmeye gayret etmek.

    Muhtemeldirki thread senkronizasyonlarına yabancı değilsinizdir ve thread’leriniz içinde kullanmışsınızdır. Ancak bu makaleyi okuduktan sonra bu senkronizasyon mekanizmalarına daha bir farklı gözle bakacağınızdan eminim. En yaygın kullanılanlar, critical sectionlar, muteksler, semaphore’lar ve eventlerdir. Yine paragrafımızın başında değindiğimiz soruya bir kere daha gönderme yapalım. Bu mekanizmalar niye var, ne gibi sorunlara önlem alıyor bu birbirinden gizemli mekanizmalar ?

    Şimdi örneğimize geri dönelim. Bir thread’imiz olduğunu ve bu thread’in programımız içinde tanımlı olan bir değişkene ulaşacağını söylemiştik. Bunu kısaca kod ile simüle edelim:

    var
      iValue : Integer = 10;
    ..
    ..
    ..
    procedure TMyThread.Execute;
    begin
    ...
    ...
      if iValue = 10 then iValue := 50;
    end;
    

    Bu son derece masumane kod, bir değişkenin değerinin 10 olup olmadığını kontrol ediyor ve değer 10 ise 50 ataması yapıyor. Aslında burada son derece ciddi problemler olabilir. Burada bakış açımızı biraz daha derinlere çevirmemiz , makina dili seviyesinde düşünmemiz gerekiyor. Yazdığımız tüm kodların makina diline çevrildiğini biliyorsunuz. Masum bir değişkene değer atamanın yada o değişkendeki değeri kontrol etme kodunun dahi makina dilinde bir karşılığı vardır. Bu karşılıkları Delphi’de CPU Window‘u açarak görebilirsiniz.

    Bir değişkenin değerinin kontrol edilmesi yada o değişkene değer atanması işlemi makina dilinde birden fazla satıra karşılık geliyor olabilir. Diyelim ki bir değişkenin değerinin kontrol edilmesi, makina dilinde(assembler) 4 satırdan oluşsun ve bu değişkene değer atanması da 6 satırdan oluşsun. Böyle bir durumda işletim sistemi tarafından çalıştırılan thread tam değişkenin değerini kontrol ederken, yani “if Value = 10″ işlemini yaparken makina dili kodlarının 3ncü satırında beklemeye alınırsa(20 ms’lik işletme zamanı dolarsa) ne olur ?

    Söylemimizi daha anlaşılır kılmak adına aşağıdaki pseudo koda bakabilirsiniz:

    //if iValue = 10
    asm
      mov ..
      pop ..
      push.. // Thread 20 ms.'lik çalışma süresini bu noktada bitirir ise !!
      je  ..
    end;
    
    // iValue := 50;
    asm
      mov ..
      mov ..
      add ..
      pop ..
      push..
      nop ..
    end;
    

    Yukarıdaki kodlar tamamen uydurma kodlardır. Amacımız ne demek istediğimizi bu uydurma kodlar ile daha anlaşılır hâle getirmeye çalışmaktır.

    Bu durumda geriye kalan 1 satırlık kod henüz işletilmemiş durumdadır. Ve aynı sınıfın bir başka thread’i bu değişkene erişip değişkenin değerini kendi 20 ms. lik zaman diliminde değiştirirse birinci thread hatalı çalışabilir. Bu ve benzeri senaryolar, birden fazla thread’in ortak kaynakları kullandığı her ortamda gerçekleşebilir. İşte bu sebeplerden ötürü, thread’lerde senkronizasyonlar son derece önemlidir. Kritik kod bloklarının birden fazla thread’in erişimi sırasında güvenli duruma getirilmesi gerekir. Buna genellikle atomik erişim adı verilir. İşletim sistemi thread’ler arasındaki zaman paylaşım mekanizmasını kendisi yürütür ve bu mekanizmaya direkt erişime müsaade etmemiştir. Eğer biz işletim sistemine “thread’ler arasında geçiş yapma” diyebilse idik, thread senkronizasyonlarına hiç gerek kalmazdı. Ancak bu derece ciddi bir husus programcıların insiyatifine bırakılmayacak kadar önemli olduğu için bu mekanizma işletim sistemi tarafından yönetilir.

    Bir veri kaynağına multi-threaded ortamlarda güvenli(atomik) erişmek için işletim sistemi bize pek çok senkronizasyon fonksiyonu sağlamıştır. Bunlardan InterlockedIncrement, InterlockedCompareExchange, InterlockedDecrement, InterlockedExchange ve InterlockedExchangeAdd metodları thread’lerin global değişkenlere erişimlerini güvenli hale getiren metodlardır. Bu metodların tanım şekilleri aşağıdaki gibidir:

    LONG InterlockedIncrement(
      LPLONG lpAddend
    );
    
    PVOID InterlockedCompareExchange(
      PVOID *Destination,	
      PVOID Exchange,	
      PVOID Comperand	
    );
    
    LONG InterlockedDecrement(
      LPLONG lpAddend 	
    );
    
    LONG InterlockedExchange(
      LPLONG Target,	
      LONG Value 	
    );
    
    LONG InterlockedExchangeAdd (
      PLONG Addend,	
      LONG Increment	
    );
    

    ve Delphi tanımları:

      function InterlockedIncrement(var Addend: Integer): Integer; stdcall;
      function InterlockedDecrement(var Addend: Integer): Integer; stdcall;
      function InterlockedExchange(var Target: Integer; Value: Integer): Integer; stdcall;
      function InterlockedCompareExchange(var Destination: Pointer; Exchange: Pointer; Comperand: Pointer): Pointer stdcall;
      function InterlockedExchangeAdd(var Addend: Longint; Value: Longint): Longint stdcall; overload;
    

    Bu bilgiler ışığında yukarıdaki örneğimizi yeniden yazarsak;

    var
      iValue : Integer = 10;
    ..
    ..
    ..
    procedure TMyThread.Execute;
    begin
    ...
    ...
      if InterlockedCompareExchange(Pointer(iValue), Pointer(iValue), Pointer(0)) = Pointer(10)
      then InterlockedExchange(iValue, 50);
    end;
    

    Gördüldüğü üzere birden fazla thread’in erişebileceği bir değişkenin değerinin kontrolü sırasında InterlockedCompareExchange metodunu, ve bu değişkene değer atarken de InterlockedExchange metodunu kullandık. Bu metodlar ilgili değişkene güvenli bir şekilde erişmemizi ve veriyi düzgün bir şekilde değiştirmemizi sağlarlar. Bu metodların kullanılması thread’in 20 ms. lik çalışma süresinin işletim sistemi tarafında sonuna da gelinmiş olsa, işlem bitmeden bir başka thread’e geçilmeyeceğinin garantisini verir. En önemli özelliklerinin özetlenmesi gerekir ise; işletim sistemi bu metodlar kullanımda iken thread’ler arası geçişi durdurur. Kısacası, bir global değişkenin değerinin değiştirilmesi yada kontrol edilmesi sırasında oluşabilme ihtimali olan thread geçişlerinin önüne set olur ve işleminiz hatasız bir şekilde sonlanır.

    InterlockedCompareExchange ilk değer olarak içeriği kontrol edilecek yada değişime uğrayacak değişkenimizin adresini alır. İkinci parametresi ise; kontrol edilecek değerin eşit olması durumunda değiştirilecek bilgiyi içerir. Üçüncü parametre, global değişkenimizin kontrol edileceği değerin adresidir. Global değişkenimiz üçüncü parametredeki değer ile eşit olduğu durumda, 2nci parametrede belirtilen değere set edilir. Fonksiyonun geri dönüş değeri ise InterlockedCompareExchange’den önce global değişkenin
    sahip olduğu değerdir. Bu metodun en can alıcı noktası kendisine 32 bit veri türlerini kabul ediyor olmasıdır. İşletim sistemi bu fonksiyona parametre olarak geçilen değerleri kendi içinde 32 bit bir kıyaslamaya tabii tutacaktır.

    Bu durumda InterlockedCompareExchange fonksiyonuna sadece 32 bit veri türlerinin geçilmesi gerekir. Bilindiği gibi, işletim sistemi 32 bit olduğu için Pointer türü de 32 bit’dir. Böyle bir durum söz konusu iken 32 bit’ten daha büyük değerlerin bu fonksiyon ile kontrol edilmesi mümkün değildir.Eğer 64 bit global değişkenleriniz var ise ve bu değişkenleri atomik düzeyde kontrol etmek istiyor iseniz, uygulamanızın çalıştığı işletim sisteminin minimum Windows Vista olması gerekir. Windows Vista altında InterlockedCompareExchange64 isimli fonksiyonu 64 bit veri kıyaslaması için kullanabilirsiniz.

    Tüm bu açıklamalar bizde Delphi’deki 32 bit harici veri türlerinin atomik olarak nasıl kontrol edileceği hususunda bazı soru işaretleri oluşmasına neden olur. Bu veri türlerinden Boolean en çok kullanılan veri türü sayılabilir. Bir boolean veri türünü global olarak tanımlarsak ve bunu atomik düzeyde InterlockedCompareExchange ile kontrol etmek ister isek ne yapacağız ?

    Bildiğiniz gibi Boolean veri türü, 1 Byte’lık(8 bit) yer kaplamaktadır. Oysa fonksiyonumuz 32 bit veri türleri ile çalışır. Bu durumda; Delphi’nin Boolean tipi yerine Win32′deki LongBool veri tipini kullanabilirsiniz. Örneğin;

    uses
      StrUtils; // IfThen fonksiyonu için..
    ..
    ..
    var
      GlobalBool : LongBool = false;
    ..
    ..
    var
      pResult : Pointer;
    begin
      pResult := InterlockedCompareExchange(Pointer(GlobalBool), Pointer(LongBool(true)), Pointer(LongBool(false)));
    
      if pResult = Pointer(LongBool(true))
      then ShowMessage('Global Boolean değişkenimiz InterlockedCompareExchange den önce True idi')
      else  ShowMessage('Global Boolean değişkenimiz InterlockedCompareExchange den önce false idi');
    
      ShowMessage('GlobalBool un yeni değeri:' + IfThen(GlobalBoolean, 'Yeni değer True', 'Yeni değer False'));
    end;
    

    Yukarıdaki kod örneği; global bir sahada tanımlanmış LongBool türündeki değişkenimizin değerini atomik seviyede kontrol eder ve gerekir ise değiştirir. InterlockedCompareExchange metodunun birinci parametresi global boolean değişkenimizi gösterir. İkinci parametremiz bu değişkenin almasını istediğimiz yeni değeri ifade eder. Üçüncü parametremiz ise global değişkenimizin karşılaştırılacağı değerdir. Fonksiyondan geriye dönen değer, InterlockedCompareExchange fonksiyonunun çağırılmasından evvel global değişkenin sahip olduğu değerdir.

    Bu mekanizma işletim sistemindeki thread senkronizasyonlarından sadece bir tanesidir. Eğer uygulamanız içinde kullandığınız thread’lerin global değişkenlere erişimi söz konusu ise, bu metodları kullanmak sizlere güvenli bir kodlamanın yollarını açacaktır.

    Critical Section

    Bir diğer popüler senkronizasyon mekanizması da; critical section’lardır. Critical Section’lar en bilinen thread senkronizasyon mekanizmalarından bir tanesidirler. Burada amaç; bir thread’in belirli bir kritik kodu çalıştırırken , başka bir thread’in aynı kod bloğuna girmesine mani olmaktır. Bu mekanizmanın en sık kullanılan metodları; InitializeCriticalSection, EnterCriticalSection, LeaveCriticalSection ve DeleteCriticalSection‘dır.

    C++ ve Delphi tanımları aşağıdaki gibidir:

    VOID InitializeCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection 	
    );
    
    VOID EnterCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection 	
    );
    
    VOID LeaveCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection
    );
    
    VOID DeleteCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection 	
    );
    
    _RTL_CRITICAL_SECTION = record
      DebugInfo: PRTLCriticalSectionDebug;
      LockCount: Longint;
      RecursionCount: Longint;
      OwningThread: THandle;
      LockSemaphore: THandle;
      Reserved: DWORD;
    end;
    
      TRTLCriticalSection = _RTL_CRITICAL_SECTION;
    
      procedure InitializeCriticalSection(var lpCriticalSection: TRTLCriticalSection); stdcall;
      procedure EnterCriticalSection(var lpCriticalSection: TRTLCriticalSection); stdcall;
      procedure LeaveCriticalSection(var lpCriticalSection: TRTLCriticalSection); stdcall;
      procedure DeleteCriticalSection(var lpCriticalSection: TRTLCriticalSection); stdcall;
    

    Critical Section’lar thread programlamada sıklıkla kullanılan ama aynı zamanda sıklıkla yanlış kullanılan mekanizmalardandır. Kullanımında son derece dikkatli olmamız gereken bir nokta vardır. Ancak o noktaya değinmeden evvel isterseniz critical section kullanımını hayalimizde canlandırmaya çalışalım.

    Şimdi elektrik faturası ödemek üzere evinize en yakın elektrik idaresine gittiğinizi düşünün. Elektrik idaresinde faturanızı yatırabilmeniz maksadı ile 10 memur çalışıyor olsun.
    Her bir memurun kendisine ait de bir odası olsun. Bu odalara 1′den 10′a kadar numara verdiğimizi düşünelim. Memurlar, bir seferde sadece bir aboneyi odaya kabul etsinler ve bir kişinin faturası ile ilgilensinler. Bir abone faturasını yatırmak için memurun bulunduğu odaya girdiğinde oda kapısının müstahdem(hademe) tarafından kapatıldığını ve başkalarının odaya girmesine müstahdemin izin vermediğini düşünelim. Böyle bir sistemde bir oda içinde aynı anda birden fazla elektrik abonesi bulunamaz. Bir abonenin içeride olduğunu bilen müstahdem kapıdan içeri girmeye çalışacak olan bir diğer aboneyi beklemesi hususunda uyaracaktır.

    Bu örneğimizde oda; güvenle çalışmasını istediğimiz koddur. Müstahdem işletim sistemidir. Oda kapısı ise critical section’dır. Tıpkı bu örnekte olduğu gibi, critical section’lar bir flag gibi çalışır. Açık yada kapalı bayrakları vardır. Bir oda kapısı ya açıktır yada kapalıdır mantığı critical section’lar içinde geçerlidir. Oda kapısının açık/kapalı olma durumuna göre odaya yeni bir abone alımından sorumlu olan müstahdem programlama dünyasında işletim sistemidir. İşletim sistemi, bir critical section ile işaretlenmiş olan kod bloğuna müstahdemlik yaparak koruma sağlayacak ve aynı kod bloğuna başka thread’lerin girmesine izin vermeyecektir.

    EnterCriticalSection kapının kapandığı durumdur. Kapı bir kere kapandıktan sonra ancak LeaveCriticalSection ile açık duruma geçebilir. Daha teknik tabirler ile ifade etmek icap ederse; EnterCriticalSection işletim sistemi içerisinde bir değişkenin değerini set eder. Bir başka thread EnterCriticalSection kod bloğuna geldiğinde bu değişkenin değerinin ne olduğuna bakar. Eğer set edilmiş ise(yani kapı kapalı ise) o zaman beklemeye başlar. LeaveCriticalSection ise, set edilmiş olan işletim sistemine has değişkeni tekrar eski haline alır. Bu durumda beklemekte olan diğer thread zaman kaybetmeden içeri girer ve sistem bu şekilde işlemeye devam eder.

    Delphi’de yukarıda tanımlarını verdiğimiz critical section metodlarını kullanmak için TRTLCriticalSection türünden bir yapıya ihtiyaç duyuyorduk. İşte bu yapının tanımlandığı yer ile ilgili programcılar büyük hatalara düşmektedirler. Bu hataların neler olduğunu örnek bir kod ile göstermek isterim:

    TMyThread = class(TThread)
    private
      Section 		: TRTLCriticalSection;
    
      fName,
      fStartValue,
      fStopValue	: String;
    protected
      procedure Execute; override;
    public
      constructor Create(const AName : String);
      destructor Destroy; override;
    
      property StartValue : String read fStartValue;
      property StopValue 	: String read fStopValue;
      property Name		: String read fName;
    end;
    
    implementation
    
    {$R *.dfm}
    
    { TMyThread }
    
    constructor TMyThread.Create(const AName : String);
    begin
      inherited Create(true);
      FreeOnTerminate := true;
      fName := AName;
    
      InitializeCriticalSection(Section);
    
      Resume;
    end;
    
    destructor TMyThread.Destroy;
    begin
      DeleteCriticalSection(Section);
      form1.Memo1.Lines.Add('Name:' + Name + ' Start Time:' + StartValue + ' Stop Time:' + StopValue);
    
      inherited;
    end;
    
    procedure TMyThread.Execute;
    var
      iCounter : Integer;
    begin
      inherited;
    
      EnterCriticalSection(Section);
      fStartValue := TimeToStr(Time);
    
      Sleep(10000);
    
      fStopValue  := TimeToStr(Time);
      LeaveCriticalSection(Section);
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      mThread1,
      mThread2 : TMyThread;
    begin
      mThread1 := TMyThread.Create('Birinci Thread');
      Sleep(2000);
      mThread2 := TMyThread.Create('İkinci Thread');
    end;
    

    Yukarıdaki kod örneğinde, Section isminde bir TRTLCriticalSection değişkenimiz var ve bu değişken TMyThread sınıfının bir üyesi olarak tanımlanmış. TMyThread’in Execute kodunda critical section’a girdiğimizi EnterCriticalSection metodu vasıtası ile söylemiş oluyoruz. Hemen ardından TimeToStr ile o anki zaman bilgisini bir değişkende depoluyoruz. Ardından 10 saniye bekliyoruz ve yeni zaman bilgisini bir başka değişkende depoluyoruz. Nihayet, LeaveCriticalSection metodunu çağırıyoruz ve kritik kod bloğundan çıktığımızı söylüyoruz.

    Şimdi dikkatinizi Button’un altındaki koda verin. Burada aynı thread sınıfından iki değişken tanımlanmış ve birincisi oluşturulup otomatikman çalıştırılır çalıştırılmaz(Resume çağrısı bir thread’i başlatır), 2 saniyelik bir bekleme yapılmış ve ardından diğer thread oluşturulmuş ve çalıştırılmıştır.

    Burada beklentilerimizin ne olduğu öncelikle bir yazalım ardından sonucu birlikte irdeleyelim.

    Beklentimiz:

  • Birinci Thread: Çalışma başlama zamanının 02:40:00 olduğu varsayılırsa, çalışma bitiş zamanı 02:40:10 olmalıdır.
  • İkinci Thread : Çalışma başlama zamanı 02:40:10 ve çalışma bitiş zamanı da 02:40:20 olmalıdır.
  • Critical section’ların çalışma mekanizmalarını tekrar hatırlayacak olursanız, bir thread’in kritik kod bloğuna girmesi bir başka threadin orada beklemesine neden oluyordu. Bizim örneğimizde “Birinci Thread” koda 02:40:00 ‘da girdiğinde “İkinci Thread” aynı kod bloğuna 02:42:00 ‘da girmeye çalışacak ancak orada bir başka thread olduğu için giremeyecektir. “İkinci Thread” imiz ancak 02:40:10′da içeriye girecektir.

    Critical Section’ların doğası gereği olması gereken budur. Ancak bu örnekte beklediğimizi bulamayacağız. Çalıştırıp sonuçlarına bir bakalım:

    Sonuç

  • Name:Birinci Thread Start Time:02:45:52 Stop Time:02:46:02
  • Name:İkinci Thread Start Time:02:45:54 Stop Time:02:46:04
  • Sizinde gördüğünüz gibi “İkinci Thread” birincisini beklemeden içeriye girebilmiştir. Peki bu nasıl oldu ? Onca yazdığımız şey doğru değilmiydi ?

    Elbetteki doğru idi. Ancak burada programcıların %95′inin yaptığı bir hatayı simüle ettik. TRTLCriticalSection türündeki ve “Section” ismindeki değişken TMyThread’in bir üye değişkenidir. Ve bu üye değişkene TMyThread sınıfı oluşturulduğunda hafızada bir yer ayırılmaktadır. TMyThread sınıfının oluşturulan her bir instance’ı için Section değişkeninin hafızadaki adresi farklı bir yeri gösterecektir.

    Tekrar elektrik idaresi örneğine dönecek olursak; “Birinci Thread” için Section 5 numaralı odayı, “İkinci Thread” için ise 10 numaralı odayı gösteriyor olabilir. Böylece; “Birinci Thread” 5 numaralı odanın kapısını açmış ve içeri girmiş; “İkinci Thread” ise 10 numaralı odanın içinde kimse olmadığı için o da 10 numaralı odaya girebilmiştir.

    Buradan anlaşılması gereken özet bilgi şudur: critical section’ları işaret eden değişkeni bir thread’in yerel değişkeni yaparsanız, hiç bir zaman istediğinizi elde edemezsiniz. Bu yerel değişken sadece ve sadece o anda create edilmiş olan instance için anlamlıdır. Diğer thread instance’larını bağlamaz. Şimdi bu Section değişkenini global bir alana taşıyıp kodumuzu revize edelim ve test sonuçlarını burada paylaşalım:

    TMyThread = class(TThread)
    private
      fName,
      fStartValue,
      fStopValue	: String;
    protected
      procedure Execute; override;
    public
      constructor Create(const AName : String);
      destructor Destroy; override;
    
      property StartValue : String read fStartValue;
      property StopValue 	: String read fStopValue;
      property Name		: String read fName;
    end;
    
    var
      Form1: TForm1;
      Section	: TRTLCriticalSection;
    
    implementation
    
    {$R *.dfm}
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      mThread1,
      mThread2 : TMyThread;
    begin
      mThread1 := TMyThread.Create('Birinci Thread');
      Sleep(2000);
      mThread2 := TMyThread.Create('İkinci Thread');
    end;
    
    { TMyThread }
    
    constructor TMyThread.Create(const AName : String);
    begin
      inherited Create(true);
      FreeOnTerminate := true;
      fName := AName;
    
      Resume;
    end;
    
    destructor TMyThread.Destroy;
    begin
      form1.Memo1.Lines.Add('Name:' + Name + ' Start Time:' + StartValue + ' Stop Time:' + StopValue);
    
      inherited;
    end;
    
    procedure TMyThread.Execute;
    var
      iCounter : Integer;
    begin
      inherited;
    
      EnterCriticalSection(Section);
      fStartValue := TimeToStr(Time);
    
      Sleep(10000);
    
      fStopValue  := TimeToStr(Time);
      LeaveCriticalSection(Section);
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      InitializeCriticalSection(Section);
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      DeleteCriticalSection(Section);
    end;
    

    Kodumuzun bu yeni halinde, TMyThread sınıfının constructor’ındaki InitializeCriticalSection kaldırılmış ve formumuzun OnCreate event’ine taşınmıştır. Benzer bir şekilde, destructor’daki DeleteCriticalSection metodu formumuzun OnDestroy metoduna taşınmış ve TMyThread içinde TRTLCriticalSection türündeki değişkenimiz global alana taşınmıştır. Geri kalan kodlar bire bir aynıdır. Buyrun çalıştırma sonuçlarına birlikte bakalım:

  • Name:Birinci Thread Start Time:02:54:01 Stop Time:02:54:11
  • Name:İkinci Thread Start Time:02:54:11 Stop Time:02:54:21
  • Görüldüğü üzere “İkinci Thread” ‘imiz “Birinci Thread” işini bitirmeden kod bloğundan içeri girememiştir. Yani critical section’lardan beklenen davranış oluşmuştur. Bu husus son derece önemlidir ve pek çok programcının yaptığı ciddi thread hatalarının başında gelir. Critical section’ları Windows API’leri ile kullanmak yerine kullanımı daha kolay olan ama bire bir aynı işi yapan TCriticalSection Delphi sınıfı ile kullanabilirsiniz. TCriticalSection sınıfı SyncObjs.pas isimli dosyada tanımlanmıştır.Enter ve Leave isimli iki metoda sahiptir. Tahmin edebileceğiniz gibi, yada Delphi kaynak kodlarından gözlemleyebileceğiniz gibi bu metodlar içlerinde EnterCriticalSection ve LeaveCriticalSection metodlarını çağırmaktadırlar.

    Critical Section’lar hakkında son söz olarak aşırı kullanımının performansa olumsuz etkileri olacağını belirtmek isterim. Bu sebeple, thread içindeki kodlarınızı optimize etmeli ve sadece gereken noktalarda critical section’ları kullanmalısınız. Critical Section içindeki kod bloklarınızı mümkün mertebe kısa tutmalısınız.

    WaitForSingleObject & WaitForMultipleObjects

    Bir sonraki senkronizasyon mekanizmamız olan mutex’ler kavramını izaha başlamadan evvel ihtiyacımız olan iki API fonksiyonuna değinmemiz gerekiyor. Bunlar WaitForSingleObject ve WaitForMultipleObjects API’leri.

    Bu iki API fonksiyonu senkronizasyon mekanizmaları için son derece önemlidir. Bu fonksiyonlar beklemek amacı ile kullanılırlar. İşletim sistemindeki senkronizasyon mekanizmaları bir önceki örneğimizde bahsettiğimiz critical section’larda da olduğu gibi bir kapı şeklinde çalışırlar. Kapı ya açıktır yada kapalıdır. WaitForSingleObject ve WaitForMultipleObjects fonksiyonları kapı kapalı olduğu müddetçe beklemek amacı ile vardır.

    Öncelikle tanımlarına yakından bir bakalım:

    DWORD WaitForSingleObject(
      HANDLE hHandle,
      DWORD dwMilliseconds
    );
    
    DWORD WaitForMultipleObjects(
      DWORD nCount,
      CONST HANDLE *lpHandles,
      BOOL bWaitAll,	
      DWORD dwMilliseconds
    );
    

    ve Delphi tanımları aşağıdaki gibidir:

      MAXIMUM_WAIT_OBJECTS = 64;
    
      TWOHandleArray = array[0..MAXIMUM_WAIT_OBJECTS - 1] of THandle;
      PWOHandleArray = ^TWOHandleArray;
    
      function WaitForSingleObject(hHandle: THandle; dwMilliseconds: DWORD): DWORD; stdcall;
    
      function WaitForMultipleObjects(nCount: DWORD; lpHandles: PWOHandleArray;
      bWaitAll: BOOL; dwMilliseconds: DWORD): DWORD; stdcall;
    

    Her iki fonksiyonda bekleme yapacakları nesnelerin tutacaklarını(THandle) parametre olarak almaktadırlar. Bu fonksiyonlarla; CreateEvent yada OpenEvent, CreateMutex yada OpenMutex, CreateProcess yada OpenProcess, CreateSemaphore yada OpenSemaphore, CreateThread yada CreateRemoteThread, CreateWaitableTimer yada OpenWaitableTimer fonksiyon çağırımlarından geri dönen handle değerlerini bekleyebilirsiniz.

    Yani, WaitForSingleObject yada WaitForMultipleObjects ile; bir Event’in, bir Mutex’in, bir Process’in, bir Semaphore’un, bir Thread’in yada bir Waitable Timer’ın bitmesini bekleyebilirsiniz. WaitForSingleObject’in ikinci parametresi, milisaniye cinsinden bekleme zaman bilgisidir. Tanımından da görebileceğiniz gibi, bu zaman bilgisi DWord türündedir ve DWord 32 bit bir tamsayıdır. 32 bit bir tamsayının alabileceği maksimum değer, High(DWord) = $FFFF FFFF = 4.294.967.295 ms. = 49.7 gündür. Tam bu noktada 49.7 gün ile ilgili bir diğer makalemizi incelemeniz faydalı olabilir.

    Delphi’de INFINITE isimli sabit aşağıdaki şekilde tanımlanmıştır:

      INFINITE = DWORD($FFFFFFFF);
    

    WaitForSingleObject fonksiyonunun ikinci parametresine genellikle INFINITE sabitini geçeriz. Bu da fonksiyonumuzun beklediği işletim sistemi nesnesini 49.7 gün boyunca beklemesi anlamına gelir. Hemen kısa bir örnek vererek yazdıklarımızı zihninizde pekiştirelim;

    Örneğimizde bir thread oluşturacak ve bu thread’in sonlanmasını bekleyeceğiz;

    TMyThread = class(TThread)
    protected
      procedure Execute; override;
    public
      constructor Create;
    end;
    
    var
    Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    { TMyThread }
    
    constructor TMyThread.Create;
    begin
      inherited Create(true);
      FreeOnTerminate := true;
    
      Resume;
    end;
    
    procedure TMyThread.Execute;
    begin
      inherited;
    
      Sleep(20000);
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      mThread : TMyThread;
      RetVal    : DWord;
    begin
      mThread := TMyThread.Create;
      RetVal := WaitForSingleObject(mThread.Handle, INFINITE);
      ShowMessage('Thread bitti');
    end;
    

    Yukarıdaki örneğimizde basit bir thread sınıfımız olduğunu görüyorsunuz. Thread’in çalışma kodunda hiç bir şey yapmadan 20 saniye bekliyoruz. Thread’i oluşturan Button1′in OnClick metodunda ise WaitForSingleObject ile thread’in bitmesini bekliyoruz. Thread’in oluşturulup çalıştırılmasından itibaren 20 saniye sonra threadimiz bitecek ve WaitForSingleObject fonksiyonunun hemen altındaki ShowMessage kodumuz çalışabilecektir. Yukarıdaki kodu biraz değiştirelim ve belirli bir zaman bekleme kodu yazalım:

    procedure TForm1.Button1Click(Sender: TObject);
    var
      mThread : TMyThread;
      RetVal    : DWord;
    begin
      mThread := TMyThread.Create;
      RetVal := WaitForSingleObject(mThread.Handle, 10000); // 10 saniye bekle..
      ShowMessage('Thread bitti');
    end;
    

    Kodumuzun bu yeni halinde thread’in çalışma zamanında herhangi bir değişiklik yapmadık ancak bekleme kodumuzu 10 saniye olarak belirttik. Bunu yapmamızın sebebi, WaitForSingleObject metodunun dönüş değerlerini size anlatabilmek içindi. Normalde thread’lerimiz içinde 20 sn. bekle gibi bir kod yazmayız, dolayısı ile genellikle thread’lerimizin çalışma süresinin ne kadar olacağını kestiremeyiz. Yukarıdaki gibi WaitForSingleObject’e 10 saniye beklemesini söylediğimiz kullanımlarda, acaba gerçekten bu zaman zarfında thread’in bittiğini nereden bileceğiz ?

    Bizim şu andaki örneğimizde thread’imiz 20 saniye sürüyordu, oysa biz 10 saniye bekledikten sonra çıktık. İşte bu sorumuza WaitForSingleObject’ten geriye dönen değerin kontrolü vasıtası ile yanıt bulabileceğiz. WaitForSingleObject yada WaitForMultipleObjects’den dönen değerler aşağıdaki gibidir:

      STATUS_WAIT_0                   = $00000000;
      STATUS_TIMEOUT                 = $00000102;
    
      WAIT_OBJECT_0 = ((STATUS_WAIT_0 ) + 0 );
      WAIT_TIMEOUT = STATUS_TIMEOUT;
    

    Eğer WaitForSingleObject’in beklediği işletim sistemi nesnesinin bitmesi sebebi ile WaitForSingleObject beklemeyi bıraktı ise; bize fonksiyonun geri dönüş değeri WAIT_OBJECT_0 olacaktır. Aksi durumda, yani işletim sistemi nesnesi hâla tamamlanmamış ancak bizim belirttiğimiz bekleme zamanı dolmuş ise o zamanda WAIT_TIMEOUT değerini elde edeceğiz. Yeni kodumuz aşağıdaki gibi olacak;

    procedure TForm1.Button1Click(Sender: TObject);
    var
      mThread : TMyThread;
      RetVal    : DWord;
    begin
      mThread := TMyThread.Create;
      RetVal := WaitForSingleObject(mThread.Handle, 10000); // 10 saniye bekle..
      case RetVal of
        WAIT_OBJECT_0 : ShowMessage('Thread tamamlanmış.');
        WAIT_TIMEOUT  : ShowMessage('Thread hâla çalışıyor ancak 10 saniye dolmuş.');
      end;
    end;
    

    Sizin de kod örneklerinden anlayabileceğiniz gibi, WaitForSingleObject yada WaitForMultipleObjects fonksiyonları bekleme yaptıkları için çağırıldıkları ortamı(thread) bloke edeceklerdir. Bizim örneğimizde WaitForSingleObject formumuzun üstündeki bir button’un altında çağırıldığına göre uygulamamızın main thread’i bloke olacaktır. 10 saniyenin geçmesine müteakip main thread’imiz yine devrede olacaktır. Tabii WaitForSingleObject fonksiyonunun ikinci parametresini INFINITE olarak geçerseniz örneğimize göre uygulamamızın main thread’i 20 saniye boyunca blok olacaktır.

    Bir önceki makalemizde TThread sınıfının WaitFor isimli metodunun aynı şeyi yaptığından bahsetmiştik. Ancak o makalede de görebileceğiniz gibi, WaitFor metodu şu an sıkıntılı. Dolayısı ile yukarıda saydığımız işletim sistemi nesnelerini şimdilik WaitForSingleObject ile beklemeniz daha iyi olacaktır.

    WaitForSingleObject için yazdığımız herşey WaitForMultipleObjects içinde geçerlidir. Ancak WaitForMultipleObjects’in biraz daha farklı yönleri vardır. Farzedelim ki, uygulamanız içinde birden fazla threadiniz var ve bu thread’lerin hepsinin bitmesini beklemek istiyorsunuz. Her thread için WaitForSingleObject kullanmak hiç de mantıklı olmazdı. O zaman imdadınıza WaitForMultipleObjects yetişecek. Bir önceki örneğimizi birden fazla thread’e göre yeniden yazalım ve bu sefer WaitForMultipleObjects kullanalım:

    TMyOtherThread = class(TThread)
    private
      fSecond : DWord;
    protected
      procedure Execute; override;
    public
      constructor Create(const Second : DWord);
    end;
    
    { TMyOtherThread }
    
    constructor TMyOtherThread.Create(const Second : DWord);
    begin
      inherited Create(true);
      FreeOnTerminate := true;
      fSecond := Second;
    
      Resume;
    end;
    
    procedure TMyOtherThread.Execute;
    begin
      inherited;
    
      Sleep(fSecond * 1000);
    end;
    
    procedure TForm1.Button2Click(Sender: TObject);
    var
      mThread1,
      mThread2,
      mThread3,
      mThread4 : TMyOtherThread;
    
      Handles    : array[0..3] of THandle;
    
      StartTime,
      StopTime  : String;
      RetVal    : DWord;
    begin
      StartTime := TimeToStr(Time);
    
      mThread1 := TMyOtherThread.Create(15);
      Handles[0] := mThread1.Handle;
    
      mThread2 := TMyOtherThread.Create(10);
      Handles[1] := mThread2.Handle;
    
      mThread3 := TMyOtherThread.Create(20);
      Handles[2] := mThread3.Handle;
    
      mThread4 := TMyOtherThread.Create(25);
      Handles[3] := mThread4.Handle;
    
      RetVal := WaitForMultipleObjects(4, @Handles, true, INFINITE);
    
      case RetVal of
        WAIT_OBJECT_0 	: Memo1.Lines.Add('Thread 0 tamamlandı.');
        WAIT_OBJECT_0 + 1	: Memo1.Lines.Add('Thread 1 tamamlandı.');
        WAIT_OBJECT_0 + 2	: Memo1.Lines.Add('Thread 2 tamamlandı.');
        WAIT_OBJECT_0 + 3	: Memo1.Lines.Add('Thread 3 tamamlandı.');
      end;
    
      StopTime := TimeToStr(Time);
    
      Memo1.Lines.Add('Start Time:' + StartTime + '-Stop Time:' + StopTime);
      Memo1.Lines.Add(InttoStr(RetVal));
    end;
    

    Yukarıdaki örneğimizde, 4 adet TMyOtherThread oluşturuluyor. Birinci thread 15 sn. , ikinci thread 10 sn. , üçüncü thread 20 sn. ve son thread ise 25 sn. çalışacak şekilde ayarlanıyor. Ardından WaitForMultipleObjects fonksiyonumuza 4 adet thread’i beklemesi gerektiği ve bu thread’lerin handle’larının Handles isimli array tipli değişkende olduğu söyleniyor. Fonksiyonun üçüncü parametresi bütün thread’lerin beklenip beklenmeyeceğini ifade ediyor. Biz bu parametreye true geçerek tüm thread’lerin beklenmesini istediğimizi söylüyoruz. Dördüncü ve son parametre ne kadar süre ile beklememiz gerektiği bilgisini içeriyor. Bu parametreye INFINITE girerek tüm thread’lerin bitmesine yetecek kadar zaman beklememiz gerektiğini ifade etmiş oluyoruz.

    Bu durumda, TMemo türündeki Memo1 nesnemizde iki zaman aralığının 25 saniye olduğunu göreceksiniz. Çünkü, en uzun zaman çalışan thread 25 saniye sürüyor. Tüm thread’leri bekleyeceğimizi söylediğimiz içinde bekleyeceğimiz süre en uzun süre çalışan thread’in çalışma süresi kadar yani 25 saniye olmuş oluyor.

    Yukarıdaki örneğimizde WaitForMultipleObjects fonksiyonundan geriye dönen değeri sakladığımız RetVal isimli değişkenin değerini bir case ifadesinde kontrol ettiğimizi göreceksiniz. WaitForMultipleObjects fonksiyonu tüm thread’lerin bekleneceğini anlatan üçüncü parametresine true geçilmesi durumunda her zaman tüm thread’lerin bitmesini bekleyeceği için geriye WAIT_OBJECT_0 döndürecektir.(Son parametrenin INFINITE olması halinde, aksi durumda belirtilen zaman yeterli gelmez ise WAIT_TIMEOUT döndürür.)

    Peki tüm thread’lerin bitmesini beklemez isek ne olur ? Bu durumda WaitForMultipleObjects fonksiyonunun üçüncü parametresine false geçmemiz gerekir. Bunu yaptığımızda, çalışma süresi en kısa olan thread işini bitirdiğinde WaitForMultipleObjects artık beklemekten vazgeçecektir. Geriye dönen değer ise biten thread’in dizideki indis numarası olacaktır. Yukarıdaki örneğimizde;

  • Thread 1-> Çalışma Zamanı : 15 sn. Dizideki Indis No: 0
  • Thread 2-> Çalışma Zamanı : 10 sn. Dizideki Indis No: 1
  • Thread 3-> Çalışma Zamanı : 20 sn. Dizideki Indis No: 2
  • Thread 4-> Çalışma Zamanı : 25 sn. Dizideki Indis No: 3
  • Çalışma süresi en az olan thread “Thread 2″ olacağından WaitForMultipleObjects bize WAIT_OBJECT_0 + 1 döndürecektir. Bu mekanizma vasıtası ile birden fazla işletim sistemi nesnesinin beklendiği durumda hangi nesnenin sonlandığını bulabilirsiniz.

    Mutex

    Critical Section’lardan sonra bu mekanizmaya son derece benzeyen Mutex’lere giriş yapabiliriz. Mutex’ler tüm senkronizasyon mekanizmalarında olduğu gibi kod bloklarımızı birden fazla thread’in girmesi vesilesi ile oluşabilecek bozulmalardan korurlar. Aynı amaca critical section’larında hizmet ettiğini söylemiştik. Peki mutex’ler de aynı şeyi yapıyor ise neden varlar ?

    Aslında mutex’ler bire bir critical section’ların yaptıklarını yaparlar. Ancak fazladan güzel özelliklere sahiptirler. Bu özellikleri anlatmaya geçmeden evvel herzamanki gibi C++ ve Delphi tanımlarımıza yakından bakalım:

    HANDLE CreateMutex(
      LPSECURITY_ATTRIBUTES lpMutexAttributes,
      BOOL bInitialOwner,
      LPCTSTR lpName
    );
    
    HANDLE OpenMutex(
      DWORD dwDesiredAccess,
      BOOL bInheritHandle,
      LPCTSTR lpName
    );
    
    BOOL ReleaseMutex(
      HANDLE hMutex 	
    );
    

    ve Delphi tanımları:

      function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName: PChar): THandle; stdcall; 
      function OpenMutex(dwDesiredAccess: DWORD; bInheritHandle: BOOL; lpName: PChar): THandle; stdcall;
      function ReleaseMutex(hMutex: THandle): BOOL; stdcall;
    

    Bir mutex senkronizasyon nesnesi CreateMutex API’si ile oluşturulur ve CloseHandle API’si ile de işletim sisteminden silinir. Mutex’lerin critical section’lara son derece benzediğini söylemiştik. Hatırlarsanız critical section’larda InitializeCriticalSection, DeleteCriticalSection, EnterCriticalSection ve LeaveCriticalSection metodlarını kullanıyorduk. Mutex ve critical section fonksiyonlarının benzerlikleri aşağıdaki tablodan gözlemlenebilir:

    Critical Section Mutex
    InitializeCriticalSection CreateMutex / OpenMutex
    EnterCriticalSection WaitForSingleObject
    LeaveCriticalSection ReleaseMutex
    DeleteCriticalSection CloseHandle

    Tüm bu bilgilerin ışığında bir critical section’un yaptığı işi mutex ile yapan bir örneği inceleyebiliriz;

    type
    TMutexThread = class(TThread)
    private
      fName,
      fStartValue,
      fStopValue	: String;
    protected
      procedure Execute; override;
    public
      constructor Create(const AName : String);
      destructor Destroy; override;
    
      property StartValue : String read fStartValue;
      property StopValue 	: String read fStopValue;
      property Name		: String read fName;
    end;
    
    var
      Form1: TForm1;
      MutexHandle   : THandle;
    
    implementation
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      MutexHandle := CreateMutex(nil, false, nil);
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      CloseHandle(MutexHandle);
    end;
    
    { TMutexThread }
    constructor TMutexThread.Create(const AName: String);
    begin
      inherited Create(true);
      FreeOnTerminate := true;
    
      fName := AName;
    
      Resume;
    end;
    
    destructor TMutexThread.Destroy;
    begin
      form1.Memo1.Lines.Add('Mutex Name:' + Name + ' Start Time:' + StartValue + ' Stop Time:' + StopValue);
    
      inherited;
    end;
    
    procedure TMutexThread.Execute;
    begin
      inherited;
    
      WaitForSingleObject(MutexHandle, INFINITE);
      fStartValue := TimeToStr(Time);
    
      Sleep(10000);
    
      fStopValue := TimeToStr(Time);
      ReleaseMutex(MutexHandle);
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      mMutex1,
      mMutex2 : TMutexThread;
    begin
      mMutex1 := TMutexThread.Create('Birinci Mutex');
      Sleep(2000); // 2 sn bekle..
      mMutex2 := TMutexThread.Create('İkinci Mutex');
    end;
    

    Yukarıdaki örneğimiz tıpkı critical section örneğindeki gibi çalışacaktır. “Birinci Mutex” çalışmasına başlayacak ve WaitForSingleObject ile global MutexHandle değişkeni vasıtası ile kapının açık olup olmadığını sorgulayacak ve kapı açık olduğundan hemen koda girecektir. “İkinci Mutex” 2 saniye sonra birincisi gibi korumalı koda girmeye çalışacak ama kapı kapalı olduğu için beklemeye başlayacaktır. “Birinci Mutex” thread’i ReleaseMutex çağrısı yapar yapmaz “İkinci Mutex” thread’i korumalı koda girebilecektir. Örnek uygulamamızın çıktısı:

  • Mutex Name:Birinci Mutex Start Time:20:44:28 Stop Time:20:44:38
  • Mutex Name:İkinci Mutex Start Time:20:44:38 Stop Time:20:44:48
  • biçiminde olacaktır. Görüldüğü gibi “İkinci Mutex” birincisini beklemiş ve ardından çalışmaya devam etmiştir.

    Bu noktaya kadar mutex’lerin critical section’lardan bir farkı görünmüyor. O halde neden mutex kullanacağız ? Mutex’lerin esas gücü, senkronizasyon mekanizmasını process’ler arasında da yapabilmesinden geliyor. CreateMutex tanımını hatırlayacak olursanız, 3ncü ve son parametresinde bir isim alabildiğini gözlemleyeceksiniz. Bu parametreye geçerli bir isim verilir ise, mutex’ler işletim sistemi üzerindeki birden fazla process arasında senkronizasyon yapabilme yeteneğine kavuşur.

    Bazı zamanlarda thread ile erişeceğimiz kritik kodlar, işletim sistemi üzerindeki bazı kaynaklara tekil erişim gerektirirler. Örneğin; makinanıza programlanabilme imkanına sahip bir donanımın bağlı olduğunu düşünelim. Bu herhangi bir donanım olabilir. Diyelim ki, bir modeminiz var ve siz bu modemin BIOS’unu güncelleyebilen bir program yazdınız. Modem’in BIOS’u güncelleme işlemi sürer iken bir başka güncelleme isteğinin modem’e yönlendirilmemesi gerekir. Yazdığınız uygulamanın derlenmiş halinin ModemBIOS.exe olduğu düşünüldüğünde, bu uygulamanın içinde Modem’e erişip BIOS’u güncelleyen kodun bir daha çalıştırılmamasını sağlayabilirsiniz. Ancak sizin uygulamanızı kullanan kullanıcının ModemBIOS.exe programını bir tane daha açması durumunda ne olur ?

    Bu durumda ModemBIOS.exe process’inden o anda çalışan iki adet olur. Ve her bir process kendi hafıza alanına sahip olduğu için , process’ler birbirlerinin kritik kod bloklarını bilemezler. Bu gibi bir durumda, ikinci ModemBIOS.exe uygulamanızda modem’in BIOS’unu güncellemeye çalışabilir ve geri dönülmesi zor donanımsal hatalara neden olabilirsiniz.

    İşte tam bu noktada mutex’lerin gücü devreye girer. Birinci uygulamanın mutex’i oluşturduğunu ve modem’in bios’unu kritik kod bloğunda güncellediğini düşünelim. İkinci uygulama artık CreateMutex ile değil, OpenMutex ile mutex’e verilen isim vasıtası ile mutex’i açmaya çalışır. Bu durumda elde edilecek mutex handle’ı birinci uygulamanın create ettiği handle olacaktır.! Elde edilen bu handle ile BIOS’u güncelleyecek olan kod bloğuna girmeye çalışacak olan ikinci uygulamamız ; birinci uygulama o kod bloğundan çıkmadan içeriye giremeyecektir. Bu durumu simüle eden bir örnek yapalım arzu ederseniz;

    type
    TMutexThread = class(TThread)
    private
      fName,
      fStartValue,
      fStopValue	: String;
    protected
      procedure Execute; override;
    public
      constructor Create(const AName : String);
      destructor Destroy; override;
    
      property StartValue : String read fStartValue;
      property StopValue 	: String read fStopValue;
      property Name		: String read fName;
    end;
    
    var
      Form1: TForm1;
      MutexHandle   : THandle;
    implementation
    
    { TMutexThread }
    constructor TMutexThread.Create(const AName: String);
    begin
      inherited Create(true);
      FreeOnTerminate := true;
    
      fName := AName;
    
      Resume;
    end;
    
    destructor TMutexThread.Destroy;
    begin
      form1.Memo1.Lines.Add('Mutex Name:' + Name + ' Start Time:' + StartValue + ' Stop Time:' + StopValue);
    
      inherited;
    end;
    
    procedure TMutexThread.Execute;
    begin
      inherited;
    
      WaitForSingleObject(MutexHandle, INFINITE);
      fStartValue := TimeToStr(Time);
    
      Sleep(10000);
    
      fStopValue := TimeToStr(Time);
      ReleaseMutex(MutexHandle);
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    var
      Error : DWord;
    begin
      MutexHandle := OpenMutex(MUTEX_ALL_ACCESS, false, PAnsiChar('Modem BIOS'));
      if MutexHandle = 0 then
      begin
        Memo1.Lines.Add('Mutex bulunamadı, oluşturulacak.');
        MutexHandle := CreateMutex(nil, false, PAnsiChar('Modem BIOS'));
        Error := GetLastError();
    
        if Error = ERROR_INVALID_HANDLE then
          Memo1.Lines.Add('Modem BIOS ismi daha önce mutex harici başka bir senkronizasyon nesnesinde kullanılmış.!');
      end
      else Memo1.Lines.Add('Mutex bulundu ve OpenMutex ile açıldı.');
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      CloseHandle(MutexHandle);
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      mMutex1,
      mMutex2 : TMutexThread;
    begin
      Label1.Caption := 'Başlangıç Zamanı:' + TimeToStr(Time);
      mMutex1 := TMutexThread.Create(Edit1.Text);
      Sleep(2000); // 2 sn bekle..
      mMutex2 := TMutexThread.Create(Edit2.Text);
    end;
    

    Yukarıdaki örneğimizde, button1′in OnClick olay yöneticisinde iki adet thread çalıştırılıyor. Bu thread’ler mutex senkronizasyon nesnelerini kullanıyorlar. Bu uygulama birden fazla sefer çalıştırıldığında ve ilgili button’a basıldığında çalışacak olan kodlar birbirlerini bekleyeceklerdir. Mutex’leri critical section’lardan farklı kılan en önemli özellik budur. Çalışacak olan thread’ler hangi process’de olurlarsa olsunlar aynı anda sadece bir tanesinin çalışmasını sağlamış olduk.

    Formumuzun OnCreate olayında “Modem BIOS” olarak isimlendirdiğimiz bir mutex’i OpenMutex API’si vasıtası ile açmaya çalışıyoruz. Eğer bu isimde bir mutex daha önce oluşturuldu ise, OpenMutex fonksiyonu oluşturulan mutex’in handle’ını döndürür aksi durumda sıfır döndürecektir. Process’ler arası senkronizasyonun en can alıcı noktası da burasıdır. Mutex’lerin bu özelliği aynı zamanda sıklıkla; çalıştırılan uygulamanın birden fazla örneğinin olmaması için de kullanılmaktadır.

    Eğer OpenMutex daha önce oluşturulmuş bir mutex bulamaz ise, o zaman mutex’i CreateMutex API’si vasıtası ile oluşturma yoluna gidiyoruz. CreateMutex fonksiyonunundan hemen sonra işletim sistemi fonksiyonlarından olan GetLastError ile son hata durumunu kontrol ediyoruz. CreateMutex API’si, kendisine geçilen isim ile(örneğimizde “Modem BIOS”) daha önce oluşturulmuş mutex harici bir nesne var(event, semaphor gibi) ise ERROR_INVALID_HANDLE değerini döndürecektir. Çünkü, event, semaphor, mutex gibi senkronizasyon nesneleri aynı isimlendirme alanını ortak kullanırlar.

    Mutex’imizi formumuzun OnCreate olayında açtıktan yada oluşturduktan sonra mutex senkronizasyon nesneleri ile korumaya alınmış thread kodlarımızın çalıştırılmasını sağlıyoruz. Aşağıda yayınlayacağımız video’dan da görebileceğiniz gibi; birden fazla aynı uygulama çalıştırıldığında button’lara basma sırasına göre thread’ler sıraya girecekler ve hangi uygulamadan çalıştırıldıklarından bağımsız olarak birbirlerini bekleyebileceklerdir.

    Semaphore

    Mutex senkronizasyon nesnelerinden sonra semaphore’lar hakkında da biraz konuşmamız gerekir. Bazen kritik olduğunu düşündüğümüz kod bloklarımıza sadece bir thread’in değil bizim belirlediğimiz sayıda thread’in girebilmesini de isteriz. Semaphore’ler bu amaca hizmet eden senkronizasyon mekanizmalarıdır. Semaphore’ler da tıpkı mutex’lerde olduğu gibi bir isimle yada isimsiz olarak oluşturulabilirler. İsimle oluşturulduklarında tahmin edeceğiniz üzere, birden fazla process’in birden fazla thread’ini sorunsuz bir şekilde senkronize edebilirler. Gelin herzamanki gibi önce tanımlarına bir göz gezdirelim:

    HANDLE CreateSemaphore(
      LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
      LONG lInitialCount,
      LONG lMaximumCount,
      LPCTSTR lpName
    );
    
    HANDLE OpenSemaphore(
      DWORD dwDesiredAccess,
      BOOL bInheritHandle,
      LPCTSTR lpName
    );
    
    BOOL ReleaseSemaphore(
      HANDLE hSemaphore,	
      LONG lReleaseCount,	
      LPLONG lpPreviousCount
    );
    
      function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes;
      lInitialCount, lMaximumCount: Longint; lpName: PChar): THandle; stdcall;
    
      function OpenSemaphore(dwDesiredAccess: DWORD; bInheritHandle: BOOL; lpName: PChar): THandle; stdcall;
    
      function ReleaseSemaphore(hSemaphore: THandle; lReleaseCount: Longint;
      lpPreviousCount: Pointer): BOOL; stdcall;
    

    Bir semaphore nesnesi, tıpkı mutexlerde olduğu gibi CreateSemaphore ile oluşturulur ve CloseHandle ile işletim sisteminden silinir. Mutex’lerde olduğu gibi semaphore’lar da da WaitForSingleObject kritik kod bloğunun başında yer alır. Kritik kod bloğu ReleaseSemaphore ile sonlandırılır. Semaphore nesnelerinin en önemli özelliğinin bir sayaç mekanizmasına sahip olduğunu ifade etmiştik. CreateSemaphore fonksiyonunun ikinci ve üçüncü parametreleri son derece önemlidir. İkinci parametre semaphore sayacının başlangıç değerini, üçüncü parametre ise kritik koda girebilecek maksimum thread sayısını ifade eder. Bu parametrelerin sahip olabilecekleri değerlere ilişkin kurallar aşağıda belirtildiği gibidir.

      lInitialCount >= 0 veya lInitialCount <= lMaximumCount
      lMaximumCount > 0
    

    Her WaitForSingleObject çağrısı semaphore mekanizmasında sayacın bir azaltılmasını sağlar ve yine her ReleaseSemaphore çağrısı sayacın bir arttırılmasına neden olur.Örneğin;

    var
      SemaphoreHandle : THandle;
    begin
      SemaphoreHandle := CreateSemaphore(nil, 2, 2, PAnsiChar('Semaphore Test')); // Sayaç değeri 2'dir.
    
      WaitForSingleObject(SemaphoreHandle, INFINITE); // Sayaç değeri 1'e iner.
      ..
      ..
      ReleaseSemaphore(SemaphoreHandle, 1, nil); // Sayaç değeri yeniden 2'ye çıkar.
    end;
    

    Yukarıda gördüğünüz basit yapıda olduğu gibi bir semaphore sizin belirttiğiniz sayaç değeri ile başlar ve her WaitForSingleObject gördüğünde bu sayaç değeribi bir azaltır. Sayaç değeri sıfıra ulaştığında içeri girmeye çalışacak olan thread’ler bekletilmeye başlanır. Bu sayede kritik kod bloğuna sizin belirttiğiniz sayıda thread’in girmesi sağlanmış olur. Her ReleaseSemaphore çağrısı sayacı 1 arttıracaktır. Sayaç sıfırdan farklı bir değere ulaşır ulaşmaz, beklemekte olan thread’lerden bir tanesi hemen kritik kod bloğuna girecektir. Semaphore’larla ilgili kod örneğine geçmeden önce, OpenSemaphore fonksiyonunun birinci parametresindeki erişim sabitlerinin Delphi 7′de tanımlanmamış olduğunu hatırlatmakta fayda görüyorum. Delphi 2009′dan aldığım sabit tanımın Delphi 7 kullanan arkadaşlarımız için faydalı olacağına inanıyorum;

      SEMAPHORE_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED or SYNCHRONIZE or $3);
    

    Şimdi isterseniz, sayaç mekanizması vasıtası ile kritik kod bloklarını senkronize eden semaphore’ler ile ilgili basit bir örnek yapalım. Bu örnek mutex’lerle son derece benzer olacak;

    type
    TSemaphoreThread = class(TThread)
    private
      fName,
      fStartValue,
      fStopValue	: String;
    protected
      procedure Execute; override;
    public
      constructor Create(const AName : String);
      destructor Destroy; override;
    
      property StartValue : String read fStartValue;
      property StopValue 	: String read fStopValue;
      property Name		: String read fName;
    end;
    
    var
      Form1: TForm1;
      SemaphoreHandle : THandle;
    
    implementation
    
    procedure TForm1.FormCreate(Sender: TObject);
    const
      SEMAPHORE_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED or SYNCHRONIZE or $3);
    var
      Error : DWord;
    begin
      SemaphoreHandle := OpenSemaphore(SEMAPHORE_ALL_ACCESS, false, PAnsiChar('Semaphore Test'));
      if SemaphoreHandle = 0 then
      begin
        Memo1.Lines.Add('Semaphore bulunamadı, oluşturulacak.');
        SemaphoreHandle := CreateSemaphore(nil, 2, 2, PAnsiChar('Semaphore Test'));
        Error := GetLastError();
    
        if Error = ERROR_INVALID_HANDLE then
          Memo1.Lines.Add('Semaphore Test ismi daha önce semaphore harici başka bir senkronizasyon nesnesinde kullanılmış.!');
      end else Memo1.Lines.Add('Semaphore bulundu ve OpenSemaphore ile açıldı.');
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      CloseHandle(SemaphoreHandle);
    end;
    
    { TSemaphoreThread }
    
    constructor TSemaphoreThread.Create(const AName: String);
    begin
      inherited Create(true);
      FreeOnTerminate := true;
    
      fName := AName;
    
      Resume;
    end;
    
    destructor TSemaphoreThread.Destroy;
    begin
      form1.Memo1.Lines.Add('Semaphore Name:' + Name + ' Start Time:' + StartValue + ' Stop Time:' + StopValue);
    
      inherited;
    end;
    
    procedure TSemaphoreThread.Execute;
    begin
      inherited;
    
      WaitForSingleObject(SemaphoreHandle, INFINITE);
      fStartValue := TimeToStr(Time);
    
      Sleep(10000);
    
      fStopValue := TimeToStr(Time);
      ReleaseSemaphore(SemaphoreHandle, 1, nil);
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      mThread1,
      mThread2,
      mThread3,
      mThread4	: TSemaphoreThread;
    begin
      mThread1 := TSemaphoreThread.Create('Semaphore 1');
      Sleep(1000);
    
      mThread2 := TSemaphoreThread.Create('Semaphore 2');
      Sleep(1000);
    
      mThread3 := TSemaphoreThread.Create('Semaphore 3');
      Sleep(1000);
    
      mThread4 := TSemaphoreThread.Create('Semaphore 4');
    end;
    

    Yukarıdaki kod örneğimiz, 4 adet thread oluşturup çalıştırılmasını sağlıyor. Semaphore’umuz kritik kod bloğuna 2 adet thread’in girebileceği bilgisi ile oluşturulmuş durumda olduğu için aynı anda kritik kod bloğunda maksimum 2 thread’imiz olabilecektir. Diğer iki thread, kritik kod bloğuna giren thread’lerden dışarı çıkan olmadığı müddetçe bekleyeceklerdir.Kritik kod bloğundan bir thread çıkar çıkmaz bekleyen thread’lerden birisi hemen içeri girebilecektir. Semaphore’larda da isimlendirme özelliği olduğu için çalışan thread’lerin hangi process’de çalıştıklarının bir önemi yoktur. Bu teknikte process’ler arası senkronizasyon sayaç mekanizması ile sağlanmış olur.

    Bundan sonraki hedefimizin Event ve Waitable Timer’lar olduğunu söyleyerek bu makaleyi neticelendirmek istiyorum müsaadeniz ile. Son derece uzun olduğu kanaatindeyim, dolayısı ile makaleyi daha da uzatmamak ve okunurluğu arttırabilmek adına diğer senkronizasyon mekanizmalarını bir sonraki makaleme taşıyacağım.

    Bu makalemizde aslında thread konusunun basit bir konu olmadığı, ancak korkulara da yer olmadığını umarım anlatabilmişimdir. Thread’lerin diğer kodlama tekniklerinden tek farkı benim bakış açıma göre, sadece daha dikkatle kodlanmalarının zorunluluğu ve biraz işletim sistemi bilgisinin gerekliliğidir.

    Sizlere bol multi-threaded’lı günler dilerim :)

    Saygılar, sevgiler..

    “Derinlemesine Threading..(1)” için 31 Yorum

    1. sadettinpolat diyor ki:

      threadler hakkinda cok guzel bir yazi olmus , eline , sabrina , bilgine saglik. threadler ile ilgili 2. makaleyi hatta 3. makaleyi hatta 4. makaleyi beklemekteyiz :)

      threadler ile vcl
      threadler ile database islemleri

      threadler ile ilgili olarak aklima gelen ilk soru ana prosesten calismasi devam eden bir threadi sorgusuz sualsiz durdurabilir miyiz ?

      mesela threadin execute metodu su sekilde yazilmis olsun

      while true do
      noop

      dongu icerisinde her hangi bir if Terminated then tarzi bir komut yok ya da islem suresi 10 dakika surecek olan bir sql.open cagrisi yaptik ama sonra vazgecmek istedik :)

    2. Tuğrul HELVACI diyor ki:

      Teşekkür ederim. Devamını da yazacağım ama devamında Event ve Waitable Timer senk. mekanizmalarını yazmayı düşünüyorum. Thread’ler içinde COM kütüphanesi kullanan her kod için CoInitialize/CoUnInitialize kullanmak gerekiyor. Veritabanı bağlantılarının ise thread’e has olması gerekiyor.

      Bir thread’i dışarıdan sonlandırmaya gelince, evet bu mümkün. TerminateThread ile. Ancak pek de tavsiye edilen bir yöntem değil. Thread başlangıç kodlarında ayrılmış hafıza bloklarınız var ise hali ile o hafıza blokları yaşamaya devam edecekler TerminateThread kullanılırsa.

      Bir thread içindeki Sql komut’larının çalıştırılmasından vazgeçmek için RecordSet’in Cancel mekanizması kullanılıyordu yanlış hatırlamıyorsam. Yarın işe gittiğimde bu soruya daha kesin bir yanıt verebilirim.

      Thread’i sonlandırmak için bir çare yok ise TerminateThread son çare :)

    3. Tuğrul HELVACI diyor ki:

      VCL konusuna gelince o konu hakkında şu aşamada yapılabilecek pek fazla şey yok maalesef :( Thread çalışma mekanizmalarında bir thread’e dışarıdan gelen SendMessage çağrıları ortalığı hallaç pamuğu gibi atabiliyor. Herne kadar GetMessage yordamı önce başka thread’lerden gelen SendMessage çağrılarını işlese de o anda thread’in bir şekilde bloke olması göndereni de bloke ediyor.

      Bu sebeple VCL’i thread içinde kullanmak şu aşamada her zaman beklenmedik sorunlara neden olabiliyor. Görsel bileşenler halihazırda işletim sisteminden gelen tonlarca mesaja yanıt verdikleri için thread’ler içinde güvenle kullanılmaları şu tasarım hali ile pek mümkün değil. İlerleyen zamanlarda VCL’in thread güvenli hale getirilmesi yönünde istekler var ama bu isteklere Embarcadero ‘da pek sıcak bakmıyor sanırım.

      Pek kolay bir iş olduğu söylenemez çünkü. Dolayısı ile, VCL görsel nesnelerine illa da thread’lerimiz içinden erişmemiz gerekiyor ise klasik çözüm Synchronize’a başvurmamız en güvenli yol gibi görünüyor.

    4. Ferruh Koroglu diyor ki:

      Tuğrul kardeş, eline sağlık.

      Benim msn mail adresim yazdığım mesajda gözüküyor.

      Msn kullanıyarsan beni msn’e eklersen sevinirim.

      Böyle değerli bir kardeşimiz ile ara ara msn üzerinden görüşmek isteriz aynı Sadettin Hocam gibi :)

      Kolay gelsin…

    5. Tuğrul HELVACI diyor ki:

      Teşekkür ederim, ekledim. Ayrıca Sadettin’e ulaşabilirsen o da bana ulaşsın lütfen. Ona thread’lerle ilgili sorduğu soruya istinaden işyerinde yazdığım component’i yollayacağım.

    6. Veli BOZATLI diyor ki:

      Çok güzel, çok yararlı bir makale.
      Emeğinize sağlık.

    7. sadettinpolat diyor ki:

      benim msn adresim sadettinpolat malum işaret yahoo nokta kom :)

    8. sadettinpolat diyor ki:

      genelde gtalk acik olur. gtalk adresimde sadettinpolat malum isaret cimeyil nokta kom) 7×24 online :)

    9. Tuğrul HELVACI diyor ki:

      Teşekkürler Veli Bey.

    10. Yusuf ÇELİK diyor ki:

      Yıllardır Delphi kullanıyorum.
      Yazını çok beğendim Tuğrul Hocam.
      Yazılanların ne kadar değerli olduğunu anlamak için alim olmaya gerek yok. Bu işi biraz bilmek yeterli.
      Yazıcıdan çıktı aldım, akşam servisle eve dönerken bana zevkle okumam için bir dökuman sundun.

      Ben de Delphi OTA ile ilgili bir yazı hazırlamıştım yıllar önce ziyaretçilerin ilgisini çekerse http://yusufcelik.googlepages.com/ adresinde bulabilirsiniz.

      Saygılar

      • Tuğrul HELVACI diyor ki:

        Beğenmenize sevindim Yusuf bey. Umarım işyeriniz ile eviniz arasındaki mesafe makaleyi okumanıza yetecek düzeyde uzundur :) Sizin sayfanızı da linkler bölümüne taşıyayım ki bilgiye aç olan arkadaşlarımız sizin bilgilerinizden de istifade edebilsinler.

    11. Tuncay ÖZER diyor ki:

      Hocam elinize kolunuza dilinize gözünüze vs.. sağlık. Çok güzeL makaLeLer yazmışsınız. beni de MSN adresinize ekLerseniz sevinirim. 1994 ten beridir Pascal ve Delphi kuLLanmaktayım. Bir türlü şu Visual Studio IDE sine de alışamadım gitti ::)))

      • Tuğrul HELVACI diyor ki:

        Sağolun Tuncay bey. Sizi de ekledim MSN’ime. Sıkı bir Delphi’ci olarak Visual Studio’nun kod yazmak için editörünü beğendiğimi söyleyebilirim. Ama geri kalanı sormayın gitsin :)

    12. Yusuf ÇELİK diyor ki:

      Tekrar Selam Tuğrul Hocam,

      Bişey dikkatimi çekti.
      Execute methodu parent class (TThread) da abstract virtual olarak tanımlanmış.
      Bu durumda Execute methodunu tanımlarken inherited’i çağımamamız gerekir.
      Çünkü Execute methodu TThread class’ında yalnızca tanımlanmıştır, impleme edilmemiştir (abstract’ın anlamı) ?
      Yanlış mı düşünüyorum?
      Ya da benim kaçırdığım başka bişey mi var?
      Tşk…

    13. Yusuf ÇELİK diyor ki:

      Nerydeyse unutuyorum, aslında benim MSN’imde hiç delphi ci yok.
      Aslında olsa iyi olur, bazen takıldığımızı soruları birbirimize sorarız.
      Bilgi paylaştıkça değerlidir.
      Tekrar Tşk

    14. Tuğrul HELVACI diyor ki:

      Merhaba Yusuf bey, bir ata sınıfta virtual; abstract olarak tanımlanmış bir metodun mirasçı sınıflarında aynı isme sahip bir başka metod yok ise (static yada override edilmiş) o zaman bu metodun çağrımı abstract error ile neticelenir. Ancak üst sınıfta virtual; abstract olarak tanımlanmış bir metod alt sınıflarda override ediliyor ise yada aynı isme sahip bir başka metod static olarak tanımlanıyor ise herhangi bir hata ile karşılaşmayız.

      Derleyicinin kodunu bilemediğimiz için abstract metod çağrımlarında derleyicinin tam olarak ne yaptığından emin olamayız ama bir fikir yürütebiliriz. Bana göre; derleyici bir metod çağrımı sırasında ilgili nesne için Virtual Method Table(VMT) üzerinde bir arama gerçekleştiriyor ve ata sınıfta virtual; abstract olarak tanımlanmış olan bir metodu ezen alt sınıf metodlarındaki inherited çağrımını dikkate almıyor.

      Üst sınıfta virtual; abstract olarak tanımlı bir metodun alt sınıflarda override edilmesine müteakip Ctrl+Shift+C tuş kombinasyonlarına basar isek inherited çağrımının otomatik geldiğini görebiliriz. Derleyicinin muhtemel sorunları elimine ettiğinin bir göstergesidir aslında inherited çağrımının otomatikman oluşması.

      Not: MSN adresinizi belirtirseniz sizi eklemekten mutluluk duyarım.
      Saygılarımla

    15. Yusuf ÇELİK diyor ki:

      Teşekkürler,
      Sanırım Derleyici dikkate almıyor.
      Ancak inherited yerine inherited Execute (Ki aynı anlama geliyor kanımca) yazınca o zaman problem çıkarıyor.
      yusufcelik 65 Malum işaret hotmail.com
      65 ile adım arasında boşluk yoktur (Tahmin ettiğiniz gibi:-))

      Syg.

    16. sadettinpolat diyor ki:

      inherited meselesinde bende tugrul abiye katiliyorum , dikkate almiyor sanirim. ayrica inherited execute olarak ta denedim hata vermeden calisti bende. (delphi 2007)

    17. Yusuf ÇELİK diyor ki:

      :) Teşekkürler.
      Sadettin hocam, MSN Delphi bağlantılarımı oluşturmaya başladım.
      Dün sağolsun Tuğrul hocam beni ilk Delğhi (Benim için) listesine ekledi.
      Siz de eklerseniz sevinirim.
      Tşk…
      yusufcelik 65 Malum işaret hotmail.com
      65 ile adım arasında boşluk yoktur (Tahmin ettiğiniz gibi:-))

      Not: Okuldan gelen bir alışkanlıkla ilk tanıdığım insanlara hep hocam diye hitap ederim, yani üstad anlamında.

    18. Olcay DAĞLI diyor ki:

      İşlerimin yoğunluğu sebebiyle makaleyi bit türlü bitiremedim :( Şanslıyım ki bu bilgilerin pek çoğunu birebir senden öğrenme fırsatını yakaladım, yorumları okuyunca da ne kadar şanslı olduğumu bir kez daha anlıyorum hocam. Bu güne kadar threadler ve diğer bütün konularda öğrettiğin herşey için tekrar teşekkürler hocam. Yoksa bende, pek çok araştırmadan yoksun programcı gibi, mainthreadin dahi ne olduğunu bilmeden object orientied dünyadan bi haber biri olacaktım.
      Tekrar teşekkürler, eline koluna sağlık…

    19. Tuğrul HELVACI diyor ki:

      Derecelendirme üzerine bir not:

      Bu makaleye 5 üzerinden 1 veren ve 5 üzerinden 2 veren iki arkadaşımızın makalede hangi konuyu beğenmediklerini de bilebilseydik keşke. Belki kendimizi geliştirme imkanına sahip olurduk. Eleştirel yaklaşımlarınızı da paylaşabilirseniz sevinirim.

    20. Yusuf ÇELİK diyor ki:

      Makaleye nereden not veriliyor?

    21. Mehmet SARIGİL diyor ki:

      Makale oldukca guzel olmus, sadece bir hatirlatma yapmak istedim.

      1-Wait Fonksiyonları ile ilgili olarak, eger bir thread window create ediyorsa (ki bazi durumlarda bu window create etme işi çağrılan fonksiyon tarafından yapılıyor mesela CoInitialize fonksiyonu window create ediyor) Wait fonksiyonlarında MsgWaitForMultipleObjects i kullanmak gerekiyor. Aksi halde WairForSingle/MultipleObjects çagrılırsa işletim sisteminin kararsız olmasına neden olunabilliyor.

      2- “Bir mutex senkronizasyon nesnesi CreateMutex API’si ile oluşturulur ve CloseHandle API’si ile de işletim sisteminden silinir”

      Cümlesinde de aslinda CloseHandle ile nesne işletim sisteminden kesin olarak silinmeyebilir, cunku handle lar için işletim sisteminde reference couting mekanizması vardır. Her bir closehandle için bu count bir azalir ve eger sifir olursa nesne işletim sisteminden silinir.

      Saygilar sunarim.
      ,

    22. Tuğrul HELVACI diyor ki:

      Yorumlarınıza teşekkür ederim Mehmet bey. Ancak; CoInitialize yada CoInitializeEx API’lerinin sadece COM init. işlemleri yaptığını biliyordum, pencere(window) oluşturduklarına dair bir bilgim yoktu. Bu bilgiyi nereden elde ettiğinizi öğrenebilirsem okuyup bende detaylarını öğrenmek isterim.

    23. Mehmet SARIGİL diyor ki:

      Tugrul bey, gec cevabim icin kusura bakmayin.

      WaitForSingleObject in MSDN deki açıklamalarında bahsettigim hususlar var.

      Saygilar,

    24. Ömür Ölmez diyor ki:

      Makalenizin için teşekkürler. Emeğinize sağlık.

      Bir de, memur, elektrik faturası ödemeye gelen aboneler ve müstahdem yerine; doktor, muayeneye gelen hastalar ve hemşire kullanabiliriz …

      • Tuğrul HELVACI diyor ki:

        Anlatmak istediğimi anlatabildi isem, örnek amacına ulaşmış demektir. Elbette sizin de belirttiğiniz gibi gerçek hayattan pek çok örnek verilebilir.

    25. amine diyor ki:

      çok teşekkür ederim Allah razı olsun :) ) bişeyler daha iyi oturdu, her ne kadar delphi kullanmasam da.. önemli olan mantık :) tekrar teşekkürler….

    26. Ahmet YEŞİLÇİMEN diyor ki:

      Makarnada salçam olsun diyerekten, hiç sıkılmadan kodların davranışlarını ve oluşabilecek her
      istisnayı senaryolar üzerine kurarak neden sonuç ilişkisi üzerinde incelerim. Konu anlatılırken
      atlanmış bir olayın izahatını izninle gerçekleştirmek istiyorum hocam :)

      Showmessage(’1′);
      Showmessage(’2′);
      Showmessage(’3);

      Bu kodlar oldukça sessiz ve sakin bir şekilde burada derlenmeyi bekliyorlar değil mi ? :)
      1 2 3 mesajı sırasıylamı gösterilecek yoksa 1 3 2 yada 2 1 3 yada 2 3 1 yada 3 2 1 yada 3 1 2
      olarak mı gösterilecek nacizane olarak söylemek istiyorum 1 2 3 olarak listelenecek oldukça
      ilgiç değil mi :D
      peki ya aşşağıdaki kodlara ne diyorsunuz ? Amaaa lütfen aşşağıdaki açıklamaları okumadan önce çalıştırıp deneyiniz.
      Tuğrul hocamın hazırlamış olduğu örneğe küçük bir eklenti yaptım. Olmasada olurmu ? Elbette olur amacım
      görsel olarak daha iyi anlatabilmektir.

      Button1 in OnClick olayına girelim ve okuyalım.

      [DELPHI]
      unit dn;

      interface

      uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, StdCtrls;

      type
      TMutexThread = class(TThread)
      private
      fName,
      fStartValue,
      fStopValue : String;
      protected
      procedure Execute; override;
      public
      constructor Create(const AName : String);
      destructor Destroy; override;

      property StartValue : String read fStartValue;
      property StopValue : String read fStopValue;
      property Name : String read fName;
      end;
      TForm1 = class(TForm)
      Memo1: TMemo;
      Button1: TButton;
      Button2: TButton;
      procedure FormCreate(Sender: TObject);
      procedure Button1Click(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
      procedure Button2Click(Sender: TObject);
      private
      { Private declarations }
      public
      { Public declarations }
      end;

      var
      Form1: TForm1;
      MutexHandle : THandle;
      Str:string;

      implementation

      {$R *.dfm}

      procedure TForm1.Button1Click(Sender: TObject);
      var
      mMutex1,
      mMutex2,mMutex3 : TMutexThread;
      begin
      Memo1.Clear;
      mMutex1 := TMutexThread.Create(‘Birinci Mutex’);
      WaitForSingleObject(mMutex1.Handle,INFINITE);
      mMutex2 := TMutexThread.Create(‘İkinci Mutex’);
      WaitForSingleObject(mMutex2.Handle,INFINITE);
      mMutex3 := TMutexThread.Create(‘Üçüncü Mutex’);

      {

      Bu kodlar çok masum öyle değil mi ? :)
      şimdi çalıştırdığınızda memo nesnesine Sırasıyla
      Birinci Mutex
      İkinci Mutex
      Üçüncü Mutex
      olarak olarak listeleneceğini hepimiz doğrudan istisnasız nedensiz düşünürüz.
      Fakat çalıştırmaya başlayın Button1 i 3 4 kere belki 4 5 kere çalıştırın. Taki memo daki görüntü
      olması gereken in tersi olana kadar
      Birinci Mutex
      Üçüncü Mutex
      İkinci Mutex
      gibi..

      sonra Button 2 ye tıklayalım memo daki durum ters olmasına rağmen showmessage da ne yazacak bize ?
      Birinci Mutex İkinci Mutex Üçüncü Mutex .. her durumda bu olacaktır neden peki ? çünkü kanalların çalışma sıraları
      yani önce mutex1 sonra mutex en son ise mutex 3 çalıştığı için kanalın Constructor un da
      Str := Str + AName + ‘ ‘; kodu bulunmakta ve AName str e eklemekte kodlar bir önce ki dizilişte mutext1..3
      çalıştığı için sıralanışta bu şekilde olacaktır her neyse…. öncelikle çalıştıralım ve bir düşünelim..açıklama aşşağıda.
      }
      end;

      procedure TForm1.Button2Click(Sender: TObject);
      begin
      ShowMessage(Str);

      end;

      procedure TForm1.FormCreate(Sender: TObject);
      begin
      MutexHandle := CreateMutex(nil, false, nil);
      end;

      procedure TForm1.FormDestroy(Sender: TObject);
      begin
      CloseHandle(MutexHandle);
      end;

      { TMutexThread }

      constructor TMutexThread.Create(const AName: String);
      begin
      inherited Create(true);
      FreeOnTerminate := true;

      fName := AName;
      Str := Str + AName + ‘ ‘;
      Resume;
      end;

      destructor TMutexThread.Destroy;
      begin
      inherited;
      form1.Memo1.Lines.Add(‘Mutex Name:’ + Name + ‘ Start Time:’ + StartValue + ‘ Stop Time:’ + StopValue);

      end;

      procedure TMutexThread.Execute;
      begin
      inherited;

      WaitForSingleObject(MutexHandle, INFINITE);
      fStartValue := TimeToStr(Time);

      Sleep(5000);

      fStopValue := TimeToStr(Time);
      ReleaseMutex(MutexHandle);
      end;

      end.
      [/DELPHI]

      evet arkadaşlar eğer yazdığım şeyi sizde düşünmüsseniz. tebrik ederim fakat çok büyültülcek birşey yok
      biraz ayrıntılı düşünürsek herkez aynı şeyi söyleyecektir.
      Nede olsa şeytan ayrıntıda saklıdır :)

      yukarıda ki açıklamaları izahatına gidelim isterseniz.

      Kanalımız Execute içinde kaç milisaniye çalışıyor du ? 5000 değil mi.

      mMutex1 := TMutexThread.Create(‘Birinci Mutex’);
      mutex1 çalışmaya başladı beklemekte olan bir kanal olmadığı için kod bloğuna giriş yaptı.
      WaitForSingleObject(mMutex1.Handle,2000);
      burada 2000 ms bekliyor bu arada “Birinci Mutex” in bekleme süresinin bitmesine kaç ms kaldı ?
      3000 ms kaldı değil mi 2000 ms bekledikten sonra “İkinci Mutex” kanal oluşturması için create oluyor
      ve 3000 ms beklemesi gerekiyordu çünkü Birinci Mutex in bitmesine 3000 ms kalmıştı.
      mMutex2 := TMutexThread.Create(‘İkinci Mutex’);
      2000 ms beklemeden sonra mutex2 kodunun çalıştırıldığını söylemiştik.
      Fakat daha 3000 ms var bu koddan sonra bir altta olan;
      WaitForSingleObject(mMutex2.Handle,2000);
      2000 ms bekleme kodumuz var buda çalıştıkdan sonra “Birinci Mutex” in bitmesine 1000 ms kaldı
      mMutex3 := TMutexThread.Create(‘Üçüncü Mutex’);
      ve yine 2000 ms bekleme kodunun ardından “Üçüncü Mutex” imiz de devreye girdi fakat 1000 ms var burada ne olduu peki
      “İkinci Mutex” ve “Üçüncü Mutex” “Birinci Mutex” in sonlanıp devreye girebilmeleri için sıradalar.
      tabi ki burada yapmış olduğumuz WaitForSingleObject bekleme işlemi programın main thread ında yer aldığı
      için diğer kanalı etkilemesi mümkün değildir. Bu yüzden burada ne kadar bekleme işlemi gerçekleşirse
      gerçekleşsin zaman akmaya devam ediyor arkadaşlar :) anlayacağınız kanalda ki sleep işini yapmaya devam ediyor

      şimdi asıl kısma geldik Thread lar işletim sisteminde 20ms ara ile çalışıyordu tabi öncelikler vs var fakat onları
      şimdi karıştırmayalım.. Burada sıraya giren kanallar işletim sisteminde InterlockedCompareExchange
      gibi işlem bitene kadar diğer kanalların geçişini engellemezler InterlockedCompareExchange ın ne yaptığını
      hatırlayalım işlem bitene kadar kendi çalışması sürecini doldurmuş olsada dahi diğer kanalların
      geçişlerini durduruyordu taki işlem bitene kadar tam tersi şekilde Critical Sectionlar da veya Mutex lerde diğer
      kanalların sıraya girme işleminde bu gerçekleşmiyor bunu neden söyledim peki ? kafanızda bazı soru işaretleri
      belirebilir olmasından örneğin sıraya girme işleminde bir birlerini bekleyebilme gibi…..

      her ne kadar mutex2 ve mutex3 çalışmış olsada sıraya girmiş olsada diğer kanalın işini bitirmesine 1000ms
      var kanallar 20 ms aralıkla çalıştıgını düşündüğümüzde bu 1000ms zaman dilminde hangisinin çalışma zamanını bitirip
      yeniden kanallar arası çalışma için sıraya girdiğini bilemeyiz bu yüzden. geride kalan mutex2 ve mutex3
      hangi zaman dilminde çalışma zamanını bitirdi ve yeni bir fırsat bulduğunu anlayamayız. Yukarıdaki
      sorunun da nedeni budur.
      peki bunu bu kadar yazdım çözümü nedir. ? öncelikle 2000 ms olan bekleme kodlarını 2510 yapın ve test edin
      gerek 3 5 kere gerek 7 8 kere fakat her zaman sonuç;
      Birinci Mutex
      İkinci Mutex
      Üçüncü Mutex
      şeklinde olacaktır peki neden bu kez aksi bir durum olmadı isterseniz kısa olarak lafı fazla uzatmadan izahatını verelim.

      mMutex1 := TMutexThread.Create(‘Birinci Mutex’);
      WaitForSingleObject(mMutex1.Handle,2510);
      ilk bekleme 2510 ms beklediğine göre “Birinci Mutex” in işini bitirmesine 2490 ms kaldı.
      mMutex2 := TMutexThread.Create(‘İkinci Mutex’);
      mutex2 sıraya girdi.
      WaitForSingleObject(mMutex2.Handle,2510);
      2510 ms bekleyecektir fakat bir önceki beklemede mutex1 in işini bitirip mutex2 nin
      çalışmasına başlamak için 2490 ms kaldı yani mutex2 çalışmaya başladıkdan 20 ms sonra mutex3 sıraya girecektir.
      Böylece sırayla çalışmasını sağlayacağız
      mMutex3 := TMutexThread.Create(‘Üçüncü Mutex’);

      Bir başka alternatif daha var tabi ki oda INFINITE :) bir işlem bitmeden diğerini sıraya bile sokmayabiliriz.
      FAKAT ŞİMDİ YAPACAĞIM AÇIKLAMA İLE KAFALARI KAŞINDIRABİLİR EĞER Kİ INFINITE YAZARSANIZ ŞUAN Kİ ÖRNEK DE
      PROGRAMINIZ KİLİTLENECEKTİR NEDEN Mİ ? HEMEN BU BOMBA SORUNU AÇIKLAMAYA KOYULALIM
      TMEMO,TBUTTON,TLISTBOX,TCHECKBOX BUNLARIN HEPSİ TWINCONTROL DAN TÜREMİŞTİR YANİ WİNDOWSUN TANIYABİLDİĞİ ÜZERİNDE MESAJ SİSTEMİ
      İŞLEMİ İŞLEM YAPMAMIZI SAĞLAYAN SINIFTIR Bknz. SENDMESSAGE,POSTMESSAGE YANİ BUNLAR WİNDOWS KONTROLÜDÜR.
      SİZ BU KODLARDA BEKLEME KODUNA INFINITE YAZDIĞINIZ ZAMAN DESTROY DA MEMO YA ERİŞİP EKLEME İŞLEMİ YAPTINIZ
      HİÇ FARK ETMEZ EXECUTE DE OLABİLİR HİÇ BİR FARKETMEZ X PROSEDÜRÜ OLABİLİR.

      WaitForSingleObject(mMutex1.Handle,INFINITE); Kodu programın ana kanalında çalışıp bu kanalı bloke ettiği için o kanaldan geçen hiç
      bir mesaj programımız cevap veremeyecek ve kilitleme olacaktır.

      Fakat burada önemli nokta bu olay yalnızca TWINCONTROL den türeyen bileşenlerde gerçekleşmektedir.

      Buradaki tespitim ardından tuğrul hocama bu sorunla ilgili bir açıklama rica ediyorum.
      Image bileşeninde bu sorun yok. Çizim yaptıkdan sonra ekranın yenilenmesi için mesaj geliyor öyleyse neden kilitlenmiyor
      fakat sadece bu olay twincontrol den üreyen sınıflarda oluyor.

      Saygılarla Ahmet YEŞİLÇİMEN

    Tuğrul HELVACI için yorum yazın