Nga tema e revistës në praktikën e projektit
Faqe shërbimi dhe teknike të përshtatshme për artikullin
Kushdo që në Delphi dëshiron të paralelizojë punë me shumë llogaritje ose me I/O të ngarkuar, shkon shpejt te Parallel Programming Library (PPL) dhe konkretisht te TParallel.For. Efekti shpesh matet menjëherë – derisa në të njëjtën frymëmarrje dëshiron të përditësosh „shpejt“ një Progress-UI. Këtu lindin ngecjet tipike: bllokime të dukshme të UI-së, një ProgressBar që kthehet prapa, ose një deadlock i plotë sapo ecën hap pas hapi në debugger.
Ky artikull trajton TParallel.For threadsichere Progress-UI: një model i qëndrueshëm që funksionon në VCL dhe FMX, mbledh në mënyrë të pastër përditësimet e UI-së përmes TThread.Queue, merr parasysh Cancel/Abort dhe shmang në mënyrë konsekuente kurthet më të zakonshme të deadlock. Fokusi është te realiteti i operimit: sjellje riprodhuese, përgjegjësi të qarta dhe udhëzime për debugging që ndihmojnë edhe kur gabimi shfaqet vetëm „tek klientët“.
Pse Progress-UI tek TParallel.For dështon aq shpesh
TParallel.For zakonisht shkon për të punuar në Worker-Threads nga threadpool-i i Delphi. Këto threads nuk duhet të prekin direkt kontrollet VCL ose FMX, sepse framework-et e UI-së (Message Loop, Window Handles, Rendering) janë të lidhura me Main Thread. Edhe një dukshëm i padëmshëm ProgressBar.Position := … nga një worker mund të çojë në sjellje të papërcaktuar: AV të sporadike, dritare të ngrira ose përditësime „të shkëlqyera“ që dukshmërisht shpërthejnë.
Riparimi i natyrshëm është shpesh TThread.Synchronize. Kjo sanksionon sigurinë e thread-it, por në cikle paralele shpejt çon te një problem tjetër: krijon një ngushticë seriale. Çdo worker pret për Main Thread-in e UI-së, i cili nga ana tjetër është i zënë me renderimin dhe përpunimin e thirrjeve Synchronize. Nën ngarkesë kjo mund të duket si një deadlock – edhe nëse në thelb është një efekt starvation/lockstep.
Dhe pastaj ekziston klasa e vërtetë e deadlock-ëve: Main Thread pret (p.sh. me WaitFor, Task.Wait ose në mënyrë indirekte përmes thirrjeve bllokuese) për përfundimin e operacionit paralel, ndërsa Worker-Threads përpiqen të dërgojnë punë në Main Thread përmes Synchronize ose përmes përdorimit të pafavorshëm të Queue. Rezultati: Main Thread pret për Worker, Worker pret për Main Thread.
TThread.Queue vs. TThread.Synchronize: ndryshimi praktik
Të dy mekanizmat shërbejnë për të ekzekutuar kod në mënyrë të sigurt në Main Thread. Diferenca është semantika e pritjes:
- TThread.Synchronize: Worker-i që thërret pret derisa Main Thread të ketë ekzekutuar kodin. Kjo është „synchronous“, rrit latencat dhe është një përbërës klasik për deadlock nëse Main Thread është i bllokuar.
- TThread.Queue: Worker vendos kodin vetëm në një radhë për Main Thread dhe vazhdon ekzekutimin. Kjo është „asinkrone“, dekuplon threads dhe në skenarë paralel është pothuajse gjithmonë zgjedhja parazgjedhur më e mirë – për sa kohë kontrollohet frekuenca e përditësimeve.
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.
Der Randfall, der wirklich wehtut: Warten im UI-Thread
In Unternehmensanwendungen sieht man häufig folgenden Ablauf: Button „Start“ klickt, UI wird deaktiviert, ProgressDialog geht auf, dann wird synchron „gewartet“, bis alles fertig ist, um anschließend wieder zu aktivieren. Dieses Muster ist der Kern vieler Deadlocks.
Typische Varianten (je nach Codebasis):
- 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.
Wenn Worker-Threads in dieser Zeit Synchronize benutzen, ist der Deadlock praktisch garantiert. 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
Ein robustes Muster besteht aus drei klar getrennten Verantwortlichkeiten:
- Worker-Threads erledigen punën actuale për çdo element/indeks. Ata raportojnë vetëm përparimin në një formë të sigurt për threads (numërues, 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.
Pse kjo ndarje funksionon mirë: ngarkesa e punës mund të jetë me frekuencë të lartë (mijëra elemente), por UI kërkon vetëm disa përditësime në sekondë. Në praktikë mjaftojnë 5–10 përditësime/sekondë, për punë shumë të shpejta edhe 2–4. Gjithçka përtej është më së shumti zhurmë optike dhe konsumon kohë CPU në Message Pump.
Threadsicher zählen: Atomic statt Lock
Për një ProgressBar të thjeshtë shpesh mjafton një numërues atomik. „Atomar“ do të thotë: inkrementimi dhe leximi ndodhin pa Race Condition, tipikisht përmes TInterlocked. Kështu shmangni Locks (Critical Sections) në hot path të ciklit.
Ide minimale e provuar:
- Sasia totale dihet paraprakisht (p.sh. numri i rekordeve, skedarëve, ID-ve).
- Çdo iteracion rrit në mënyrë atomike DoneCounter.
- Një UI-Timer lexon periodikisht counter-in dhe vendos ProgressBar.Position.
Përparësi: Jo TThread.Queue për element, jo mbingarkesë e UI-së. Disavantazh: Nuk keni mesazhe detajesh për çdo element (p.sh. emri i skedarit). Për këtë mund të shtoni një mesazh statusi të dytë, të kufizuar (shih seksionin vijues).
Statusmeldungen ohne Spam: „letzter Status gewinnt“
Nëse dëshironi gjithashtu të shfaqni një tekst të shkurtër (elementi aktual, faza, mesazh gabimi), kërkohet një model që të mos përmbyt UI-në me çdo hap të Worker-it. Në praktikë „statusi i fundit fiton“ funksionon shumë mirë:
- Worker shkruan një informacion statusi në një strukturë threadsigurt (p.sh. string i shkëmbyeshëm në mënyrë atomike, ose i mbrojtur me një Lock të vogël).
- UI-Timer periodikisht merr statusin e fundit të parë dhe e vendos në një Label.
Kështu UI mbetet reaguese, dhe përsëri shihni që „diçka po ndodh“. Më e rëndësishme këtu është jo vetë string-u, por jetëgjatësia: mos bartni referenca ndaj objekteve me jetë të shkurtër nga thread-et e Worker-it në UI-thread. Nëse kaloni objekte, sqaroni pronësinë (ownership) në mënyrë eksplicite.
TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster
Ekzistojnë skenarë ku një UI-Timer vetëm nuk mjafton: p.sh. kur në fund dëshironi të siguroheni që të nxisni saktësisht një përditësim „Fertig“, ose kur përditësimi i UI-së është një hap më kompleks (p.sh. shënim në një dritare log, por i kufizuar). Atëherë TThread.Queue është i përshtatshëm – por jo për çdo iteracion, por i synuar.
Një qasje praktike është një Queue nur për Ereignisse mit geringer Frequenz:
- Start-Event (përgatit UI-në, bllokon butonat)
- Ngjarje periodike të progresit (maks. çdo X millisekonda)
- Ngjarje gabimesh (opsionale, të mbledhura)
- Done-Event (rikthen UI-në, shfaq rezultatet)
Periodikën nuk e arrini përmes UI-thread-it, por tashmë në kontekstin e Worker-it: lejoni Worker-in të queue-ojë një përditësim të UI-së vetëm kur ka kaluar mjaft kohë që nga përditësimi i fundit i UI-së. Për këtë përshtatet një burim kohe monoton (p.sh. TickCount) plus një vlerë atomike „last update“.
E rëndësishme: vetë përditësimi i UI-së duhet të jetë „i shpejtë“. Llogaritjet e shtrenjta, I/O skedari apo akseset në bazën e të dhënave nuk duhet të jenë në callback-in e gequeuar të UI-së. Callback-u duhet vetëm të lexojë gjendjet dhe të vendosë control-et.
Cancel-Handling: Abbrechen ohne Hänger
Në aplikacione reale anulimi nuk është opsional. Vendimtare është: Cancel nuk është një „Kill“, por një kooperatives përfundim. Worker-at duhet të kontrollojnë rregullisht nëse është vendosur një sinjal anulimi dhe pastaj të dalin pastër. Në Delphi ka disa mënyra për këtë (sipas PPL-Konstruktion): një flamur Volatile i veçantë, një Boolean atomik, ose një koncept Cancellation përmes Tasks (sipas versionit dhe strukturës së Delphi).
Për operimin janë të rëndësishme dy rregulla:
- Anulimi duhet të bëhet i dukshëm shpejt: Kontrolloni flamurin e ndërprerjes në vende të arsyeshme, jo vetëm në fund të një iteracioni, kur iteracioni mund të zgjasë edhe sekonda.
- Anulimi duhet të pastrrojë: Handle-t e hapura, skedarët e përkohshëm, transaksionet ose bllokimet nuk duhet të mbeten. Kjo do të thotë: në çdo iteracion të worker-it, blloqet try/finally janë të detyrueshme kur janë të përfshira burime.
Në nivelin e UI-së, anulimi duhet vetëm të vendosë një sinjal dhe të sjellë UI-në në një gjendje „Stopping…“. Përfundimi i vërtetë dhe riaktivizimi i UI-së ndodhin pastaj në Done-Event, jo menjëherë me klikimin.
Deadlocks vermeiden: die häufigsten Fallen in der Praxis
Kurthi 1: WaitFor/Task.Wait im Main Thread
Nëse Main Thread bllokohet, ai nuk mund të ekzekutojë Queue-callbacks dhe nuk mund të përpunojë mesazhe. Kjo duket si një Deadlock, edhe nëse worker-ët vazhdojnë të punojnë normalisht. Zgjidhja: asnjë wait bllokues në UI-Thread. Në vend të tyre, veprimi i përfundimit të bëhet përmes TThread.Queue ose një mekanizmi me evente (p.sh. një Timer që kontrollon nëse është përfunduar).
Kurthi 2: Synchronize innerhalb eines Locks
Një klasik: worker mban një Critical Section, thërret më pas Synchronize, dhe në callback-in e UI-së nevojitet (drejtpërdrejt ose indirekt) përsëri e njëjta Critical Section. Rezultati: pritje në rreth. Rregulla është e thjeshtë: asnjë dorëzim tek UI-ja (Synchronize/Queue) nga brenda një lock të mbajtur. Nëse një lock është i nevojshëm, merrni të gjitha të dhënat në variabla lokale, lëshoni lock-un dhe pastaj bëni queue.
Kurthi 3: UI-Callback triggert Reentrancy
Nganjëherë azhurnimi i UI-së vetë nuk është „i pafajshëm“: vendosja e Properties mund të shkaktojë Events (OnChange, OnResize), të cilat nga ana tjetër nxisin logjikë që qaset në gjendjet e worker-ëve. Kjo nuk është një Deadlock në kuptimin e ngushtë, por shkakton ngecje të vështira për tu sqaruar dhe Race Conditions. Zgjidhja: vendosni azhurnimet e UI-së në rrugë „të qeta“ (çaktivizoni event-et përkohësisht) ose përdorni Reentrancy-Guards (p.sh. një guard atomik për fazën e azhurnimit).
Kurthi 4: Zu viele queued Updates
Edhe pa wait-e bllokuese, UI-ja mund të „ndalet“ nëse krijoni dhjetëra mijëra callbacks të queued. Simptomat: ProgressBar vazhdon të lëvizë gjatë, dritarja përgjigjet ngadalë, dhe CPU-ja e Main Thread rritet. Zgjidhje: frenim (dritare kohe), agregim (counter), ose një strukturë reale Producer/Consumer ku vetëm një UI-update mund të jetë „pending“ (coalescing).
Wenn es komplizierter wird: Ergebnisse sammeln, Fehler bündeln, Reihenfolgen garantieren
TParallel.For është ideal kur iteracionet janë të pavarura. Në software biznesi iteracionet shpesh janë vetëm „në masë të madhe“ të pavarura: lexoni skedarë, thërrisni REST-APIs, shkruani rreshta në bazën e të dhënave. Atëherë duhet të planifikoni qartë tre pika shtesë:
- Mblidhje rezultatesh e sigurt për thread: Ose për çdo thread një buffer lokal (bashkoni në fund) ose një Queue/Collection threadsigurt. Shmangni lock-et në hot path.
- Fehlerbehandlung: Exceptions aus Worker-Threads müssen eingesammelt werden. In der Praxis bewährt sich: erste Exception merken und Cancel auslösen, oder alle Exceptions sammeln und am Ende zusammengefasst anzeigen.
- Reihenfolge: Wenn die Ausgabe eine stabile Reihenfolge braucht (z. B. Logs nach Index), ist parallele Verarbeitung mit späterem Sortieren oft einfacher als „thread-sicheres geordnetes Einfügen“.
Für die UI heißt das: Zeigen Sie nicht jede einzelne Fehlermeldung sofort. Das führt zu Modal-Dialog-Hölle. Sammeln Sie Fehler (z. B. Liste von Strings), und zeigen Sie am Ende eine Zusammenfassung oder ein exportierbares Log.
Debugging: Wie Sie den Deadlock wirklich sichtbar machen
Deadlocks in Parallel-Code sind frustrierend, weil sie im Debugger anders aussehen können als im Release. Trotzdem gibt es ein paar sehr praktische Hebel:
Threads-Fenster und Call Stacks nutzen
Wenn die UI hängt, schauen Sie sich alle Threads an: Wo steht der Main Thread? Wartet er? Ist er in einer Message-Loop? Wo stehen Worker-Threads? Wenn Worker in Synchronize hängen, ist die Ursache fast immer „Main Thread blockiert“ oder „Main Thread braucht einen Lock“.
Queue-/Synchronize-Stellen markieren
Setzen Sie gezielt Logging an Übergabestellen (vor dem Queue, im Queue-Callback, am Ende der Iteration). Im Betrieb ist das oft wertvoller als Breakpoints, weil Timing entscheidend ist. Achten Sie darauf, dass Logging selbst thread-sicher und nicht blockierend ist (z. B. keine UI-Log-Ausgabe direkt aus Worker-Threads).
Last-Update-Zeit messen
Wenn die UI „hängt“, kann es schlicht sein, dass sie zu viele Updates abarbeiten muss. Messen Sie daher im Main Thread, wie oft Sie pro Sekunde ein UI-Update ausführen und wie lange es dauert. Sobald UI-Callbacks mehr als wenige Millisekunden brauchen, ist Drosselung oder Vereinfachung nötig.
Wann lohnt sich TParallel.For mit Progress-UI wirklich?
Parallelisierung ist kein Selbstzweck. Sie lohnt sich besonders, wenn:
- die Iterationen groß genug sind (Millisekunden bis Sekunden), sodass Threadpool-Overhead untergeht,
- der Job CPU-lastig ist (Parsing, Kompression, Hashing) oder gut parallelisierbare I/O hat (mehrere Dateien, mehrere HTTP-Requests mit Limits),
- Sie eine klare Cancel- und Fehlerstrategie haben,
- die UI-Anforderungen mit aggregiertem Progress auskommen.
Sie lohnt sich weniger, wenn jede Iteration extrem kurz ist (Mikro-Operationen) oder wenn alle Iterationen auf denselben Engpass laufen (eine serielle DB-Transaktion, ein globaler Lock, eine einzelne Datei). Dann ist der schnellere Hebel oft: Algorithmus verbessern, Batchen, Datenzugriff reduzieren oder den Engpass explizit entkoppeln.
Praxis-Checkliste: So bleibt die UI stabil
- Main Thread blockiert nicht: keine Waits, keine langen Loops ohne Message Pump.
- Worker fassen nie Controls an: keine VCL/FMX-Zugriffe außerhalb des UI-Threads.
- UI-Updates sind gedrosselt: Counter/Timer oder coalesced Queue-Updates statt pro Iteration.
- Keine Synchronize-Aufrufe aus Locks heraus.
- Cancel ist kooperativ, häufig geprüft und räumt sauber auf.
- Exceptions werden gesammelt und am Ende geordnet behandelt.
Fazit: Mit Queue entkoppeln, mit Aggregation stabilisieren
Një UI e qëndrueshme për progresin me TParallel.For nuk krijohet duke „vendosur Synchronize kudo“, por përmes një parimi të qartë arkitekturor: worker-ët punojnë në mënyrë të pavarur, Main Thread mbetet i lirë dhe përpunon vetëm disa përditësime të shpejta të UI-së. TThread.Queue është vegla e duhur kur e përdorni të synuar dhe të rregulluar. Për sjellje „të ngadalta“ ose të paqëndrueshme përgjegjësit janë gati gjithmonë dy: Main Thread pret diku në mënyrë bllokuese – ose mbytet nga shumë përditësime të radhitura.
Pasi të keni ngritur shablonin në mënyrë të pastër (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), ia vlen ta aplikoni atë në shumë vende të një aplikacioni Delphi të zhvilluar: Import/Export, kontrolle të të dhënave, punë me skedarë dhe punë API – gjithçka bëhet më reaguese, pa rrezikun që me çdo Progress-Update të shkaktoni deadlock-e të reja.
Nëse kërkoni ndihmë për stabilizimin e kodit paralel, për debugim të ngrirjeve të UI-së ose për modernizim të rregullt të aplikacioneve Delphi të zhvilluara: kontaktoni.
Për këtë temë janë të rëndësishme edhe Delphi Parallel Programming Library dhe Tthread.queue Vs Synchronize. Artikulli rendit këto aspekte në mënyrë të kuptueshme dhe tregon çfarë ka rëndësi në praktikë.
Diskutoni projektin ose iniciativën e modernizimit me Net-Base.
Hapi tjetër
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Ne nuk mbështesim vetëm në çështje të veçanta, por edhe kur nga fragmente të kodit burimor, temat legacy ose idetë për portale duhet të zhvillohen në një projekt korporativ të qëndrueshëm.
- Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Ju e shihni herët se cila rrugë është e qëndrueshme ekonomikisht dhe operativisht.