Nedir bu Thread’lerden çektiğimiz..!

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

Uzun yıllık programcılık hayatımda pek çok defalar Thread kullandım ancak ilk defa bu kadar yoğun ve derinlemesine kullanma ihtiyacı hissettiğim bir projede çalıştım. Bu projemde TThread sınıfı ile uğraşırken karşılaştığım bazı sorunları sizlerle de paylaşmak istedim. Sizler önlemlerinizi baştan alın ki, saç baş yolmayın…

Eğer gerçekten time-critical işler yapıyorsanız thread’iniz içerisinde kesinlikle Synchronize kullanmayın.Çünkü Synchronize çağrısı thread’in çalışmasını kesip main thread’e geçirir.

Eğer siz TThread sınıfından türettiğiniz kendi sınıfınızda Execute metodunda bütün işi Synchronize metoduna teslim etmiş iseniz o zaman siz MTA (Multi Threaded Application) değil STA (Single Threaded Application) geliştiriyorsunuz demektir.. Yani ha formunuzun bir metodunu çağırmışsınız ha bir Thread create etmişsiniz.Arada hiçbir fark olmaz.! Bir örnek vermek gerekir ise:

TMyThread = class(TThread)
private
  fErrorString : String;
  procedure CallExternalProc;
protected
  procedure Execute; override;
public
  constructor Create;
  property ErrorString read fErrorString write fErrorString;
end;

constructor TMyThread.Create;
begin
  inherited Create(true); // Bekler vaziyette oluştur.!
  FreeOnTerminate := true; // Thread terminate olduğunda nesneyi Free et.!
  Resume; // Thread başlasın artık
end;

procedure TMyThread.CallExternalProc;
begin
  frmMain.Bilmemne;
  frmMain.BaskabirMetod;
  ...
  ...
 // Burada ise pek çok hesaplama vs. yaptığınızı düşünelim.!
end;

procedure TMyThread.Execute;
begin
  inherited;

  try
    Synchronize(CallExternalProc);
  except on E: Exception do
  begin
    ErrorString := Format('TMyThread: Error occured. %s, %s', [E.ClassType.ClassName, E.Message]);
  end;
end;


Şimdi yukarıdaki kodumuzda Synchronize metodu ile sarmaladığımız CallExternalProc istediği her türlü VCL nesnesine güvenle ulaşabilir.Kodlama da hata yapmadı iseniz bir hata ile karşılaşmazsınız. Ancak bunun da bir maliyeti var elbette. Bütün metodunuzu Synchronize ile sarmaladığınız da “bütün işi ana thread’e yaptırmış oluyorsunuz, o zaman da thread kullanmanın bir anlamı kalmıyor.!” “Peki Synchronize ne işe yarıyor o zaman ?” diyebilirsiniz. Delphi VCL (Visual Component Library)’nin pek çok bileşeni thread destekli değildir. Dolayısı ile bu bileşenlere thread içerisindeki kodlardan erişimler her zaman hatalara açıktır.

Buradan anlaşılan şu, eğer VCL sınıflarına erişmeniz gerekiyor ise bu erişim için bir procedure yazmalısınız ve buradaki kodu mümkün olduğunca kısa tutmalısınız ve bu procedure’ü Synchronize ile sarmalısınız. Elbette bunun bir başka alternatifi daha var o da mesajlaşma API’lerini kullanmak.. Thread içerisinden ana forma , daha doğru bir ifade ile VCL sınıflarından herhangi birine ulaşmak ve o sınıfın özelliklerini kullanmak istiyorsanız PostMessage yada SendMessage API’lerinden birisini de kullanabilirsiniz. Bu daha fazla tercih edilmesi gereken bir yöntem. Bu fonksiyonların kullanımı sırasında mesaj göndermek istediğiniz pencerenin (ki bu genellikle ana formunuz olacaktır) Handle’ına ihtiyaç duyacaksınız. Burada da kritik bir durum var ama bu durumu açıklamadan önce küçük bir örnek vermeme müsaade edin:

const WM_MY_THREAD_MESSAGE = WM_USER + $1001;
...
...
...
...
procedure TMyThread.Execute;
begin
  inherited;

  try
   ....
   ....
   ....
   PostMessage(Application.MainForm.Handle, WM_MY_THREAD_MESSAGE, 0, 0);
   // yada
   PostMessage(frmMain.Handle, WM_MY_THREAD_MESSAGE, 0, 0);
   ....
   ....
  except on E: Exception do
  begin
    ErrorString := 'Error';
  end;
end;

Yukarıdaki durumda yine tehlikelerle karşılaşabilirsiniz..! İhtimalli konuşuyorum çünkü hataların oluşması, oluşan hataları görebiliyor olabilmeniz dahi pek çok özel duruma bağlı. Burada karşılaşabileceğiniz büyük sorun Application nesnesi üzerinden ana formunuza ulaşmaya çalışmak yada direkt ana formunuza ulaşmaya çalışmaktır. Daha önce de söylediğimiz gibi VCL thread destekli değildir. O halde yukarıdaki kodumuzu güvenli bir şekilde kullanabilmemiz için TMyThread sınıfımız içerisinde bir değişken tanımlayıp , Thread nesnemizi çalıştırmadan önce bu değişkene ana formumuzun Handle’ını aktarmak ve onu kullanmak durumundayız. Şöyle ki:

TMyThread = class(TThread)
private
  fMainFormHandle : HWND;
protected
  procedure Execute; override;
public
  constructor Create(const FormHandle : HWND);
  property MainHandle : HWND read fMainFormHandle;
end;

constructor TMyThread.Create(const FormHandle : HWND);
begin
  inherited Create(true);

  fMainFormHandle := FormHandle;
  Resume;
end;

procedure TMyThread.Execute;
begin
  inherited;
  ....
  ....
  PostMessage(MainHandle, WM_MY_THREAD_MESSAGE, 0, 0);
end;

// Thread'inizi ihtiyaç duyduğunuz yerden çağırırken de
var
  mHandle : HWND;
  mThread : TMyThread;
begin
  mHandle := frmMain.Handle; // Yada Application.MainForm.Handle
  mThread := TMyThread.Create(mHandle);
  ...
  ...
end;

Görüldüğü üzere pek de zor değil. SendMessage yada PostMessage API mesajlaşma fonksiyonlarını kullanarak daha güvenli bir multi-threaded uygulama yazabilirsiniz..

Gelelim bir başka soruna.. Diyelim ki sürekli çalışmasını istediğiniz bir thread’iniz var.. O zaman aşağıdaki yapıya benzer bir yapı kurarsınız.

procedure TMyThread.Execute;
begin
  inherited;

  while not Terminated do
  begin
   ...
   ...
  end;
end;

Burada bir sorun yok gibi görünüyor.Oysa burada da çok önemli bir ipucu var bilmemiz gereken. O da Sleep(0) çağrısı. Bildiğiniz üzere işletim sistemi gerçek bir multi-threaded işletim sistemi değil, sadece taklit yapıyor.Bu taklidi nasıl yapıyor ? Her bir thread parçacığı için belli bir zaman planlayarak. Kısacası işletim sistemi thread’ler arasında o kadar hızlı geçişler yapıyor ki bunu gözümüz ile fark edemiyoruz ve herşey aynı anda çalışıyormuş hissine kapılıyoruz. Aslında durum böyle değil. İşletim sistemi üzerinde aynı anda sadece 1 tane çalışan thread vardır. Bu çalışan thread kendisi için ayrılan çalışma zamanını tamamladığında yerini sırada bekleyen diğer thread’e devreder. Multi-threaded uygulamalar yazarken özellikle de sürekli çalışan thread’ler yazarken CPU kullanımının çok fazla olduğunu gözlemleyebilirsiniz. Çünkü işletim sistemi sizin insafsızca ;) yazdığınız kod yüzünden thread’i sürekli işletmeye çalışmaktadır. Bu durumda yapılacak en güzel iş Sleep(0) çağrısını yapmaktır. Bu çağrı ile birlikte işletim sistemi “ooh” diyecek ve sıradaki diğer thread’lerin de çalışmasına izin verebilecektir.!

Bir diğer önemli husus ise Application.ProcessMessages çağrısı ile ilgili. Bildiğiniz gibi Application nesnesi TApplication sınıfının bir örneğidir ve o da thread destekli değildir. Bir uygulama içinde Application nesnesinden sadece bir tane vardır ve esas amacı uygulamanın Main Thread’ini yönetmektir. Application.ProcessMessages çağrısına her başvurduğumuzda threadimizin akışının duracağını ve akışı Main Thread’e devredeceğini bilmelisiniz. Bu şu an zararsız gibi görünüyor olsa da ciddi sorunlara neden olabilir. Söylemedi demeyin ;)
Yukarıda saydığımız iki fonksiyonun kullanım yerlerini yazmıştık. Sleep’i thread içerisinde (genellikle while döngüsünün en sonunda) kullanıyorken, Application.ProcessMessages çağrısını ise Thread’imiz içerisinde asla kullanmıyorduk. Ancak başınıza gelebilecek ilginç bir başka durum senaryosu daha var.. Diyelim ki yine bir thread sınıfımız var ve bu thread’i kullanan değişkenimizi OnTerminate olay yöneticisinde Free ediyoruz yada değişkenimizi nil’e eşitliyoruz. Threadimizi Terminate ettiğimiz yerde de Terminate işlemi bitene kadar bekle diyoruz. İşte bu bekleme koduna da Sleep koyarsanız tıpkı thread içerisinde Application.ProcessMessages kullanmış gibi tehlikeli bir iş yapmış olursunuz. Bir örnek verelim:

TMyThread = class(TThread)
protected
  procedure Execute; override;
public
  constructor Create;
end;

var
  myThread : TMyThread;

constructor TMyThread.Create;
begin
  inherited Create(true);

  FreeOnTerminate := true;
  OnTerminate := frmMain.TerminateMyThread;
  Resume;
end;

procedure TMyThread.Execute;
begin
  ....
  ....
end;

procedure TFrmMain.TerminateMyThread(Sender : TObject);
begin
  myThread := nil;
end;

procedure TFrmMain.Button1Click(Sender : TObject);
begin
  myThread := TMyThread.Create;
  .....
  .....
  .....
  myThread.Terminate;

  while myThread <> nil do
  begin
    // Burada kesinlikle Sleep kullanmayın.
   Application.ProcessMessages;
  end;
  ....
  ....
end;

Yukarıdaki örneğimizde ise bir önce söylediğimizin tam tersini yaptık.Yani Sleep kullanmadık Application.ProcessMessages kullandık.Aslında uygulamanızını main thread’i içerisinde istediğiniz herhangi bir yer ve zamanda Application.ProcessMessages çağrısını yapabilirsiniz, ama Thread içerisinde yapmayın. Sleep kullanılmamasının gerektiği noktada ise şöyle bir durum oluşuyor.Hatırlayacağınız üzere işletim sistemi Sleep gördüğünde dayanamayıp hemen sıradaki thread’i işletme cihetini gidiyordu. :) İşte bu durum uygulamanızın kilitlenmesine neden olabilir. Siz en iyisi Application.ProcessMessages kullanın. ;)
Birkaç püf noktaya daha değinip makaleyi burada neticelendirmek istiyorum. Bunlardan birincisi eğer bir database thread yapıyorsanız yani bir veritabanına bağlanıp çeşitli veriler üzerinde maniplasyonlar yapıyorsanız o zaman bağlantı için gerekecek connection nesnesinin thread’e has olması gerektiğidir. Bu şu anlama gelir, uygulamanızın herhangi bir formunda yada bir datamodule üzerinde olan bir Connection nesnesini kullanabilirsiniz (sözgelimi TADOConnection) ancak göreceksiniz ki , bir müddet sonra thread’iniz beklenmeyen şekilde hatalı çalışmaya başlayacak. Bazen hiç hata görmeyeceksiniz bazı zamanlarda ise ilginç hatalarla karşılaşacaksınız. Gelin siz yine beni dinleyin ve thread içerisinden herhangi bir veritabanına bağlanmak için kullanacağınız bağlantı nesnenizi create edin, open edin ondan sonra kullanın işiniz bitince de Close edip Free yaparsınız. Bu elbette performansı biraz etkileyecektir.Hele hele yüzlerce defa çalışan bir thread’iniz var ise. O zamanda başka bir çözüme gitmek durumundasınız. Threadiniz eğer herhangi bir veritabanına bağlanıp bir veri çekmek durumunda ise yada veri güncelleme, silme yada ekleme yapıyorsa ancak bunu çok sık yapmıyor ise o zaman yukarıdaki yöntem uygulamayı çok fazla yormayacaktır. Ancak, eğer thread’iniz ard arda 15-20 kez çalışıp bir veritabanına bağlanıp veri güncellemesi yada eklemesi veya silmesi yapacak ise işte bu modelde o zaman sorun var demektir. Çünkü 15-20 defa veritabanına bir connection aç/kapa işlemi gerçekleşecektir. Bu duruma mani olmak için belki thread’inizi sürekli yaşayan bir thread olarak dizayne edebilir ve connection nesnenizin create edilip edilmediğini kontrol edebilir, eğer edildi ise bir daha create etmezsiniz. Bu elbette bir çözüm..Ancak sürekli yaşayan bir thread yaptığınız da (ki bunu while not Terminated gibi bir kod ile yapıyorduk hatırlarsanız ) o zaman da pek çok defa aynı insert, update yada delete kodunun çalışması söz konusu olabilecektir. Artık bunun çözümü sizin dizaynınıza bakar. Belki sürekli çalışma döngü koduna bir başka boolean değişkenin değeri ni de ekleyebilirsiniz.

Bir de unutulmaması gereken önemli başka bir husus ise COM objelerine thread içerisinden erişmek istediğinizde ortaya çıkar. Bu gibi durumlara ActiveX.pas dosyasını uses ile projenize eklemeniz ve CoInitialize(nil); çağrısını yapmak zorunda kalırsınız. Her CoInitialize(nil) çağrısı bir CoUnInitialize; çağrısı ile sonlanmak zorundadır. veritabanları üzerinden sorgu yaparken de bu hususu unutmamanızı dilerim. Kodunuz şu şekilde görünecek:

procedure TMyThread.Execute;
begin
  inherited;

  try
    CoInitialize(nil);
    try
     // Database'e bağlan birşeyler yap..
     // yada başka bir COM nesnesi ile çalış..
     ....
    finally
      CoUnInitialize;
    end;
  except on E: Exception do
  begin
    .....
    .....
  end;
end;

Son bir ekleme daha yapmak istiyorum..Thread içerisinde kullanmayı düşündüğünüz kritik nesnelerinizi Thread sınıfınızın constructor ‘unda değil Execute metodunda create etmeye mümkün olduğunca çaba gösterin. Unutmayın ki constructor ‘ın içinde yazdığınız kodlar ana thread içerisinde çalışırlar !

Saygılar, sevgiler..

“Nedir bu Thread’lerden çektiğimiz..!” için 8 Yorum

  1. sadettinpolat diyor ki:

    hocam yeni blog hayirli olsun. yeni yazilarini sabirsizlikla bekliyoruz :D

  2. Tuğrul HELVACI diyor ki:

    Teşekkür ederim Sadettin, yazıyorum takibe devam ;)

  3. Mehmet Fatih KARABACAK diyor ki:

    Kolay gelsin .
    Bu yazınızı okuduktan sonra daha önce gliştirdiğim ibr projeyi yeniden yazdım.
    Lakin nedeni anlayamadığım bir şekilde kullanmış olduğum TThread ya duruyor ya da kendi kendine terminate oluyor.

    Bu konuda ne yapmamı tavsiye edersiniz?

  4. Tuğrul HELVACI diyor ki:

    Threading karmaşık bir husustur. Bakmadan yada incelemeden sorun hakkında yorum yapabilmem bir ütopya olurdu. Arzu ederseniz, threading ile ilgili 2 makalem daha var onları da okuyun. Hâla sorununuz devam ediyor ise yine paslaşalım.

  5. Ahmet diyor ki:

    Hocam Sorunum Şu Post yada SendMessage’yi vermissiniz ama bir prosedürü nasıl çağıracağını vermememissiniz yani SynChronize(Pro) yaptığınız zaman pro prosedürünü çağrıyor ama post yada send bişe yi çağırmıyor lütfen yardım edin synchronize işime yaramıyor :S

  6. Tuğrul HELVACI diyor ki:

    Ahmet bey, PostMessage yada SendMessage metodlarının açıklaması bu makalenin konusu değildir. Bu sebeple burada detayları ile değinilmemiştir ancak, bu makalemizde belirttiğimiz PostMessage/SendMessage kullanımı için küçük bir örnek verebilirim.

    TForm1 = class(TForm)


    public
    procedure WMThreadMessage(var Message : TMessage); message WM_MY_THREAD_MESSAGE;
    end;

    ….
    ….
    ….

    procedure TForm1.WMThreadMessage(var Message : TMessage);
    begin
    ShowMessage(‘Thread Message’);
    end;

    Gönderirkende; SendMessage(Form1.Handle, WM_MY_THREAD_MESSAGE, 0, 0) gibi kullanabilirsiniz.

  7. yangın alarm diyor ki:

    teşekkürler çok güzel bir makale olmuş elinize sağlık

  8. Orhan diyor ki:

    Teşekkürler, güzel bir yazı kaleme almışsınız.

Yorum Yazın