Net-Base Dergi

15.05.2026

TThread ve Synchronize ile UI kilitlenmeleri olmadan: VCL ve legacy kod için sağlam desenler

UI'nin kilitlenmesine neden olmadan TThread, Synchronize ve Queue ile güvenilir şekilde çalışma: tipik deadlock nedenleri, pratikte uygulanabilir bir UI-Dispatcher deseni (zaman aşımı dahil), kapanış koruması, kilit stratejileri ve büyümüş Delphi uygulamaları için hata ayıklama kontrolleri.

15.05.2026

Delphi ile Threads üzerinde çalışanlar er ya da geç TThread.Synchronize ile karşılaşırlar. Ve tam da orada hoş olmayan şeyler olur: aralıklı takılmalar, „UI yanıt vermiyor“, kapanırken veya bir diyaloğu açarken görünüşte rastgele Deadlocks. Sorunun özü nadiren „Delphi ist kaputt“ dur; neredeyse her zaman Synchronize, bloklayan bekleme işlemleri ve Message Loop’ını (VCL’nin olay işleme mekanizmasını) artık düzgün işleyemeyen bir UI-Thread’in uğursuz bileşimi söz konusudur. Bu yazı, Legacy bağlamında uygulanabilir ve sağlam kalıpları gösterir: TThread und Synchronize ohne UI-Deadlocks — zaman aşımı varyantı, düzgün hata aktarımı, kapatma kuralları ve gerçek Bestandsanwendungen’da yardımcı olacak debugging ipuçları dahil.

Neden Synchronize etrafında pratikte Deadlocks oluşur

Synchronize şunu ifade eder: Bir worker-Thread, im Main Thread çalıştırılmak üzere bir prosedürü kuyruğa koyar ve genellikle bu prosedürün bitmesini bekler. VCL uygulamalarında Main Thread aynı zamanda UI-Thread’tir (pencereler, kontroller, olaylar). Ek olarak birçok kurulumda orada COM nesneleri STA-Modell içinde çalışır (Single-Threaded Apartment: COM çağrıları aynı iş parçacığında işlenmelidir), bu da çalışan bir Message Loop’a olan bağımlılığı daha da artırır.

Deadlock’lar tipik olarak şu durumlardan kaynaklanır:

  • Main Thread’da WaitFor: UI-Thread bir worker’ı bekler (ör. MyThread.WaitFor), o sırada worker Synchronize aracılığıyla UI-Thread’e ihtiyaç duyar. İkisi bekler — sonuç: deadlock.
  • Lock-Inversion: Worker bir kilidi tutar (ör. TCriticalSection veya TMonitor) ve Synchronize çağırır. Senkronize edilen UI prosedürü aynı kilidi almaya çalışır (doğrudan veya dolaylı, sıklıkla logging/cache/singleton’lar üzerinden) — klasik deadlock.
  • Shutdown/Destroy: Bir form kapatılırken bir thread sonlandırılırken halen Synchronize-görevleri sıradaysa. Özellikle kötü olan: senkronize çağrılar o anda yok edilen kontrolleri referans eder.
  • Message Loop blockiert: Modaller, uzun süren UI işlemleri, bloklayan bir COM çağrısı veya „birazcık“ DB/REST yapan bir handler Main Thread’i meşgul eder. Synchronize-görevleri gecikmeli veya hiç işlenmez.

Mimari ve işletme için en önemli sonuç: Synchronize bir bloklama kenarıdır. İthalatlar, BDE-Ablosung mit nativer Anbindung-sorguları, Schnittstellen-Jobs veya UI bileşeni olan arka plan servisleri içeren bireysel kurumsal yazılımlarda bu kenar bilinçli olarak kontrol edilmelidir — yoksa „nadiren“ zamanla „acil olduğunda her sefer“e dönüşür.

Grundregel: UI-Thread nie auf Worker warten lassen (wenn Synchronize im Spiel ist)

Eğer bir worker bir yerde Synchronize kullanıyorsa, Main Thread bu workeri sertçe bloklayarak beklememelidir. Bu basit geliyor, ancak Legacy kodda en sık rastlanan nedenlerden biridir; çünkü „kapatırken biraz bekleyelim“ veya „Progress-Dialog wartet auf Ende“ ifadeleri hızlıca eklenir.

Pratik çıkarımlar:

  • Worker içinde Synchronize kullanan bir yol var ise UI iş parçacığında WaitFor çağrıları yapılmamalıdır.
  • İş parçacığının tamamlanmasını Event/Callback ile bildirin: UI yanıt verir durumda kalır, temizliği sinyal geldikten sonra yapar.
  • UI güncellemelerini temel olarak TThread.Queue veya bir Dispatcher üzerinden gönderin, böylece Worker’lar bloke olmaz.

TThread.Queue genellikle daha iyi bir varsayılan seçenektir: Worker işi Main Thread’e gönderir, çalışmaya devam eder ve bloke olmaz. Bu birçok deadlock’u önler. Ancak tüm kenar durumları çözmez – örneğin bir Worker’da zorunlu olarak Main Thread’te üretilen bir sonuca ihtiyacınız varsa (ör. bir UI’ye bağlı bir kaynağa erişim veya thread-bağlı bir bileşen).

TThread ve Synchronize ile UI deadlock’ları olmadan: temiz devretmeler için düşünce modeli

Güçlü bir düşünce modeli şudur: Main Thread’e yapılan senkron devretmeler çok az ve meşrudur. Geri kalan her şey durum, sunum veya telemetri olup asenkron olmalıdır.

Basit bir sınıflandırma, incelemelerde ve mevcut projelerin stabilizasyonunda yardımcı olur:

  • „Sadece göster“: İlerleme, log satırı, sayaç, trafik ışığı, etkin/pasif – her zaman Queue.
  • „Durum devri“: Worker veri nesnesi/DTO sağlar, UI render eder – Queue, ancak kopyalama/değiştirilemezlik ile (yani ortak olarak değiştirilmiş yapılar yok).
  • „UI karar vermeli“: Sadece burada senkron semantiğe ihtiyacınız olur (ör. kullanıcı sorgusu). Asıl soru: Gerçekten bir Worker beklemeli mi, yoksa iş akışı yeniden düzenlenebilir mi (Durum Makinesi, işi iptal etme, daha sonra devam ettirme)?

Üçüncü kategori özellikle bir deadlock tuzağıdır: Worker UI sonucunu beklediğinde, UI de kolayca Worker’ı beklemeye (veya dolaylı olarak kilitler yoluyla) teşvik edilir. Bu, yük altında, yavaş veritabanlarında veya Uzak Masaüstü ortamlarda çok daha kolay ortaya çıkar.

Kaynak örneği: UI-Dispatcher, Queue, isteğe bağlı Timeout ve temiz kapanış

Aşağıdaki desen UI devretmelerini küçük bir yardımcı sınıfa sarar. Size şunları sunar:

  • Post: TThread.Queue üzerinden fire-and-forget (genellikle durum güncellemeleri için).
  • Call: Senkron çağrı, Timeout ile (alışılmadık, ama Legacy durumlarda faydalı), doğrudan Synchronize‚ı blok noktası olarak kullanmadan.
  • Shutdown-Schutz: Yeni UI işlerini kabul etmeme; kuyruğa alınan işler kontrolleri değiştirmeden önce bir bayrağı kontrol eder.

Teknik sınıflandırma: Geri bildirim için Queue artı TEvent (bir Kernel-Event) kullanıyoruz. Worker Synchronize‚ı beklemiyor, onun yerine kuyruğa alınan eylem yürütüldükten sonra Main Thread’te set edilen bir eventi bekliyor. Timeout, UI-thread’in herhangi bir nedenle işi işlemeyi durdurması durumunda “sonsuz” beklemeyi engeller.

Delphi
unit UiDispatch;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs;

type
  EUiDispatchTimeout = class(Exception);
  EUiDispatchShuttingDown = class(Exception);

  /// <summary>
  ///  Worker iş parçacıklarından gelen UI çağrılarını kapsüller.
  ///  Post: asenkron (Queue).
  ///  Call: zaman aşımı ile eşzamanlı, TThread.Synchronize ile doğrudan bloke etmeden.
  /// </summary>
  TUiDispatcher = class
  strict private
    class var FShuttingDown: Integer;
  public
    class procedure BeginShutdown; static;
    class function IsShuttingDown: Boolean; static;

    class procedure Post(const AProc: TProc); static;
    class procedure Call(const AProc: TProc; ATimeoutMs: Cardinal = 5000); static;
  end;

implementation

{ TUiDispatcher }

class procedure TUiDispatcher.BeginShutdown;
begin
  TInterlocked.Exchange(FShuttingDown, 1);
end;

class function TUiDispatcher.IsShuttingDown: Boolean;
begin
  Result := TInterlocked.CompareExchange(FShuttingDown, 0, 0) = 1;
end;

class procedure TUiDispatcher.Post(const AProc: TProc);
begin
  if not Assigned(AProc) then
    Exit;

  // Shutdown sırasında yeni UI işleri kabul etme.
  if IsShuttingDown then
    Exit;

  // Queue, worker'ı engellemez.
  TThread.Queue(nil,
    procedure
    begin
      if IsShuttingDown then
        Exit;
      AProc();
    end);
end;

class procedure TUiDispatcher.Call(const AProc: TProc; ATimeoutMs: Cardinal);
var
  DoneEvent: TEvent;
  RaisedObj: TObject;
begin
  if not Assigned(AProc) then
    Exit;

  if IsShuttingDown then
    raise EUiDispatchShuttingDown.Create('UI-Dispatcher kapatma aşamasında.');

  DoneEvent := TEvent.Create(nil, True, False, '');
  try
    RaisedObj := nil;

    TThread.Queue(nil,
      procedure
      begin
        try
          if not IsShuttingDown then
            AProc();
        except
          // Exception objesini thread sınırları üzerinden iletme.
          // Dikkat: Burada "raise" kullanmayın, aksi halde ana iş parçacığına düşer.
          RaisedObj := AcquireExceptionObject;
        end;
        DoneEvent.SetEvent;
      end);

    case DoneEvent.WaitFor(ATimeoutMs) of
      wrSignaled:
        begin
          if Assigned(RaisedObj) then
            raise Exception(RaisedObj);
        end;
      wrTimeout:
        raise EUiDispatchTimeout.CreateFmt(
          ' %d ms sonra zaman aşımı: Ana iş parçacığı UI çağrısını işlemedi.',
          [ATimeoutMs]);
    else
      raise Exception.Create('UI-Dispatcher içinde beklenmeyen WaitFor durumu.');
    end;
  finally
    DoneEvent.Free;
  end;
end;

end.

Kodun amacı ve bilerek ‚alışılmadık‘ olduğu noktalar

Bu desen Synchronizei tamamen yerine koymaz, ancak eşzamanlı geçişleri kontrollü hale getirir: Worker, Synchronize mekanizmasını değil bir Eventi bekler. Böylece zaman aşımı zorlayabilir, çalışırken UI iş parçacığının takıldığını görünür kılabilir ve kapanış (Shutdown) aşamasında yeni UI işlerini tutarlı şekilde reddedebilirsiniz.

Alışılmadık olan kısım Event değil; eşzamanlı semantiği Queue + Event ile modelleme kararıdır. Bu yaklaşım, mevcut (Bestands-) uygulamalara adım adım kararlılık eklemek gerektiğinde ve her Synchronize noktasını hemen mimari olarak yeniden düzenleyemeyeceğiniz durumlarda işe yarar.

Sınır koşulları ve dikkat edilmesi gerekenler

  • Bellek görünürlüğü: DoneEvent senkronizasyon sınırıdır. Bu sayede WaitFor sonrası RaisedObj okuması tutarlı olur. Yine de RaisedObj her çağrı için yerel kalmalı (buradaki gibi), asla global olmamalıdır.
  • İstisna yönetimi: AcquireExceptionObject, istisnanın ana iş parçacığında ‚kaybolmasını‘ önler. Worker’da yeniden fırlatıldığında stack trace kökeniyle birebir olmayabilir, ancak hata mesajı Worker-log’unda kalır ve görev düzgün şekilde başarısız olabilir.
  • Zaman aşımı hem teşhis hem korumadır: O, bloke olmuş bir ana iş parçacığını ‚tamir‘ etmez. Ancak Worker’ların sınırsız kaynak bağlamasını (ör. BDE-Ablosung mit nativer Anbindung-Transaktionen açık tutmak) engeller ve hata sınıfını ölçülebilir kılar.
  • Kapatma erken başlamalıdır: BeginShutdown merkezi bir kapatma dizisine ait olmalıdır (ör. ana formun OnCloseQuery olayında çok erken). Aksi halde pencereler zaten yok edilirken UI işler hâlâ kuyruğa alınır.

Lock-Strategie: UI-Callback’lerle kilit inversiyonlarını nasıl önlersiniz

Birçok deadlock WaitFor yüzünden değil, belirsiz bir kilit sıralaması yüzünden oluşur. Tipik akış: Worker „Datenmodell“i kilitler, Synchronize ile bir UI güncellemesi çağırır, UI güncellemesi yeniden „Datenmodell“e erişir. Mantıksal olarak anlaşılabilir, ancak teknik olarak ciddi sorun yaratır.

Ekiplerde uygulanabilecek pratik kurallar:

  • İş parçacığı sınırları boyunca kilit tutmayın: Bir Worker UI yönüne herhangi bir şey kuyruğa almadan/senkronize etmeden önce, iş ile ilgili kilitler serbest bırakılmalıdır.
  • UI snapshot’ları okumalıdır: UI-Callback’leri Worker yapılarına ‚canlı‘ olarak bakmamalı, bunun yerine kopyalar/snapshot’lar göstermelidir (ör. DTO, Record, basit değerler).
  • Logging bir kilit adayıdır: Eğer logging dahili olarak bir kuyruk, dosya kilidi veya bir singleton kullanıyorsa, deadlock’un bir parçası olabilir. UI-Callback’leri logging’i asgari seviyede tutmalı veya ayrı, bloklamayan bir log hattına yazmalıdır.

Eğer zaten bir Layer-3-mimariniz (UI, Services/Domäne, altyapı olarak veri erişimi gibi) varsa: UI-Callback’leri ideal olarak yalnızca UI işi yapmalıdır. „Service“ olan her şey callback içine girmemelidir. Bu, yeniden giriş (reentrancy) etkilerini belirgin şekilde azaltır.

Kilitlenmeden kapanış: „nicht WaitFor, sondern kooperatives Stoppen“

Kapanışta sık sık sorun çıkar: UI kapanır, bir iş parçacığı sonlandırılmalı ama kuyruğa alınmış UI işler hâlâ açık olur. Temiz bir shutdown, „iş parçacığını öldürmek“ten ziyade küçük bir koreografidir:

  1. Kapatma bayrağı ayarlayın (ör. TUiDispatcher.BeginShutdown): Bu andan itibaren yeni UI işleri kabul edilmesin.
  2. Worker’ı işbirlikçi biçimde durdurun: Worker bir iptal bayrağını (ör. TEvent veya TCancellationToken-benzeri) kontrol eder ve döngüleri/wait’leri sonlandırır.
  3. UI’yi engellemeyin: Ana iş parçacığında sert bir bekleme döngüsü olmayacak. „Beklemeniz gerekiyorsa“ bunu yalnızca mesaj döngüsü çalışırken yapın (veya daha iyisi: bitişi bir callback ile işleyip tamamen kaçının).
  4. Son UI temizlik işleri yalnızca pencerelerin/controlllerin hâlâ var olduğu garanti edilebiliyorsa yapılmalı. VCL’de zamanlama kritiktir: handle kaybolduğunda en geç, kuyruğa alınmış işler artık controllere yönlendirilemez.

Bu süreç işletme ve destek için önemlidir: „Uygulama kapanırken takılıyor“ klasik bir kabul problemidir, hatta iş mantığı doğru işlendiğinde bile ortaya çıkabilir. Tanımlı bir shutdown burada gerçek zaman kazandırır.

Debugging: Deadlock’u somut hale nasıl getirirsiniz (tahmin yapmadan)

Eğer sistem takıldığında, temel soru şudur: Kim kimin için bekliyor? Mevcut projelerde işe yarayan birkaç yaklaşım:

  • Tüm bekleme noktalarını envanterleyin: Döngülerdeki WaitFor, Sleep, TEvent.WaitFor, INFINITE için tam metin araması. Birçok sorun „gizli“ beklemelerden kaynaklanır (kütüphaneler içinde de olabilir).
  • Thread durumunu loglayın: Thread sınırlarında loglayın: „Job startet“, „queued UI“, „UI ausgeführt“, „Job fertig“. Böylece Main Thread’in kuyruğa alınmış işleri gerçekten işleyip işlemediğini görürsünüz.
  • Message-Loop-Verdacht prüfen: Donma sadece modal dialoglarda veya belirli COM etkileşimlerinde ortaya çıkıyorsa, genellikle Message Loop dar boğazdır. Hedef: UI-handler’ları hafifletmek, COM çağrılarını izole etmek, UI içinde uzun operasyonlar yapmamak.
  • Lock’ları görünür kılın: TCriticalSection/TMonitor kullanılıyorsa, „Owner“ meta verisi (ör. Enter anındaki Thread-ID) ve zaman ölçümü ile bir Debug-Build faydalıdır. Böylece Worker’lar UI için beklerken Main Thread’in hangi lock’u tuttuğunu görürsünüz.

Önemli olan yaklaşım: Deadlock’lar nadiren „rastgele“ ortaya çıkar. Bunlar deterministik döngülerdir ve nadiren tetiklenir. Döngüyü bir kez düzgün şekilde tanımladığınızda, çözüm genellikle açıktır.

Varianten für Datenzugriff und Schnittstellen-Jobs (FireDAC, REST, Dateisystem)

Özellikle FireDAC (veya diğer DB erişimleri) söz konusuysa: Bağlantı, işlem ve Datasets pratikte iş parçacığına bağlıdır. Bir Worker-Thread kendi DB-bağlamına tek başına sahip olmalıdır. UI çağrıları sunuma odaklanmalı, DB operasyonları yapmamalıdır. Dayanıklı bir desen şudur:

  1. Worker, Query/REST çağrısı yapar, sonucu hesaplar ve DTO üretir.
  2. Worker DTO’yu Queue/TUiDispatcher.Post üzerinden UI’ye post eder.
  3. UI DTO’yu devralır ve Controls’u günceller (Worker-objelerine geri başvurmadan).

Eğer tarihsel olarak oluşmuş karışık formlarınız varsa („UI DB’yi tetikliyor, DB callback’i UI’yi tetikliyor“), kademeli olarak gevşetme yapmaya değer: Önce teslim noktalarını izole edin (Dispatcher), sonra durumları Services/Model’e taşıyın. Bu büyük bir yeniden yapılandırmadan daha az risklidir, ancak deadlock’ları belirgin şekilde azaltır.

Fazit: Deadlocks vermeiden heißt Übergaben kontrollieren

TThread und Synchronize ohne UI-Deadlocks bir tekniğin ötesinde bir disiplindir: Engelleri en aza indirmek, lock sıralamalarını temiz tutmak, Shutdown tanımlamak ve senkron UI-bağımlılıklarını azaltmak. Gösterilen UI-Dispatcher legacy durumlarda özellikle yararlıdır; çünkü varsayılan olarak Queue kullanır, gerekli senkron geçişler için ise Timeout ve net Shutdown kuralları ekler.

Sınırlamalar devam eder: Eğer Main Thread sürekli bloke oluyorsa (ağır UI mantığı, modal dialog zincirleri veya COM-STA-Aufrufe nedeniyle), Dispatcher yalnızca teşhis edebilir ve kontrollü bir şekilde kesintiye uğratabilir. Kalıcı çözüm, UI’yı hafifletmek ve sorumlulukları ayırmaktır. Mevcut bir Delphi uygulamasında—threading tuzaklarından kademeli stabilizasyona kadar—destek ihtiyacınız varsa, girişimi burada sınıflandırabilirsiniz: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Uzmanlık alanında, entegrasyonlar, veri akışları ve sürdürülebilir geliştirme temiz bir şekilde birlikte çalışması gerektiğinde Delphi Multithreading ve Synchronize Deadlock da önemli rol oynar.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp ve e-posta hemen kullanılabilir. Instagram için bağlantı ve kısa metni doğrudan hazırlıyoruz.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.