TThread & TQueue işbirliği..

// 26 Mayıs 2009 // Delphi, Programlama

Kısa bir aradan sonra tekrar merhaba, bu makalemde iki önemli sınıfın birbirleri ile iletişiminin nasıl olacağını ve yaptıkları işbirliğinin getirilerini izah etmeye gayret edeceğim.

Malumunuz odur ki, hemen hemen her programcı time-critical kod parçacıklarında TThread nesnesine en azından bir kere müracaat etmiştir. Thread’ler programlama dünyasında işimizi kolaylaştıran son derece faydalı yapılardır. Ancak faydaları olduğu gibi, dikkatle kodlanmalarını gerektirecek pek çok neden de var. Bu nedenleri dilim döndüğünce daha evvel izah etmiştim. İlgilenen arkadaşlarımın öncelikle thread’ler hakkındaki makalemi okumasında yarar olduğunu düşünüyorum.

Bu noktadan sonra thread’ler hakkında bir fikre sahip olduğunuzu varsayarak ilerleyeceğim. Makalemize konu olan yararlı işbirliğinin diğer tarafındaki sınıfımız ise TQueue sınıfı. Bu sınıf, ilk giren ilk çıkar(first in first out) prensibine göre tasarlanmış bir Delphi sınıfı. Bu sınıfın tam zıttı olan TStack sınıfı ise tahmin edeceğiniz gibi son giren ilk çıkar(last in first out) mantığına göre tasarlanmış bir sınıf. Lâkin şimdi bizim konumuz TQueue.

TQueue sınıfı, bir TStringList yada TList gibi bir liste sınıfıdır aslında. Tek özelliği Peek ve Pop gibi metodlarının herzaman listenin en tepesindeki item’ı döndürmesidir. First in first out’un gerçek hayatta pek çok örneğine rastlanabilir. Örneğin elektrik faturanızı ödemek için girdiğiniz sırada, yada ramazan’da Sultanahmet’te iftar açacaksanız köftecide beklediğiniz kuyrukta rastlanabileceği gibi, bu mekanizmada her zaman ilk gelen hizmeti ilk alacaktır. Sona kalan dona kalır :)

Belki bir daha TStack sınıfını anlatmak için ayrı bir makale yazmayabilirim. Bu vesile ile TStack sınıfının gerçek yaşamda kullanımından da kısa da olsa bir örnek vererek ilerlemek istiyorum. TStack sınıfı, daha önce de söylediğimiz gibi son giren ilk çıkar(last in first out) prensibine göre tasarlanmış bir sınıftır. Diyelim ki, bulaşık yıkıyorsunuz büyük bir lokantada. Siz yıkadıkça yeni yeni bulaşıklar gelip duruyor. Yeni gelenler her zaman eskilerin üstüne konulur ve siz her zaman en son gelen bulaşıkları yıkarsınız. (Aklıma ilk gelen örneğin bulaşık olmasından mütevellit benim bulaşık yıkamaya aşina olduğum sanılmasın :) )

Bir miktar ön bilgi verdikten sonra, bir gerçek hayat senaryosu üretmeye çalışalım ve bu senaryo üzerinden anlatımımızı devam ettirelim. Diyelim ki yazdığınız kodların son derece hızlı çalışmasının gerektiği bir proje geliştiriyorsunuz. O zaman hemen hemen hepimizin aklına elbetteki thread’ler gelecektir. Çünkü thread’ler bildiğiniz gibi uygulamanızın ana thread’ini bloke etmeden arka planda çalışırlar.(Aslında işletim sistemi bizi bu hususta kandırır, diğer makalemde izah etmiştim.) Uygulamamızı bloke etmemeleri gerçekten güzel işimize yarayan bir özellik. Ama gelin sorunumuzu biraz daha zorlaştıralım. Yine farzedelim ki, hızlı çalışması için thread’lerin yardımına başvurduğumuz kodlarımızın aynı zamanda bir sıralama ile çalışıyor olması gereksin.

Mesela; bir yazarkasa uygulaması geliştirirken müşterilerin kasa önünde beklemesini kimse istemez. Dolayısı ile işlerin son derece süratli işlemesi gerekir. Ancak uygulamanın yapısı gereği pek çok bilginin kontrol edilmesi ve loglanması icap etmektedir. Yazarkasaya giden ve kasadan gelen tüm bilgilerin sıralaması hayati önem arzetmektedir. Yazarkasaya gönderilen komutun cevabının, bir sonra gelecek olan cevaptan sonra log’lanmaması gerekir !

Sizler kendi projelerinizdeki bazı sorunları da bu makalenin içeriğine uydurabilirsiniz. Yazarkasa benim konuya ışık tutabilmek için verdiğim küçük bir örnek sadece. İhtiyacımızı bir kere daha özetleyecek olursak; bazı hızlı çalışması gereken ve uygulamamızın ana thread’ini bloklamasını istemediğimiz kodlarımız var ve bu kodlar arka planda çalışırken de belirli bir sırada çalışmasını istiyoruz.

İşte tam bu noktada TThread ile TQueue sınıflarının faydalı işbirliği devreye giriyor. Ancak bu mekanizma üzerinde hâla biraz konuşmamız icap ediyor. Diyelim ki TThread nesnelerimizi oluşturduk, bu nesneleri nerede çalıştıracağız ?

Oluşturduktan hemen sonra mı, TQueue nesnesinin kuyruğuna attıktan sonra mı ? Bunun ne önemi var dediğinizi duyar gibiyim :)

Hemen daha anlaşılır olması babında pseudo kod ile örneklemeye çalışalım:

Thread1 oluştur.
Thread1 kuyruğa at.
Thread1 çalıştır.

Thread2 oluştur.
Thread2 kuyruğa at.
Thread2 çalıştır. ??

Thread3 oluştur.
Thread3 kuyruğa at.
Thread3 çalıştır. ??

Yukarıda gördüğümüz üç adet thread’in şu anda üçü de aynı anda çalışıyor. Ancak bu durumda thread’lerin hangisinin daha önce biteceğini bilemeyiz. Dolayısı ile baştan beri sıralı olarak çalışmasını istediğimiz mekanizmayı bu şekilde halledemeyiz. Farkettiğiniz gibi, thread’leri kuyruğa atmadan önce çalıştırmak ile attıktan sonra çalıştırmak arasında şu aşamada hiç bir fark yok. Her iki durumda da istediğimizi elde edemediğimizi görüyoruz. Tabii bu bir ihtimaller silsilesi. Şans eseri, Thread1 30 ms. , Thread2 50 ms, ve Thread3 100 ms. de çalışıyor olabilir. O zaman herşey sorunsuz olacaktır. Ama bizim ihtimallerle kaybedecek emeğimiz ve zamanımız yok. O halde daha kesin bir çözüme ihtiyacımız var.

Her sorunun birden fazla çözümü olduğu gerçeği ile aklınıza pek çok çözüm gelmiş olabilir. Bunların içinde bir adet TTimer kullanmak yada her biten thread’in ana pencereye bir mesaj göndermesine müteakiben sıradaki diğer thread’in işletilmesi de olabilir. Ancak bana göre bunlar da pek şık çözümler değil.

Bizim üreteceğimiz mekanizmada thread’lerin sıra ile çalıştırılması görevini bizzat TQueue sınıfı üstlenecek. Bu kadar konuştuktan sonra, sanırım biraz kod görme zamanı:

  TThreadQueue = class;

  TCustomThread = class(TThread)
  private
    fOwner   : TThreadQueue;

    procedure NotifyOwner;
  protected
    procedure Execute; override;
  public
    constructor Create(const AOwner : TThreadQueue);
  end;

  TThreadQueue = class(TQueue)
  protected
    function Push(AItem: Pointer) : Pointer;
  public
    procedure Notify(const AThread : TCustomThread);
  end;

implementation
{ TThreadQueue }

procedure TThreadQueue.Notify(const AThread : TCustomThread);
var
  pThread : Pointer;
  iThreadCount : Integer;
begin
  pThread := Pop; // İlk item silinsin..

  iThreadCount := Count;

  if iThreadCount > 0 then // Listede hâla çalışmayı bekleyen bir thread var mı ?
  begin
    pThread := Peek; // Listeden o itemi silmeden al..
    if pThread <> nil then
      if TObject(pThread) is TCustomThread then TCustomThread(pThread).Resume;
  end;
end;

function TThreadQueue.Push(AItem: Pointer) : Pointer;
var
  iThreadCount : Integer;
begin
  iThreadCount := Count;

  Result := inherited Push(AItem); // Thread listeye eklensin..

  if iThreadCount = 0 then //ilk item eklenince çalışmaya başla
    if TObject(AItem) is TCustomThread then
      TCustomThread(AItem).Resume;
end;

{ TCustomThread }

constructor TCustomThread.Create(const AOwner : TThreadQueue);
begin
  inherited Create(true); // Thread hemen çalışmasın, beklesin.
  FreeOnTerminate := true; // İşi bitince free olsun.

  fOwner := AOwner;

  if fOwner <> nil then
    fOwner.Push(Self);
end;

procedure TCustomThread.Execute;
begin
  // Bu sınıfı miras alan sınıfların Execute metodlarında inherited çağrısını unutmamak gerekir (En sonda) !

  NotifyOwner;
end;

procedure TCustomThread.NotifyOwner;
begin
  if fOwner <> nil then
    fOwner.Notify(Self);
end;

Yukarıdaki mekanizma tam olarak istediğimizi yapıyor. Thread nesnelerinin kullanılması sayesinde uygulamamızın ana thread’i blok olmuyor ve TQueue sınıfı içine yazdığımız metodlar sayesinde de TQueue sınıfı thread’leri sırası ile çalıştırıyor.

İşleyiş nasıl oluyor gelin kısaca bakalım:

TCustomThread.Create(queueListesi); gibi bir çağrı yaptığımız an, oluşan thread sınıfımız queueListesi isimli nesnenin Push metodunu kendisini parametre geçerek çağırıyor. Push metodunda, listeye ekleme yapmadan hemen evvel liste içindeki nesne sayısına bakılıyor. Ve herhangi bir nesne bulunamaz ise, yani eklenen thread ilk thread ise zaman kaybetmeden thread’in çalıştırılması cihetine gidiliyor. TThread sınıflarının Resume çağrıları otomatik olarak Execute metoduna dallanılmasını sağlar. Execute metodundan çıkılması thread’in bittiği anlamına gelir. Bu bağlamda çalışmaya başlayan thread’imiz Execute metoduna dallanacaktır.

Execute metodunda kendisinden beklenen işleri yapan thread’imiz son kod olarak NotifyOwner metodunu çağırır. NotifyOwner metodu ise TQueue türündeki sınıfa thread’in bittiğini belirten bir mesaj yollar. Şu anda TQueue sınıfı içinde tanımlanmış olan, Notify metodundayız. Bu metodda ilk yaptığımız iş çalışması biten thread’i listeden Pop metodunu kullanarak silmektir. Silme işleminden sonra listemizde hâla çalışmaya hazır thread olup olmadığı kontrol edilir. Eğer var ise bu thread Peek metodu ile listeden silinmeden alınır ve çalıştırılır.

Böylece, thread’lerimizin ne zaman çalıştırılacakları konusunda bir kaygımız kalmıyor. Herşeyi TQueue sınıfı bizim belirlediğimiz prensibe göre uyguluyor. Bu mekanizma uygulamamızın yorulmasını ve iş yapamaz duruma gelmesini engellediği gibi, arka planda çalışan iş parçacıklarını da bir sıralama ile çalıştırıp isteğimize ulaşmamızı sağlıyor.

Umarım faydalı olabilmişimdir, bir sonraki makalede görüşmek dileği ile..

Sevgiler, saygılar..

“TThread & TQueue işbirliği..” için 4 Yorum

  1. Veli BOZATLI diyor ki:

    Ben bu tarz işlerde sizin de bahsettiğiniz Ana Threade mesaj gönderme yöntemini kullanıyordum ama bu makaleyi okuduktan sonra bundan vazgeçerim sanırım :)
    Gerçekten çok hoş bir yöntem.

    TThreadQueue.Notify parametre olarak TPosThread bekliyor. TPosThread ?

    Emeğinize sağlık…

  2. Tuğrul HELVACI diyor ki:

    Beğenmenize sevindim. Ayrıca TPosThread konusunda haklısınız, düzelttim, teşekkür ederim.

  3. mehmet diyor ki:

    Tuğrul hocam elinize sağlık çok güzel yazılarınız var ancak bu yazınızdaki TQueue class ını Xe2 de kullanmaya çalıştıgımda “undeclared identifier” hatasını alıyorum uses kısmına Generics.Collections ı eklememe rağmen bir fikriniz varmıdır veya elinizde TQueue ile ilgili örnek varsa paylaşırsanız sevinirim

    iyi günler.

    • Tuğrul HELVACI diyor ki:

      İlgili makalede TStack ve TQueue sınıflarının generic olmayan versiyonları kullanılmıştır, o sınıfların tanımlarını System.Contnrs.pas dosyası içinde bulabilirsiniz.

Yorum Yazın