Net-Base Магазин

24.07.2026

Delphi: TParallel.For са thread-safe Progress UI-јем (TThread.Queue) без deadlock-ова

Како да комбинујете Delphi TParallel.For са UI-јем за приказ напретка који је безбедан за нити: ажурирања преко TThread.Queue, чиста агрегација, руковање отказивањем и типичне замке deadlock-а при дебаговању и у раду.

24.07.2026

Од теме часописа до пројектне праксе

Одговарајуће странице услуга и техничке странице за чланак

Ko god u Delphi želi da paralelizuje računarski zahtevne ili I/O-intenzivne poslove, brzo će doći do Parallel Programming Library (PPL) i konkretno do TParallel.For. Efekat je često odmah merljiv – dok ne pokušate u istoj sekundi „samo brzo“ da osvežite Progress-UI. Upravo tu nastaju tipični zastoji: naizgled nasumično zamrzavanje UI-ja, ProgressBar koji skače unazad ili potpuni deadlock čim u debuggeru korak po korak izvršavate kod.

U ovom članku radi se o TParallel.For sigurna za niti Progress-UI: robusnom obrascu koji funkcioniše u VCL i FMX, koji uredno grupiše osveženja UI-ja preko TThread.Queue, uzima u obzir Cancel/Abort i dosledno izbegava najčešće zamke deadlock-a. Fokus je na operativnoj realnosti: reproducibilno ponašanje, razumljive odgovornosti i saveti za debug koji pomažu čak i kada se greška pojavljuje samo „kod kupaca“.

Zašto Progress-UI kod TParallel.For toliko često pođe naopako

TParallel.For tipično radi na worker-nitima iz Delphi-threadpool-a. Te niti ne smeju direktno dirati VCL- ili FMX-kontrole, jer su UI-frameworks (message loop, window handles, rendering) vezani za glavnu nit. Čak i naizgled bezopasan ProgressBar.Position := … iz workera može dovesti do nedefinisanog ponašanja: povremeni AV-ovi, zaleđena prozora ili „treperava“ ažuriranja.

Logičan popravak je često TThread.Synchronize. To i jeste thread-safe, ali u paralelnim petljama brzo uvodi drugi problem: pravite serijski usko grlo. Svaki worker čeka na glavnu nit, koja je zauzeta renderovanjem i obradom Synchronize-poziva. Pod opterećenjem to izgleda kao deadlock — iako je u suštini efekt starvation/lockstep.

A onda postoji i klasična kategorija deadlock-a: glavna nit čeka (npr. preko WaitFor, Task.Wait ili indirektno kroz blokirajuće pozive) na kraj paralelne operacije, dok worker-niti pokušavaju putem Synchronize ili nepažljive upotrebe Queue-a da pošalju rad glavnoj niti. Ishod: glavna nit čeka workere, workeri čekaju glavnu nit.

TThread.Queue vs. TThread.Synchronize: der praktische Unterschied

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Pri debugovanju brzo se vidi da li worker-niti čekaju glavnu nit ili samo postavljaju queued osveženja.

Oba mehanizma služe da se kod sigurno izvrši na glavnoj niti. Razlika je u semantici čekanja:

  • TThread.Synchronize: Pozivajući worker čeka dok glavna nit ne izvrši kod. To je „sinhrono“, povećava latencu i predstavlja klasičan sastojak za deadlock-e kada je glavna nit trenutno blokirana.
  • TThread.Queue: Worker ставља код само у ред за Main Thread и наставља да ради. То је „асинхроно“, раздваја нити и у паралелним сценаријима је готово увек бољи подразумевани избор — под условом да контролишете фреквенцију ажурирања.
  • Важно: Queue није дозвола за неограничено слање. Ако у свакој итерацији петље пошаљете једно Queue-ажурирање, преоптеретићете Main-Thread-Queue. UI се тада можда неће замрзнути због Deadlock-а, али хоће због саме количине порука. UI делује „тромо“, а завршетак обраде се одлаже јер још стотине или хиљаде UI-ажурирања остају да се изведу.

    Der Randfall, der wirklich wehtut: Warten im UI-Thread

    У пословним апликацијама често се види следећи ток: клик на дугме „Start“, онемогућавање UI-а, отварање ProgressDialog-а, па се потом синхронo „чека“ док све не буде готово да би се након тога поново омогућило. Овај образац је језгро многих Deadlock-ова.

    Типичне варијанте (у зависности од базе кода):

    • Main Thread покреће TParallel.For и након тога позива блокирајућу логику чекања (директно или индиректно).
    • ProgressDialog у Constructor-у или OnShow позове рутину која унутар себе чека.
    • Дугме за отказивање постави флаг, али Main Thread ипак остане заглављен у петљи чекања.

    Ako Worker-нитe у том периоду користе Synchronize, Deadlock је практично загарантован. Са Queue такође може доћи до зависавања ако Main Thread блокира и не пумпа поруке — јер се у том случају и Queue не обрађује.

    Покретно-практична последица је: Main Thread не сме блокирајуће чекати на паралелну петљу ако су паралелно потребна UI-ажурирања. Уместо тога, обрада мора или у потпуности бити премештена у позадински task, или организовати „асинхрони крај“ (Callback/Queued Abschlussaktion) који на крају поново ослобађа UI.

    Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt

    Arbeitsplatzmotiv mit abstrahierter Progress-Anzeige und Zeitmessung als Symbol für gedrosselte UI-Updates
    Агрегација и временско ограничавање спречавају да UI буде преоптерећен превише ажурирања.

    Робустан образац састоји се из три јасно одвојене одговорности:

    • Worker-Threads обављају фактички посао по елементу/индиксу. Они пријављују само напредак у нитно-безбедном облику (бројач, Queue, thread-safe Queue).
    • Aggregator (често: Main Thread или посвећени таймер у UI-у) израчунава из напредка UI-статус (позиција, текст, ETA) и ажурира контроле. Тако избегавате 1:1-ажурирања по итерацији.
    • Abschluss (такође у Main Thread): реактивирање UI-а, приказ резултата, сабирање грешака, ослобађање ресурса.

    Зашто ова подела тако добро функционише: Workload може бити високофреквентан (хиљаде елемената), али UI треба само неколико ажурирања у секунди. У пракси је довољно 5–10 ажурирања/с, код врло брзих послова чак 2–4. Све изнад тога је најчешће само визуелна бука и троши CPU-време у Message Pump.

    Threadsicher zählen: Atomic statt Lock

    За једноставну ProgressBar често је довољан атомарни бројач. „Атомарно“ значи: повећање и читање се извршавају без race condition, типично преко TInterlocked. Тако избегавате Lock-ове (Critical Sections) у hot path-у петље.

    Проверена минимална идеја:

    • Укупна количина је унапред позната (нпр. број записа, датотека, ID-јева).
    • Свака итерација атомарно повећава DoneCounter.
    • UI-Timer периодично чита бројач и подешава ProgressBar.Position.

    Предност: ниједан TThread.Queue по елементу, нема преплављивања UI-ја. Недостатак: немате детаљне поруке по елементу (нпр. име датотеке). За то се може додати други, ограничени статус (видети наредни одељак).

    Statusmeldungen ohne Spam: „letzter Status gewinnt“

    Ако желите да прикажете кратак текст (тренутни елемент, фаза, порука о грешци), такође треба образац који не поплављује UI при сваком кораку worker-а. У пракси добро функционише „последњи статус побеђује“:

    • Worker уписује статусну информацију у threadsafe структуру (нпр. атомарно заменљив String, или заштићено малим Lock-ом).
    • UI-Timer периодично преузима последњи видљиви статус у један Label.

    Тако UI остаје реактиван, а ипак видите да „нешто се дешава“. Важно је овде мање сам String него његов животни век: не преносите референце на краткотрајне објекте из worker-нитова у UI-нит. Ако преносите објекте, јасно разјасните власништво.

    TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster

    Постоје сценарији у којима сам UI-Timer није довољан: нпр. када на крају желите да сигурно покренете тачно једно „Fertig“-ажурирање, или када је UI-ажурирање сложенији корак (нпр. запис у лог-прозор, али ограничен). Тада је TThread.Queue прикладан — али не по итерацији, већ селективно.

    Практичан приступ је ред само за догађаје ниске фреквенције:

    • Start-Event (припрема UI, онемогућавање дугмића)
    • Периодична Progress-Events (највише сваких X милисекунди)
    • Грешка-ивенти (опционо сакупљани)
    • Done-Event (враћање UI-ја у почетно стање, приказ резултата)

    Периодику не постижете преко UI-нит-а, већ већ у контексту worker-а: дозволите worker-има да само онда queue-ују UI-ажурирање када је од последњег UI-ажурирања прошло довољно времена. За то одговара монотони извор времена (нпр. TickCount) плус атомарна вредност „last update“.

    Важно: само UI-ажурирање мора бити „брзо“. Скупе рачунске операције, датотечни I/O или приступи бази података не припадају у UI-callback стављен у ред. Callback треба само да чита стања и подешава контроле.

    Cancel-Handling: Abbrechen ohne Hänger

    У реалним апликацијама отказивање није опционо. Кључно је: Cancel није „убиј“, већ кооперативно завршавање. Worker-и морају редовно проверавати да ли је сигнал за прекид постављен и онда чисто изаћи. У Delphi за то постоји неколико приступа (у зависности од PPL-конструкције): сопствени Volatile-флаг, атомарни Boolean, или концепт Cancellation преко Tasks (у зависности од верзије и структуре Delphi).

    Za rad su važna dva pravila:

    • Dugme „Cancel“ mora biti brzo vidljivo: Proveravajte flag za prekid na smislenim mestima, ne samo na kraju iteracije, posebno ako iteracija može potrajati sekunde.
    • Cancel mora očistiti: Otvoreni handle-ovi, privremene datoteke, transakcije ili lockovi ne smeju ostati. To znači: u svakoj iteraciji workera su try/finally-blokovi obavezni ako su uključeni resursi.

    Sa UI strane bi dugme „Cancel“ trebalo samo da postavi signal i dovede UI u stanje „Stopping…“. Stvarno završavanje i reaktivacija UI-a zatim se odvijaju u Done-događaju, a ne odmah na klik.

    Izbegavanje deadlock-ova: najčešće zamke u praksi

    Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
    Deadlock-ovi često nastaju zbog cikličnog čekanja: UI-Thread blokiran, Worker čekaju pristup UI-u.

    Zamka 1: WaitFor/Task.Wait u glavnoj niti

    Ako je glavna nit blokirana, ne može izvršavati Queue-callbacks i ne može obrađivati poruke. To deluje kao deadlock, čak i ako worker-i nastave da rade. Rešenje: ne koristiti blokirajuće Wait-e u UI niti. Umesto toga završnu akciju obavite preko TThread.Queue ili upravljanjem događajima (npr. timer proverava „završeno“).

    Zamka 2: Synchronize unutar lock-a

    Klasik: Worker drži Critical Section, zatim poziva Synchronize, a u UI-callbacku se (direktno ili indirektno) ponovo zahteva ista Critical Section. Rezultat: ciklično čekanje. Pravilo je jednostavno: Bez predavanja UI-ja (Synchronize/Queue) iz držanog lock-a. Ako je lock neophodan, prvo preuzmite sve podatke u lokalne promenljive, napustite lock, pa tek onda stavite u queue.

    Zamka 3: UI-callback pokreće reentrancy

    Ponekad samo ažuriranje UI-ja nije „bezazleno“: postavljanje Properties može pokrenuti događaje (OnChange, OnResize) koji potom aktiviraju logiku koja pristupa stanjima workera. To nije deadlock u strožem smislu, ali dovodi do teško objašnjivih zastoja i race condition-a. Rešenje: smestite UI-ažuriranja u „tihi“ tok (privremeno onemogućite događaje) ili koristite reentrancy-guardove (npr. atomski guard za fazu ažuriranja).

    Zamka 4: Previše queued ažuriranja

    Čak i bez Wait-ova UI može „stati“ ako generišete desetine hiljada queued callback-ova. Simptomi: ProgressBar dugo „trči“ za stanjem, prozor reaguje tromo, CPU glavne niti visoko opterećen. Rešenje: dovedite u red (vremenski prozor), agregirajte (brojač), ili primenite pravu Producer/Consumer strukturu u kojoj samo jedno UI-ažuriranje može biti pending (coalescing).

    Kada postane komplikovanije: prikupljanje rezultata, grupisanje grešaka, garantovanje redosleda

    TParallel.For je idealan kada su iteracije nezavisne. U poslovnom softveru su iteracije često samo „uglavnom“ nezavisne: čitaju datoteke, pozivaju REST-API-je, upisuju redove u bazu podataka. U tom slučaju morate jasno isplanirati još tri tačke:

    • Prikupljanje rezultata bezbedno za niti: Ili po niti lokalni bafer (na kraju spojiti) ili thread-safe Queue/Collection. Izbegavajte lock-ove na hot-path-u.
    • Руковање грешкама: изузеци из радних нити морају се сакупљати. У пракси се показало: запамтити први изузетак и покренути Cancel, или сакупити све изузетке и на крају их приказати сабрано.
    • Редослед: ако излаз треба стабилан редослед (нпр. логови по индексу), паралелна обрада са каснијим сортирањем је често једноставнија од „уређеног уметања које је безбедно за нити“.

    За UI то значи: немојте приказивати сваку појединачну поруку о грешци одмах. То води у пакао модалних дијалога. Сакупљајте грешке (нпр. листа стрингова) и на крају прикажите резиме или лог који се може извозити.

    Debugging: Wie Sie den Deadlock wirklich sichtbar machen

    Deadlock-ови у паралелном коду су фрустрирајући, јер у дебагеру могу изгледати другачије него у релизу. Ипак, постоји неколико врло практичних приступа:

    Користите прозор нити и стек позива

    Ако се UI заглави, погледајте све нити: где се налази главна нит? Чека ли она? Да ли је у петљи порука? Где су радне нити? Ако су радне нити заглављене у Synchronize, узрок је готово увек „главна нит блокирана“ или „главна нит тражи закључавање“.

    Означите Queue-/Synchronize-места

    Поставите циљано логовање на местима предаје (пре Queue, у Queue-callback, на крају итерације). У раду је то често корисније од breakpoints, јер је временска координација пресудна. Обратите пажњу да само логовање буде thread-safe и не блокира (нпр. нема UI-лог из директно из радних нити).

    Измерите време последњег ажурирања

    Ако се UI „заглави“, може бити да мора обрадити прегршт ажурирања. Стога измерите у главној нити колико често по секунди извршавате UI-ажурирање и колико то траје. Чим UI-колбецкови трају више од неколико милисекунди, потребно је дроселовање или поједностављење.

    Када се TParallel.For са Progress-UI заиста исплати?

    Паралелизација није самоциљ. Исплати се посебно када:

    • итерације су довољно велике (милисекунде до секунди), тако да трошак Threadpool-а постане занемарљив,
    • посао је CPU-оптерећен (парсирање, компресија, хеширање) или има I/O који се добро паралелизује (више фајлова, више HTTP-захтева са ограничењима),
    • имате јасну стратегију Cancel и руковања грешкама,
    • UI захтеви могу да се задовоље агрегираним прогресом.

    Мање се исплати када је свака итерација изузетно кратка (микро-операције) или када све итерације погађају исто уско грло (серилна DB-транзакција, глобално закључавање, један фајл). Тада је често брже: побољшати алгоритам, пакетирати у батчеве, смањити приступ подацима или експлицитно раздвојити уско грло.

    Практична чек-листа: Како одржати UI стабилним

    • Главна нит није блокирана: без дугих чекања, без дугих петљи без обраде порука.
    • Радне нити никада не приступају контролама: нема VCL/FMX приступа изван UI-ните.
    • UI-ажурирања су ограничена: Counter/Timer или спојена Queue-ажурирања уместо по итерацији.
    • Ни позиви Synchronize изнутра закључавања.
    • Cancel је кооперативан, често провераван и врши правилно чишћење.
    • Изузеци се сакупљају и на крају организовано обрађују.

    Закључак: Раздвојити помоћу Queue-а, стабилизовати агрегирањем

    Robusna Progress-UI kod TParallel.For ne nastaje ubacivanjem „irgendwo Synchronize rein“, već jasnim arhitektonskim principom: radnici rade nezavisno, glavna nit ostaje slobodna i obrađuje samo nekoliko brzih UI-azuriranja. TThread.Queue je pri tom pravo sredstvo ako ga upotrebljavate ciljano i ograničeno. Za „sporo“ ili nestabilno ponašanje gotovo uvek su odgovorna dva uzroka: glavna nit negde blokirajuće čeka – ili se ugušava u prevelikom broju ažuriranja u redu (queued updates).

    Ako jednom pravilno postavite obrazac (Counter/Coalescing, Cancel-Flag, završni callback), isplati se primeniti ga na mnogim mestima u postojeće Delphi-aplikacije: Import/Export, provere podataka, poslovi sa fajlovima i API poslovi – sve postaje reaktivnije, bez toga da pri svakom ažuriranju napretka prouzrokujete nove deadlock-e.

    Ako vam treba podrška pri stabilizaciji paralelnog koda, pri debugovanju zamrzavanja korisničkog interfejsa ili pri urednom modernizovanju postojećih Delphi-aplikacija: Kontaktirajte nas.

    Za ovu temu su takođe važne Delphi Parallel Programming Library i Tthread.queue Vs Synchronize. Ovaj članak jasno razjašnjava te aspekte i pokazuje na šta se u praksi treba fokusirati.

    Razgovarajte o projektu ili modernizacionom poduhvatu sa Net-Base.

    Следећи корак

    Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

    Подржавамо не само у појединачним питањима, већ и када из исечака изворног кода, застарелих тема или идеја за портале треба да настане поуздан корпоративни пројекат.

    • Постојеће стање, циљано стање и технички ризици оцењују се заједно.
    • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
    • Ви рано увидите који пут је економски и оперативно одржив.

    Подели објаву

    Поделите ову објаву директно

    LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. für Instagram bereiten wir Link und Kurztext direkt vor.

    Е-пошта

    Инстаграм се отвара у новој картици. Линк и кратак текст се претходно копирају у међуспремник.