Од тема во магазинот до проектна пракса
Соодветни страници за услуги и технички информации поврзани со објавата
Кој во Delphi сака да паралелизира пресметувачки интензивни или I/O-интензивни задачи, брзо ќе заврши кај Parallel Programming Library (PPL) и конкретно кај TParallel.For. Ефектот често е веднаш мерлив – сè додека во ист момент не посакате „брзо“ да ја ажурирате Progress-UI. Тука се појавуваат типичните застои: наизглед случајни закочувања на UI, ProgressBar што скока наназад, или комплетен Deadlock кога дебагирате чекор-по-чекор.
Овој текст го опфаќа TParallel.For threadsichere Progress-UI: робустен образец кој функционира во VCL и FMX, чисто агрегира UI-ажурирања преку TThread.Queue, ги зема предвид Cancel/Abort и доследно ги избегнува најчестите Deadlock-замки. Фокусот е на оперативната реалност: репродуцибилно однесување, јасни одговорности и совети за дебагирање што помагаат дури и кога грешката се јавува само „кај клиентите“.
Зошто Progress-UI кај TParallel.For толку често не функционира правилно
TParallel.For типично се извршува на Worker-нишки од Delphi-threadpool. Овие нишки не смеат директно да пристапуваат до VCL или FMX контролите, затоа што UI-фрејмворците (Message Loop, Window Handles, Rendering) се поврзани со Main Thread. Дури и наизглед безвреден ProgressBar.Position := … од Worker може да предизвика недефинирано однесување: спорадични AVs, замрзнати прозорци или „трепкачи“ ажурирања.
Очигледната поправка често е TThread.Synchronize. Тоа иако ја решава thread-safety, во паралелни циклуси брзо води до друг проблем: создава сериозен серијален теснец. Секој Worker чека на UI-нишката, која пак е зафатена со рендерирање и со обработка на Synchronize-повиците. Под оптоварување тоа изгледа како Deadlock – иако „само“ е ефект на starvation/lockstep.
Исто така постои и вистинската класа на Deadlock: Main Thread чека (на пр. преку WaitFor, Task.Wait или индиректно преку блокирачки повици) на завршетокот на паралелната операција, додека Worker-нишките се обидуваат да испратат работа во Main Thread преку Synchronize или преку нерационална употреба на Queue. Резултат: Main Thread чека на Worker, Worker чекаат на Main Thread.
TThread.Queue vs. TThread.Synchronize: der praktische Unterschied
Двата механизми служат за безбедно извршување на код во Main Thread. Разликата е во семантиката на чекање:
- TThread.Synchronize: повикувачкиот Worker чека додека Main Thread го изврши кодот. Тоа е „синхроно“, ги зголемува латенциите и е класичен фактор за Deadlocks кога Main Thread е блокиран.
- TThread.Queue: Работникот ја става само кодата во редица за главната нишка и продолжува да работи. Тоа е „асинхроно“, ги декоплира нишките и во паралелни сценарија речиси секогаш е подобра подразбрана опција – под услов да се контролира фреквенцијата на ажурирања.
Важно: Queue не е дозвола за неограничено користење. Ако во секоја итерација на циклусот испраќате Queue-ажурирање, ќе ја преплавите редицата на главната нишка. Тогаш UI-то не заглавува поради deadlock, туку поради огромна количина пораки. UI-то делува „споро“, а завршетокот на обработката се одложува бидејќи уште стотици или илјадници UI-ажурирања остануваат да се извршат.
Краен случај што навистина боли: чекање во UI-нишката
Во корпоративните апликации често се среќава следниот тек: се кликнува копчето „Start“, UI-то се деактивира, ProgressDialog-от се отвора, потоа синхроно се „чека“ додека сè не заврши, за потоа повторно да се овозможи. Овој образец е сржта на многу deadlock-ови.
Типични варијанти (во зависност од кодната база):
- Главната нишка стартува TParallel.For и потоа повикува блокирачка логика за чекање (директно или индиректно).
- Еден ProgressDialog во Constructor или OnShow повикува рутина што внатрешно чека.
- Копчето за откажување може да постави флаг, но главната нишка сепак останува заглавена во петља за чекање.
Ако Worker-нишките во тоа време користат Synchronize, deadlock-от е практично загарантиран. Со Queue исто така може да заглави ако главната нишка е блокирана и не пумпа пораки – тогаш и Queue нема да се обработи.
Практичната последица е: главната нишка не смее блокирачки да чека на паралелната петља ако истовремено се потребни UI-ажурирања. Наместо тоа, обработката мора или целосно да се пресели во позадинска задача, или да се организира „асинхрон крај“ (Callback/Queued завршна акција) што на крајот ја ослободува UI-то.
Чист пристап: Прогрес само агрегирано, UI-ажурирања регулирани
Еден робустен модел се состои од три јасно одделени одговорности:
- Worker-Threads ја извршуваат вистинската работа по елемент/индекс. Тие пријавуваат само напредок во thread-сигурна форма (бројач, Queue, thread-safe Queue).
- Aggregator (често: главната нишка или посветен тајмер во UI) пресметува од напредокот статус за UI (позиција, текст, ETA) и ги ажурира контролите. Така избегнувате 1:1-ажурирања по итерација.
- Завршување (исто така во главната нишка): реактивирање на UI-то, прикажување на резултатот, сумирање на грешки, ослободување на ресурси.
Зошто оваа разделба толку добро функционира: Работното оптоварување може да биде високофреквентно (илјадници елементи), но UI-то треба само неколку ажурирања во секунда. На практика, 5–10 ажурирања/секунда се доволни, кај многу брзи работни задачи дури и 2–4. Сè над тоа обично е само оптички шум и чини CPU‑време во Message Pump.
Тред‑безбедно броење: атомарно наместо Lock
За едноставна ProgressBar често е доволен атомарен броач. „Атомарно“ значи: инкремент и читање се случуваат без Race Condition, типично преку TInterlocked. Со тоа избегнувате Locks (Critical Sections) во hot path‑от на циклусот.
Проверено минимално решение:
- Вкупната количина е позната однапред (на пр. број записи, датотеки, ID‑та).
- Секоја итерација атомарно го зголемува DoneCounter.
- UI‑Timer‑от периодично го чита бројачот и ја поставува ProgressBar.Position.
Предност: нема TThread.Queue по елемент, нема преплавување на UI‑то. Недостаток: немате детални пораки по елемент (на пр. име на датотека). За тоа можете да додадете втора, ограничена статус‑порака (видете следниот дел).
Статус‑пораки без спам: „последниот статус победува“
Ако дополнително сакате да прикажете краток текст (тековен елемент, фаза, порака за грешка), исто така ви треба образец кој нема да ја преплави UI‑то при секој Worker‑чекор. На практика „последниот статус победува“ функционира многу добро:
- Worker запишува информација за статусот во тред‑безбедна структура (на пр. атомарно заменлив String, или заштитено со мал Lock).
- UI‑Timer‑от периодично го презема последно видениот статус и го поставува во label.
Со тоа UI‑то останува реактивно и јасно е дека нешто се случува. Поголемо значење тука има не толку самиот стринг, колку неговиот век на траење: немојте да пренесувате референци на краткотрајни објекти од Worker‑нишки во UI‑нишката. Ако пренесувате објекти, јасно дефинирајте ownership.
TParallel.For тред‑безбедно Progress‑UI со TThread.Queue: еден робустен образец
Постојат сценарија каде UI‑Timer‑от сам по себе не е доволен: на пр. кога на крајот сакате сигурно да емитувате точно едно „Fertig“‑ажурирање, или кога UI‑ажурирањето е попретен чекор (на пр. внес во лог‑прозорец, но ограничиравано). Тогаш TThread.Queue е соодветен – но не по итерација, туку селективно.
Практичен пристап е една редица само за настани со ниска фреквенција:
- Start‑настан (подготовка на UI, заклучување на копчиња)
- Периодични Progress‑настани (макс. секој X милисекунди)
- Грешка‑настани (опционално собирани)
- Done‑настан (враќање на UI, прикажување на резултатот)
Периодиката не ја реализирате преку UI‑нишката, туку во контекстот на Worker‑от: дозволувате Worker‑от да queue‑нува UI‑ажурирање само ако од последното UI‑ажурирање поминало доволно време. За тоа одговара монотонски временски извор (на пр. TickCount) плус атомарна вредност „last update“.
Важно: самото UI‑ажурирање мора да биде „брзо“. Скапи пресметки, датотечни I/O или пристапи до база на податоци не припаѓаат во gequeu‑аниот UI‑callback. Callback‑от треба само да чита состојби и да поставува контроли.
Cancel‑Handling: Прекинување без закочување
Во реални апликации откажувањето не е опција. Клучно е: Cancel не е „Kill“, туку едно кооперативно завршување. Worker‑ите мораат редовно да проверуваат дали е поставен сигнал за откажување и тогаш чисто да излезат. Во Delphi за тоа постојат неколку начини (во зависност од PPL‑конструкцијата): сопствен Volatile‑флаг, атомарен Boolean, или концепт за Cancellation преку Tasks (во зависност од Delphi‑верзијата и структурата).
За оперативната работа важат две правила:
- Cancel треба да стане видлив брзо: Проверувајте го флагот за прекин во соодветни точки, не само на крајот од една итерација, ако итерацијата може да трае и неколку секунди.
- Cancel треба да чисти по себе: Отворени Handles, привремени датотеки, трансакции или Locks не смее да останат. Тоа значи: во секоја Worker-итерација се задолжителни try/finally-блоковите кога учествуваат ресурси.
Од страна на UI, Cancel треба само да постави сигнал и да ја стави UI во состојба „Stopping…“. Самото завршување и повторното активирање на UI се случуваат во Done-Event, не веднаш при кликањето.
Избегнување на deadlock-ови: најчестите замки во практиката
Замка 1: WaitFor/Task.Wait во главната нишка
Ако главната нишка е блокирана, не може да изврши Queue-callbacks и не може да процесира пораки. Тоа делува како deadlock, иако Worker-ите правилно продолжуваат. Решение: нема блокирачки Waits во UI-нишката. Наместо тоа завршната акција преку TThread.Queue или управување со настани (на пр. Timer проверува „готово“).
Замка 2: Synchronize внатре во Lock
Класика: Worker ја држи критичната секција, потоа повикува Synchronize, а во UI-callback-от (директно или индиректно) повторно се бара иста критична секција. Резултат: кружно чекање. Правилото е едноставно: Никаква предавање на UI (Synchronize/Queue) од внатре во држан Lock. Ако Lock е неопходен, земете ги сите податоци во локални променливи, излезете од Lock-от, па потоа направете queue.
Замка 3: UI-Callback предизвикува реентрантност
Понекогаш самиот UI-апдејт не е „безазлен“: поставување на Properties може да предизвика настани (OnChange, OnResize), кои пак ќе активираат логика што пристапува до состојбите на Worker-ите. Ова не е deadlock во тесен смисол, но води до тешко објасниви заглавувања и race conditions. Решение: ставете UI-ажурирања во „тихи“ патеки (привремено оневозможете ги настаните) или користете реентрантни гарди (на пр. атомарен Guard за фазата на ажурирање).
Замка 4: Преголем број queued ажурирања
Дури и без Waits, UI може да „застане“ ако генерирате десетици илјади queued callbacks. Симптоми: ProgressBar долго продолжува да се ажурира, прозорецот реагира тромо, CPU во главната нишка високо оптоварен. Решение: намалување (временски прозорец), агрегирање (бројач), или вистинска Producer/Consumer-структура при која само едно UI-ажурирање може да биде „pending“ (Coalescing).
Кога е потешко: собирање резултати, групирање грешки, гарантирање на редослед
TParallel.For е идеален кога итерациите се независни. Во бизнис-софверот, сепак, итерациите често се само „делумно“ независни: читаат датотеки, повикуваат REST-APIs, запишуваат редови во базата на податоци. Тогаш треба јасно да планирате три дополнителни точки:
- Нитко-безбедно собирање на резултати: Или по нишка локален бафер (на крајот споите ги) или нитко-безбедна Queue/Collection. Избегнувајте Locks во Hot Path.
- Ракување со грешки: Исклучоците од worker-нишките мора да се собираат. Во пракса се покажува: запаметете ја првата исклучок и повикајте Cancel, или соберете ги сите исклучоци и на крај прикажете ги заедно.
- Редослед: Ако излезот бара стабилен редослед (на пр. логови по индекс), паралелната обработка со подоцна сортирање често е полесна отколку „нитно-безбедно уредно вметнување“.
За UI тоа значи: Не прикажувајте секоја поединечна порака за грешка веднаш. Тоа води кон пекол на модални дијалози. Собирајте грешки (на пр. листа од низи) и на крај прикажете резиме или лог кој може да се извезе.
Дебагирање: Како навистина да го направите deadlock-от видлив
Deadlock-ите во паралелен код се фрустрирачки, бидејќи во дебагерот може да изгледаат поинаку отколку во релиз. Сепак, постојат неколку практични мерки:
Користете го прозорецот Threads и Call Stacks
Ако UI запецнува, прегледајте ги сите нишки: каде стои Main Thread? Дали чека? Дали е во Message-Loop? Каде се Worker-нишките? Ако Worker-ите се заглавени во Synchronize, причината скоро секогаш е „Main Thread блокиран“ или „Main Thread треба Lock“.
Означете ги Queue-/Synchronize-местата
Поставете целно логирање на точките на предавање (пред Queue, во Queue-callback, на крајот од итерацијата). Во работа тоа често е повредно од breakpoints, бидејќи времескиот распоред е пресуден. Внимавајте логирањето само по себе да биде нитно-безбедно и неблокирачко (на пр. да нема UI-лог-излез директно од worker-нишки).
Мерење на времето на последно ажурирање
Ако UI „запецнува“, можно е едноставно да има премногу ажурирања за обработка. Затоа во Main Thread мерете колку пати во секунда извршувате UI-ажурирање и колку трае тоа. Штом UI-callbacks бараат повеќе од неколку милисекунди, е потребно ограничување или поедноставување.
Кога навистина вреди TParallel.For со Progress-UI?
Паралелизацијата не е самоцел. Се исплати особено кога:
- итерациите се доволно големи (милисекунди до секунди), така што Threadpool-Overhead се поништва,
- работата е CPU-интензивна (парсирање, компресија, хеширање) или има I/O што лесно се паралелизира (повеќе датотеки, повеќе HTTP-запити со ограничувања),
- имате јасна стратегија за Cancel и ракување со грешки,
- потребите на UI можат да се задоволат со агрегирано прикажување на напредокот.
Не се исплати толку кога секоја итерација е екстремно кратка (микро-операции) или кога сите итерации се ограничени од истото вратно грло (сериска DB-трансакција, глобален Lock, единечна датотека). Тогаш побрз чекор често е: подобрување на алгоритамот, батчирање, намалување на пристапот до податоци или експлицитно декуплирање на вратното грло.
Практичен чек-лист: Како да остане UI стабилен
- Main Thread не е блокиран: нема чекања (Waits), нема долги циклуси без Message Pump.
- Worker-ите никогаш не пристапуваат кон Controls: нема VCL/FMX-пристапи надвор од UI-нишката.
- UI-ажурирањата се ограничени: Counter/Timer или споени Queue-ажурирања наместо по итерација.
- Нема повици на Synchronize додека е задржан Lock.
- Cancel е кооперативен, често се проверува и чисто ослободува ресурси.
- Исклучоците се собираат и на крајот се обработуваат во уреден ред.
Заклучок: Декуплирајте со Queue, стабилизирајте со агрегација
Робусен Progress-UI при TParallel.For не се постигнува со „каде‑каде да се вметне Synchronize“, туку со јасно архитектонско правило: worker‑ите работат независно, Main Thread останува слободен и обработува само неколку, брзи UI‑ажурирања. TThread.Queue е соодветен алат кога се користи намерно и со дроселирање. За „тврдо“ или нестабилно однесување речиси секогаш постојат две причини: Main Thread‑от некаде чека блокирачки – или се дави во премногу queued Updates.
Откако ќе го воспоставите моделот правилно (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), вреди тоа да се применува на многу места во развиена Delphi-апликација: Import/Export, проверки на податоци, датотечни и API‑задачи – сè станува пореактивно, без да си предизвикате нови Deadlocks при секое Progress-Update.
Ако ви треба поддршка за стабилизирање на паралелен код, за дебагирање на замрзнувања на UI или за чисто модернизирање на постоечки Delphi-апликации: контактирајте не.
За оваа тема важни се и Delphi Parallel Programming Library и Tthread.queue Vs Synchronize. Статијата ги објаснува овие аспекти и покажува на што треба да се обрне внимание во секојдневната пракса.
Разговарајте за проект или план за модернизација со Net-Base.
Следен чекор
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Не поддржуваме само при поединечни прашања, туку и кога од исечоци од изворен код, legacy-теми или идеи за портали треба да прерасне во робустен корпоративен проект.
- Постоечката состојба, целната слика и техничките ризици се проценуваат заедно.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Ќе увидите рано кој пат е економски и оперативно одржлив.