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ıkend; 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şburduğumuda 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 ne dekleyebilirsiniz.
Bir de unutulmaması gereken önemli başka bir husus ise COM ebjelerine 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..
Tuğrul HELVACI

Entries (RSS)
July 29th, 2006 at 01:12:23
Hocam ellerine sağlık. Oldukça zevkli ve bilgilendirici bir yazıydı.
July 29th, 2006 at 08:16:45
Beğendiğine sevindim, bende burada yazdıklarımızı kimse okumuyor hissine kapılıyordum. Site yöneticisi olsa da demek ki birileri okuyormuş
Saygılar, sevgiler..
Tuğrul HELVACI
July 29th, 2006 at 03:31:16
Hocam okunmaz olur mu hic? En azindan istatistiklere gore okundugunu soylemek mumkun
Yanliz iste, yorum yazmakta biraz zorlaniyoruz biz sanirim.
July 29th, 2006 at 07:18:53
tesekkurler tugrul. guzel bir makale olmus. thread ile veritabani konusunda bende bir iki deneme yapmistim fakat basarili olamamistim. threadi create ederken main formun handleını gectigin gibi bende datamodulde duran tibdatabase in handleini threade gecerek kullanmistim. acma , kapama ve sorgulama gibi islemlerde bir sorun yasamadim ama is quick reportla raporlamaya gelince ya da dbgrid üzerinde kayitlar arasinda dolasmaya gelince program cakilmisti
konunun uzerine pek fazla dusemedim bende ilgili kodu ayri bir exe yaparak problemi cozmustum. senin yazdiklarin isiginda bu konuya tekrar donmeyi dusunuyorum bir ara.
dipnot: her ne kadar su an balkonda bir baskasinin kablosuz agini kullanarak internete girsemde basvurumuzun 6. gununde bugun ptt den elemanlar gelip telefon hattimizi bagladilar.. kisa bir sure sonra insallah faal bir sekilde tekrar buralarda olacam
August 1st, 2006 at 06:31:49
Aman dikkat et, wireless network üzerinden internete girerken paylaşımın falan olmasın, neme lazım komşun da bir internet kurdu olabilir, yada kendini hacker zanneden cracker olabilir
Daha thread’ler hakkında söylenebilecek pek çok şey var..Özellikle birden fazla thread’in ortak kullandığı kaynakların erişimi konusunda.. Fiziksel bir dosya, bir değişken vb.. Burada CriticalSection denilen yapılar devreye giriyor.. Eğer paylaşımı birden fazla EXE içerisindeki thread yapacaksa o zaman Mutex’ler devreye giriyor vs..
Belki bir ara onlar hakkında da ufak çaplı bir bilgi verebilirim.
Saygılar, sevgiler..
Tuğrul HELVACI
October 26th, 2006 at 12:17:02
S.a.
Hocam eline sağlık çok güzel bir döküman olmuş bu DB olayları ilgilide örnek hazırlaman fadalı olabilir bizler için bu tip kaliteli döküman oluşturmanın zor olduğunu bilirim kolay gelsin…
December 22nd, 2006 at 09:58:27
Hocam harikasın, kolay bulunamayan ve çok yararlı bilgiler olmuş.Tam aradığım şeydi,ellerine sağlık
March 14th, 2007 at 10:23:01
Merhabalar ben thread kullanımına biraz uzağım bir türlü düzgün kullanamadım.Benim sorunum şu bir tane ekranımda 2 tane timerım var ve belli zaman aralıklarında çalışıp bir rapor oluşturuyorlar.sorun timerın zamanı geldiğinde çalıştıracağı procedure thread içinde çalıştırıyorum ama bu procedurun içinde başka bir procedure veya function çağırıyorum ama o çalıştığı zaman ekran kilitleniyor gibi oluyor yani thread ‘ın dışına çıkıyor .bana yardımcı olabilir misiniz eğer bana cevap vereceseniz mail adresime yazarsanız sevinirim.Bu arada makaleniz güzel olmuş teşekkürler.
Saygılarımla
Hülya
March 14th, 2007 at 07:17:19
Timer deyince biraz duralamak lazım. “İstediğiniz zaman” geldiğinde OnTimer a girince Timer.Enabled := False; ve yapılacak işler yapılıp OnTimer dan çıkarken de Timer.Enabled := True; yapıyor musunuz? Burada sanki peşpeşe tetikleme var gibi hissettim;)
March 15th, 2007 at 07:53:26
Aslında şu şekilde anlatmam gerekir Timer zamanı gelince bir thread içinde proceduremu çalıştırıyor bu arada timer.enabled := false yapıyorum işin bitince timer.enabled := true yapıyorum.Sorunum yada anlamadığım nokta thread’e tanımladığım procedure çalışıyor ama procedurun içinde bir procedur daha çağırdığım zaman işte o zaman kilitleniyor gibi oluyor yani multi thread olmaktan çıkıyor .bende çağırdığım 2.procedure tekrar bir thread oluşturuyorum ama sanki bu bana doğru gelmiyor .Eğer sizin bir fikriniz varsa ve yardımcı olursanız çok sevinirim .
Teşekkürler
March 28th, 2007 at 03:44:56
Ooo Hocalar, herkes buradaymış
, şu gugıl ‘ın gözünü seveyim :))
Makale çok faydalı olmuş, teşekkürler…
March 31st, 2007 at 09:50:59
Mrb,
SendMessage veya PostMessage kullanabilirsiniz demişsiniz yalnız SendMessage senkron çalışır yani karşı Thread mesaj ile işini bitirmeden geri dönmez. Yani normalde SendMessage ve Synchronize kullanmak arasında bir fark yoktur. Eğer SendMessage’in ana Thread’deki process’in işini bitirmden dönmesini istiyorsak, o zaman Mesaj’ı alan Thread’in Mesaj Routine’inde hemen ilk iş olarak ReplyMessage dememiz lazım. Yani:
procedure TXForm.GetMessage(var M:TMessage);
var
.
.
begin
ReplyMessage(1); // Mesajı yollayan Thread’i serbest bırak
// zaman alan işleri yapmaya devam et
.
.
end;
Yukardaki yöntem PostMessage’a tercih edilmedlir çünkü PostMessage gönderilen mesajı karşı tarafın alıp almadığını size söyleyemez. SendMessage ise size bunu söyler.
April 14th, 2007 at 01:11:43
Ek olarak;
Sycronize edilen çağrılar bir kuyruktu toplanır, ve applicationın mesajları alıp dağıtan anaa döngüsü mesaj kuyruğunda mesaj kalmayınca idle olmadan önceki yaptığı son iş synchronize kuyruğunda bekleyen çağrıları çalıştırmaktır.
Onun için Synchronize yerine postmessage veya sendmessage kullanmanın tek bir avantakjı kuyruğa o andan sonra eklenen mesajları beklemeden çağrıyı gerçekleştirebilirsiniz. Çünkü sizde kuyruğa girmiş olursunuz. Synchronizede ise her zaman kuyruğun boşalıp idle olmayı beklemek durumundasınız. Çok yoğun şekilde mesaj alan bir uygulamada avantajlı bir durum…
Timer’ın çalışma şekli ise message yoluyla oluyor. Bir apiyle windows kuruluyor ve programa belirlenmiş bir interval de timer mesajı yolluyor uygulamanın message queue’sine(buda ana threaddedir), eğer program idle durumdaysa bu şekilde idle olmaktan çıkıp timer nesnesinin eventi bu mesajla tetikleniyor. Eğer değilse, (yanlış hatırlamıyorsam)kuyruğun her zaman en sonunda bekletiliyor bu timer messagaı ve yalnız kuyruk boşaldığı zaman işleme konuluyor.