Dergi konusundan proje pratiğine
İçeriğe Uygun Hizmet ve Teknik Sayfalar
Wer in Delphi rechenintensive oder I/O-lastige Jobs parallelisieren will, landet schnell bei der Parallel Programming Library (PPL) und konkret bei TParallel.For. Der Effekt ist oft sofort messbar – bis man im selben Atemzug eine Progress-UI „mal eben“ aktualisieren möchte. Genau hier entstehen die typischen Hänger: scheinbar zufällige UI-Freezes, eine ProgressBar, die rückwärts springt, oder ein kompletter Deadlock, sobald man im Debugger Schritt für Schritt läuft.
In diesem Beitrag geht es um TParallel.For threadsichere Progress-UI: ein robustes Muster, das in VCL und FMX funktioniert, UI-Updates über TThread.Queue sauber bündelt, Cancel/Abort berücksichtigt und die häufigsten Deadlock-Fallen konsequent umgeht. Der Fokus liegt auf Betriebsrealität: reproduzierbares Verhalten, verständliche Zuständigkeiten und Debugging-Hinweise, die auch dann helfen, wenn der Fehler nur „bei Kunden“ auftritt.
Warum Progress-UI bei TParallel.For so oft schiefgeht
TParallel.For läuft typischerweise auf Worker-Threads aus dem Delphi-Threadpool. Diese Threads dürfen keine VCL- oder FMX-Controls direkt anfassen, weil die UI-Frameworks (Message Loop, Window Handles, Rendering) an den Main Thread gebunden sind. Schon ein scheinbar harmloses ProgressBar.Position := … aus einem Worker kann zu undefiniertem Verhalten führen: sporadische AVs, eingefrorene Fenster oder „flackernde“ Updates.
Die naheliegende Reparatur ist oft TThread.Synchronize. Das behebt zwar die Thread-Safety, führt aber in parallelen Schleifen schnell zu einem anderen Problem: Sie erzeugen einen seriellen Engpass. Jeder Worker wartet auf den UI-Thread, der wiederum mit dem Rendern und dem Abarbeiten von Synchronize-Aufrufen beschäftigt ist. Unter Last sieht das wie ein Deadlock aus – auch wenn es „nur“ ein starvation/lockstep-Effekt ist.
Und dann gibt es die echte Deadlock-Klasse: Der Main Thread wartet (z. B. per WaitFor, Task.Wait oder indirekt über blockierende Aufrufe) auf das Ende der Parallel-Operation, während Worker-Threads versuchen, per Synchronize oder ungünstiger Queue-Nutzung Arbeit in den Main Thread zu schicken. Ergebnis: Main Thread wartet auf Worker, Worker warten auf Main Thread.
TThread.Queue vs. TThread.Synchronize: der praktische Unterschied
Beide Mechanismen dienen dazu, Code sicher im Main Thread auszuführen. Der Unterschied ist die Wartesemantik:
- TThread.Synchronize: Der aufrufende Worker wartet, bis der Main Thread den Code ausgeführt hat. Das ist „synchron“, erhöht Latenzen und ist eine klassische Zutat für Deadlocks, wenn der Main Thread gerade blockiert.
- TThread.Queue: Worker kodu yalnızca Main Thread için bir kuyruğa ekler ve işlemeye devam eder. Bu „asenkron“dur, iş parçacıklarını birbirinden ayırır ve paralel senaryolarda — güncelleme sıklığı kontrol edildiği sürece — neredeyse her zaman daha iyi bir varsayılan tercihtir.
Wichtig: Queue ist kein Freifahrtschein. Wenn Sie in jeder Iteration einer Schleife ein Queue-Update schicken, überfluten Sie die Main-Thread-Queue. Dann hängt die UI zwar nicht wegen Deadlock, aber wegen schierer Menge an Nachrichten. Die UI wirkt „zäh“, und das Ende der Verarbeitung verzögert sich, weil noch hunderte oder tausende UI-Updates nachlaufen.
Gerçekten can yakan uç durum: UI-Thread’te beklemek
Kurumsal uygulamalarda sıklıkla şu akış görülür: Button „Start“ tıklanır, UI devre dışı bırakılır, ProgressDialog açılır, sonra her şey bitene kadar senkron olarak „beklenir“ ve ardından tekrar etkinleştirilir. Bu desen birçok Deadlocks çekirdeğidir.
Tipik varyantlar (kod tabanına bağlı olarak):
- Main Thread startet TParallel.For und ruft danach eine blockierende Wartelogik auf (direkt oder indirekt).
- Ein ProgressDialog ruft im Constructor oder OnShow eine Routine auf, die intern wartet.
- Ein Abbrechen-Button setzt zwar ein Flag, aber der Main Thread bleibt trotzdem in einem Warteloop hängen.
Bu sırada Worker-Threads Synchronize kullanırsa, Deadlock neredeyse garantidir. Mit Queue kann es ebenfalls hängen, wenn der Main Thread blockiert und keine Messages pumpt – denn dann wird auch die Queue nicht abgearbeitet.
Die betriebsfeste Konsequenz lautet: Der Main Thread darf nicht blockierend auf die Parallel-Schleife warten, wenn parallel UI-Updates benötigt werden. Stattdessen muss die Verarbeitung entweder vollständig in einen Hintergrund-Task ausgelagert werden, oder man organisiert ein „asynchrones Ende“ (Callback/Queued Abschlussaktion), das die UI am Schluss wieder freigibt.
Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt
Sağlam bir desen üç açık ayrı sorumluluktan oluşur:
- Worker-Threads machen die eigentliche Arbeit pro Element/Index. Sie melden nur Fortschritt in einer threadsicheren Form (Zähler, Queue, Thread-safe Queue).
- Aggregator (oft: Main Thread oder ein dedizierter Timer im UI) berechnet aus dem Fortschritt einen UI-Status (Position, Text, ETA) und aktualisiert Controls. So vermeiden Sie 1:1-Updates pro Iteration.
- Abschluss (ebenfalls im Main Thread): UI reaktivieren, Ergebnis anzeigen, Fehler zusammenfassen, Ressourcen freigeben.
Bu ayrımın neden bu kadar iyi çalıştığı: İş yükü yüksek frekanslı olabilir (binlerce öğe), ancak UI saniyede sadece birkaç güncelleme gerektirir. Pratikte 5–10 güncelleme/saniye yeterlidir, çok hızlı işler için 2–4 bile. Bunun üzerindeki her şey genellikle sadece görsel gürültüdür ve Message Pump’ta CPU süresi harcar.
İş parçacığı güvenli sayma: Lock yerine atomik
Basit bir ProgressBar için genellikle atomik bir sayaç yeterlidir. “Atomik” demek: Artırma ve okuma yarış durumu olmadan gerçekleşir, tipik olarak TInterlocked üzerinden. Bu sayede döngünün hot path’inde Lock’ları (Critical Sections) önlersiniz.
Kullanımda kendini kanıtlamış minimal fikir:
- Toplam miktar önceden biliniyor (örn. kayıt sayısı, dosyalar, ID’ler).
- Her yineleme bir DoneCounter‚ı atomik olarak arttırır.
- Bir UI-Timer sayacı periyodik olarak okur ve ProgressBar.Position‚ı ayarlar.
Avantaj: Her öğe için TThread.Queue yok, UI taşması yok. Dezavantaj: Her öğe için ayrıntılı mesajlarınız olmaz (örn. dosya adı). Bunun yerine frekansı azaltılmış ikinci bir durum bildirimi ekleyebilirsiniz (bkz. sonraki bölüm).
Spam olmadan durum bildirimleri: „son durum kazanır“
Ek olarak kısa bir metin göstermek istiyorsanız (mevcut öğe, aşama, hata mesajı), her Worker adımında UI’yi sel halinde boğmayacak bir desene de ihtiyaç vardır. Pratikte “son durum kazanır” yaklaşımı çok iyi çalışır:
- Worker bir durum bilgisini iş parçacığı güvenli bir yapıya yazar (örn. atomik olarak değiştirilebilen bir string veya küçük bir Lock ile korunmuş).
- UI-Timer periyodik olarak en son görülen durumu bir Label’a aktarır.
Böylece UI yanıt verebilir kalır ve yine de “bir şeylerin olduğunu” görürsünüz. Burada stringin kendisinden daha önemli olan yaşam süresidir: Worker thread’lerinden gelen kısa ömürlü nesnelere dair referansları UI thread’ine taşımayın. Nesneler geçiriyorsanız sahipliği (ownership) açıkça netleştirin.
TParallel.For iş parçacığı güvenli Progress-UI ile TThread.Queue: sağlam bir desen
Bazı senaryolarda bir UI-Timer tek başına yeterli olmayabilir: örneğin sonunda kesin olarak tek bir “Fertig” güncellemesi tetiklemek istiyorsanız veya UI-güncellemesi daha karmaşık bir adımsa (örn. bir log penceresine kayıt, ama sınırlı sıklıkta). Bu durumlarda TThread.Queue uygundur — ama yine de her iterasyon için değil, hedefe yönelik olarak.
Pratik bir yaklaşım düşük frekanslı olaylar için sadece bir Queue tutmaktır:
- Start-Event (UI hazırlama, butonları kilitleme)
- Periyodik Progress-Events (en fazla her X Millisekunde)
- Fehler-Events (isteğe bağlı olarak toplanmış)
- Done-Event (UI’yi sıfırlama, sonucu gösterme)
Periyodu UI-Thread üzerinden değil, Worker bağlamında sağlayın: Worker’ların bir UI-güncellemesini queuelemesine yalnızca son UI-güncellemesinden yeterli süre geçmişse izin verin. Bunun için monoton bir zaman kaynağı (örn. TickCount) ve atomik bir “last update” değeri uygundur.
Önemli: UI-güncellemesi kendisi “hızlı” olmalı. Maliyetli hesaplamalar, dosya I/O veya veritabanı erişimleri kuyruklanan UI-callback’ine konulmamalıdır. Callback yalnızca durumları okumalı ve kontrolleri ayarlamalıdır.
Cancel-Handling: Kilitlenmeden iptal etmek
Gerçek uygulamalarda iptal seçmeli değildir. Önemli olan: Cancel bir “Kill” değil, kooperatif bir sonlandırmadır. Worker’lar düzenli olarak bir iptal sinyalinin ayarlanıp ayarlanmadığını kontrol etmeli ve sonra temiz şekilde çıkmalıdır. Delphi içinde bunun için (PPL yapısına bağlı olarak) birkaç yol vardır: kendi bir Volatile flag’i, atomik bir Boolean veya Tasks üzerinden bir Cancellation-kavramı (Delphi sürümüne ve mimarisine bağlı olarak).
İşletme açısından önemli iki kural vardır:
- İptal hızlıca görünür olmalıdır: İptal bayrağını uygun noktalarda kontrol edin, sadece iterasyon sonunda değil, özellikle iterasyon birkaç saniye sürebiliyorsa.
- İptal temizlik yapmalıdır: Açık tutulan Handles, geçici dosyalar, işlemler veya kilitler bırakılmamalıdır. Bu demektir ki: Kaynaklar dahilse her Worker-iterasyonunda try/finally-blokları zorunludur.
UI tarafında İptal yalnızca bir sinyal göndermeli ve UI’yi “Stopping…” durumuna sokmalıdır. Asıl sonlandırma ve UI’nin yeniden etkinleştirilmesi Done-Event’te gerçekleşmelidir, tıklamada hemen değil.
Deadlock’ları önlemek: Uygulamada en sık rastlanan tuzaklar
Tuzak 1: WaitFor/Task.Wait ana iş parçacığında
Ana iş parçacığı bloke olduğunda, Queue-callback’leri çalıştıramaz ve mesajları işleyemez. Bu, Worker’lar doğru şekilde devam etse bile deadlock gibi görünür. Çözüm: UI iş parçacığında bloklayıcı Wait kullanmayın. Bunun yerine bitiş işlemini TThread.Queue üzerinden veya bir olay kontrolüyle (örn. bir timer „fertig“i kontrol eder) yapın.
Tuzak 2: Synchronize bir kilit içinde
Klasik bir durum: Worker bir critical section tutar, sonra Synchronize çağırır ve UI callback’inde (doğrudan veya dolaylı olarak) aynı critical section tekrar gereklidir. Sonuç: bekleme döngüsü. Kural basittir: Tutulan bir kilitten UI teslimi (Synchronize/Queue) yapılmaz. Bir kilit gerekiyorsa, tüm verileri önce yerel değişkenlere alın, kilidi bırakın, sonra queue’layın.
Tuzak 3: UI-Callback yeniden giriş (Reentrancy) tetikliyor
Bazen UI güncellemesi kendisi „zararsız“ değildir: Property atamaları OnChange, OnResize gibi olayları tetikleyebilir ve bu da Worker durumlarına erişen mantığı çalıştırabilir. Bu dar anlamda bir deadlock olmasa da açıklanması zor donmalara ve race condition’lara yol açar. Çözüm: UI güncellemelerini „sessiz“ yollarla yapmak (olayları geçici olarak devre dışı bırakmak) veya yeniden giriş koruyucuları kullanmak (ör. Update aşaması için atomik bir guard).
Tuzak 4: Çok fazla kuyruklanan güncelleme
Wait’ler olmasa bile, on binlerce queued callback oluşturuyorsanız UI „donabilir“. Belirtiler: ProgressBar uzun süre arkasında kalır, pencere yavaş tepki verir, ana iş parçacığında CPU kullanımı yüksek olur. Çözüm: Durdurma (zaman penceresi), birleştirme (counter), veya yalnızca bir UI-güncellemesinin „pending“ olmasına izin veren gerçek bir Producer/Consumer yapısı (Coalescing).
Daha karmaşık olduğunda: Sonuçları toplama, hataları gruplayma, sıralamayı garanti etme
TParallel.For iterasyonlar bağımsızsa idealdir. İş yazılımlarında iterasyonlar genellikle yalnızca „büyük ölçüde“ bağımsızdır: dosyalar okursunuz, REST-API’lerini çağırırsınız, veri tabanı satırları yazarsınız. Bu durumda üç ek noktayı düzgün planlamalısınız:
- Thread-güvenli sonuç toplama: Ya her thread için yerel bir buffer (sonunda birleştirilir) ya da thread-güvenli bir Queue/Collection kullanın. Hot Path’te kilitlerden kaçının.
- Hata yönetimi: Worker iş parçacıklarından gelen istisnalar toplanmalıdır. Pratikte işe yarayan yaklaşımlar: ilk istisnayı kaydetip iptali tetiklemek veya tüm istisnaları toplamak ve sonunda topluca göstermek.
- Sıralama: Çıktının kararlı bir sıralamaya ihtiyacı varsa (ör. indeks bazlı loglar), paralel işlemeyi sonradan sıralamak genellikle „iş parçacığı güvenli sıralı ekleme“ yapmaktan daha kolaydır.
UI için bu şunu ifade eder: her hata mesajını hemen göstermeyin. Bu modal diyalog cehennemine yol açar. Hataları toplayın (ör. string listesi) ve sonunda bir özet veya dışa aktarılabilir bir log gösterin.
Debugging: Deadlock’u gerçekten görünür kılma
Paralel koddaki deadlock’lar sinir bozucudur; çünkü debugger’da release moduna göre farklı görünebilirler. Buna rağmen birkaç pratik yaklaşım vardır:
Threads penceresini ve çağrı yığınlarını kullanma
UI takıldığında tüm iş parçacıklarına bakın: Ana iş parçacığı nerede? Bekliyor mu? Bir mesaj döngüsünde mi? Worker iş parçacıkları nerede bekliyor? Worker’lar Synchronize içinde takılıyorsa, neden neredeyse her zaman ana iş parçacığının bloklanması veya ana iş parçacığının bir kilide ihtiyaç duymasıdır.
Queue-/Synchronize noktalarını işaretleme
Geçiş noktalarına hedefli logging yerleştirin (Queue öncesi, Queue geri çağrısında, iterasyon sonu). Operasyon sırasında timing kritik olduğundan bu genellikle breakpoint’lerden daha değerlidir. Logging’in kendisinin thread-güvenli ve bloklamayan bir yapı olduğundan emin olun (ör. Worker iş parçacıklarından doğrudan UI’ya log yazmayın).
Son güncelleme zamanını ölçme
UI „takılıyorsa“, basitçe çok fazla güncelleme ile uğraşıyor olabilir. Bu yüzden ana iş parçacığında saniyede kaç UI-güncellemesi yaptığınızı ve bunun ne kadar sürdüğünü ölçün. UI geri çağrıları birkaç milisaniyeden fazla sürmeye başlarsa, hız sınırlama veya basitleştirme gerekir.
Wann lohnt sich TParallel.For mit Progress-UI wirklich?
Paralelleştirme amaç için yapılmaz. Özellikle fayda sağlar, eğer:
- iterasyonlar yeterince büyükse (milisaniyeler ila saniyeler), böylece thread pool overhead’i önemsizleşiyorsa,
- iş CPU-ağırlıklıysa (parsing, sıkıştırma, hashing) veya iyi paralelleştirilebilen I/O varsa (birden fazla dosya, limitli birden fazla HTTP isteği),
- net bir iptal ve hata stratejiniz varsa,
- UI gereksinimleri toplulaştırılmış ilerleme ile uyumluysa.
Paralelleştirme daha az faydalıdır, eğer her iterasyon çok kısa ise (mikro işlemler) veya tüm iterasyonlar aynı darboğaza dayanıyorsa (seri bir DB-işlemi, global bir kilit, tek bir dosya). Bu durumda daha hızlı etkisi olan yaklaşımlar genellikle şunlardır: algoritmayı iyileştirmek, batchlemek, veri erişimini azaltmak veya darboğazı açıkça ayrıştırmak.
Uygulama kontrol listesi: UI böyle stabil kalır
- Ana iş parçacığı bloklanmasın: beklemeler olmasın, mesaj pompası olmayan uzun döngüler olmasın.
- Worker’lar kontrol nesnelerine dokunmasın: UI iş parçacığı dışında VCL/FMX erişimi yok.
- UI güncellemeleri sınırlandırılmış olsun: her iterasyon yerine sayaç/zamanlayıcı veya birleştirilmiş Queue-güncellemeleri kullanın.
- Kilitlemelerin içinden Synchronize çağrıları yapmayın.
- İptal işbirlikçi olsun, sıkça kontrol edilsin ve kaynakları temiz bırakacak şekilde tasarlansın.
- İstisnalar toplanır ve sonunda düzenli bir şekilde işlenir.
Sonuç: Queue ile ayrıştırın, agregasyonla stabil hale getirin
Bir sağlam Progress-UI TParallel.For ile “herhangi bir yere Synchronize koymak” sayesinde oluşmaz; bunun yerine net bir mimari ilke gerekir: workerlar bağımsız çalışır, ana iş parçacığı serbest kalır ve yalnızca birkaç, hızlı UI güncellemesini işler. TThread.Queue, hedefli ve kısıtlı kullanıldığında doğru araçtır. “Sıkışmış” veya kararsız davranışların nedeni neredeyse her zaman iki şeydir: ana iş parçacığı bir yerde bloklayarak bekliyor – veya çok fazla sıraya alınmış güncellemede boğuluyor.
Bu deseni bir kez düzgünce kurduğunuzda (Counter/Coalescing, Cancel-Flag, tamamlama geri çağrısı), olgunlaşmış Delphi-uygulamanın birçok yerinde işe yarar: Import/Export, veri doğrulamaları, dosya ve API işleri – her şey daha yanıt verir hale gelir ve her Progress-Update ile yeni deadlock’lar edinme derdi yaşamazsınız.
Paralel kodun stabilize edilmesi, UI takılmalarının debug edilmesi veya olgunlaşmış Delphi-uygulamaların temiz modernizasyonu konusunda desteğe ihtiyacınız varsa: İletişime geçin.
Bu konu için Delphi Parallel Programming Library ve Tthread.queue Vs Synchronize da önemlidir. Yazı bu konuları anlaşılır şekilde sınıflandırır ve günlük kullanımdaki öncelikleri gösterir.
Projeyi veya modernizasyon girişiminizi Net-Base ile görüşün.
Sonraki adım
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Bireysel sorularda destek vermekle kalmıyoruz; kaynak kodu parçacıklarından, legacy konularından veya portal fikirlerinden sağlam bir kurumsal projeye dönüşene kadar da destek veriyoruz.
- Mevcut durum, hedef durum ve teknik riskler birlikte değerlendirilir.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.