Net-Base Списание

24.07.2026

Delphi: TParallel.For с потокобезопасен Progress-UI (TThread.Queue) без deadlocks

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

24.07.2026

От темата в списанието към проектната практика

Подходящи страници за услуги и технологии към публикацията

Който в Delphi иска да паралелизира изчислително натоварени или I/O-интензивни задания, бързо стига до Parallel Programming Library (PPL) и конкретно до TParallel.For. Ефектът често е веднага измерим – докато не опитате в същия дъх „бързо“ да обновите интерфейс за прогрес (Progress-UI). Точно тук възникват типичните забивания: на пръв поглед случайни замръзвания на интерфейса, една ProgressBar, която скача назад, или пълен deadlock, щом стъпите през изпълнението в дебъгъра.

В тази статия става дума за thread-сигурен Progress-UI при TParallel.For: устойчив шаблон, който работи във VCL и FMX, който аккуратно групира UI-актуализации чрез TThread.Queue, отчита Cancel/Abort и последователно заобикаля най-честите капани за deadlock. Фокусът е върху реалната оперативна среда: възпроизводимо поведение, ясни отговорности и указания за дебъгване, които помагат дори когато грешката се проявява само „при клиенти“.

Защо Progress-UI при TParallel.For толкова често се проваля

TParallel.For обикновено се изпълнява на работни нишки от Delphi-threadpool-а. Тези нишки не трябва да засягат директно VCL- или FMX-контроли, защото UI-фреймуърковете (Message Loop, Window Handles, Rendering) са обвързани с основната нишка (Main Thread). Дори един на пръв поглед безвреден ProgressBar.Position := … от Worker може да доведе до непредвидимо поведение: спорадични AV-та, замръзнали прозорци или „фликерищи“ обновявания.

Очевидният ремонт често е TThread.Synchronize. Това решава въпроса за thread-safety, но в паралелни цикли бързо води до друг проблем: създава сериен тесен участък. Всеки Worker чака UI-нишката, която от своя страна е заета с рендериране и обработка на Synchronize-извиквания. Под натоварване това изглежда като deadlock – дори когато е „само“ ефект на starvation/lockstep.

И после има истинския клас deadlock: Main Thread чака (напр. чрез WaitFor, Task.Wait или индиректно чрез блокиращи извиквания) края на паралелната операция, докато Worker-нишките се опитват чрез Synchronize или неблагоприятно използване на Queue да изпратят работа към Main Thread. Резултат: Main Thread чака Worker, Worker чакат Main Thread.

TThread.Queue vs. TThread.Synchronize: практическата разлика

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
При дебъгване бързо се вижда дали Worker-нишките чакат Main Thread или само пускат обновления в опашката.

И двата механизма служат за безопасно изпълнение на код в Main Thread. Разликата е в семантиката на изчакване:

  • TThread.Synchronize: Извикващият Worker чака, докато Main Thread изпълни кода. Това е „синхронно“, увеличава латенцията и е класическа причина за deadlock, когато Main Thread е в блокирано състояние.
  • TThread.Queue: Worker-ът поставя кода само в опашката за главния поток и продължава изпълнението. Това е „асинхронно“, разcouple-ва нишките и в паралелни сценарии почти винаги е по-добрият избор по подразбиране – стига да се контролира честотата на актуализациите.

Важно: Queue не е безплатен пропуск. Ако при всяка итерация на цикъл изпращате актуализация чрез Queue, ще наводните опашката на главния поток. В такъв случай UI-то няма да блокира заради Deadlock, но ще страда от просто прекомерно количество съобщения. UI-то ще изглежда „завалено“/„бавно“, а завършването на обработката ще се забави, защото все още ще се изпълняват стотици или хиляди UI-актуализации.

Граничният случай, който наистина боли: изчакване в UI нишката

В корпоративни приложения често се наблюдава следният поток: натиска се бутон „Start“, UI-то се деактивира, отваря се ProgressDialog, след което се извършва синхронно „изчакване“, докато всичко не приключи, и накрая UI-то се активира отново. Този модел е в основата на много Deadlock-и.

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

  • Главният поток стартира TParallel.For и след това извиква блокираща логика за изчакване (директно или индиректно).
  • ProgressDialog извиква в конструктора или OnShow рутина, която вътрешно изчаква.
  • Бутонът „Отказ“ задава флаг, но въпреки това главният поток остава в изчакващ цикъл.

Ако през това време работещите нишки използват Synchronize, Deadlock-ът е практически гарантиран. При използване на Queue също може да настъпи задържане, ако главният поток е блокиран и не обработва съобщенията — тогава и опашката няма да бъде изпразнена.

Практическото следствие е: главният поток не бива да чака блокиращо за паралелния цикъл, ако паралелно са необходими UI-актуализации. Вместо това обработката трябва или да бъде изцяло пренесена във фоновa задача, или да се организира „асинхронен финал“ (callback/queued крайна операция), който накрая освобождава UI-то.

Чист подход: прогрес само агрегирано, UI-актуализации регулирани

Arbeitsplatzmotiv mit abstrahierter Progress-Anzeige und Zeitmessung als Symbol für gedrosselte UI-Updates
Агрегацията и времевото дроселиране предотвратяват наводняване на UI-то от твърде много актуализации.

Стабилен модел се състои от три ясно разделени отговорности:

  • Worker-нишки извършват реалната работа за всеки елемент/индекс. Те докладват само напредъка в нишкобезопасна форма (брояч, Queue, thread-safe Queue).
  • Агрегатор (често: главният поток или специализиран таймер в 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-то. Недостатък: нямате подробни съобщения за всеки елемент (напр. име на файл). За това може да се добави второ, ограничено съобщение за статус (виж следващия раздел).

Statusmeldungen ohne Spam: „letzter Status gewinnt“

Ако искате допълнително да покажете кратък текст (текущ елемент, фаза, съобщение за грешка), също е необходим шаблон, който да не залива UI при всяка стъпка на Worker-а. На практика „letzter Status gewinnt“ работи много добре:

  • Worker записва статусна информация в потокобезопасна структура (напр. атомарно заменяем низ, или защитено с малък Lock).
  • UI-Timer периодично поема последно видяния статус в Label.

Така UI-то остава отзивчиво и все пак виждате, че „нещо се случва“. По-важна тук е не самата стойност на низа, а продължителността на живота: не пренасяйте референции към краткотрайни обекти от Worker нишки в UI нишката. Ако предавате обекти, уточнете Ownership експлицитно.

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

Има сценарии, в които само UI-Timer не е достатъчен: напр. когато в края искате сигурно да предизвикате точно едно „Fertig“-обновление, или когато UI-обновлението е по-сложна стъпка (напр. запис в лог-прозорец, но ограничен). Тогава е подходящо TThread.Queue – но не за всяка итерация, а целенасочено.

Практичен подход е една опашка само за събития с ниска честота:

  • Start-Event (подготовка на UI, блокиране на бутони)
  • Периодични Progress-Events (макс. на всеки X милисекунди)
  • Fehler-Events (по избор събрани)
  • Done-Event (възстановяване на UI, показване на резултата)

Периодичността не се постига от UI нишката, а в контекста на Worker-а: позволявате на Worker-а да queue-не UI-обновление само ако е изминало достатъчно време от последното UI-обновление. За това е подходящ монотонен източник на време (напр. TickCount) плюс атомарна стойност „last update“.

Важно: самото UI-обновление трябва да е „бързо“. Скъпи изчисления, файлов I/O или заявки към база данни не трябва да бъдат в gequeu-вания UI callback. Callback-ът трябва само да чете състояния и да задава контроли.

Cancel-Handling: Прекъсване ohne Hänger

В реални приложения прекъсването не е по желание. Ключово е: Cancel не е „Kill“, а кооперативно приключване. Worker-ите трябва редовно да проверяват дали е зададен сигнал за прекъсване и тогава да излизат чисто. В Delphi има няколко начина за това (в зависимост от PPL-конструкцията): собствен Volatile-флаг, атомарен Boolean, или концепция за Cancellation през Tasks (в зависимост от версията и структурата на Delphi).

За експлоатацията важат две правила:

  • Cancel muss schnell sichtbar werden: Проверявайте флага за прекъсване на подходящи места, не само в края на итерация, когато итерацията може да продължи и секунди.
  • Cancel muss aufräumen: Отворени хендъли, временни файлове, транзакции или заключвания не бива да остават. Това означава: Във всяка Worker-итерация са задължителни try/finally-блоковете, когато участват ресурси.

От гледна точка на UI Cancel следва само да постави сигнал и да постави UI в състояние „Stopping…“. Самото приключване и реактивиране на UI се случват в Done-Event, а не веднага при клика.

Deadlocks vermeiden: die häufigsten Fallen in der Praxis

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlocks entstehen oft durch zyklisches Warten: UI-Thread blockiert, Worker warten auf UI-Zugriff.

Falle 1: WaitFor/Task.Wait im Main Thread

Когато Main Thread е блокиран, той не може да изпълнява Queue-callbacks и не може да обработва съобщения. Това изглежда като deadlock, дори когато Worker-ите продължават да работят коректно. Решение: никакви блокиращи изчаквания в UI-Thread. Вместо това извършете финализиращото действие чрез TThread.Queue или чрез управление по събития (например Timer проверява „готово“).

Falle 2: Synchronize innerhalb eines Locks

Класика: Worker държи Critical Section, извиква Synchronize, а в UI-callback-а (директно или индиректно) отново се изисква същата Critical Section. Резултатът е кръг на изчакване. Правилото е просто: Keine UI-Übergabe (Synchronize/Queue) aus einem gehaltenen Lock heraus. Ако е необходим Lock, вземете всички данни първо в локални променливи, напуснете Lock-а и едва след това направете queue.

Falle 3: UI-Callback triggert Reentrancy

Понякога самото обновяване на UI не е „безобидно“: задаване на свойства може да предизвика събития (OnChange, OnResize), които пък стартират логика, достъпваща състояния на Worker-ите. Това не е deadlock в тесния смисъл, но води до трудно обясними замръзвания и състезателни условия. Мерки: поставяйте UI-ъпдейтите в „тихи“ пътища (временно деактивирайте събитията) или използвайте Reentrancy-Guards (напр. атомарен гард за фазата на обновяване).

Falle 4: Zu viele queued Updates

Дори без изчаквания UI може да „замръзне“, ако генерирате десетки хиляди queued callbacks. Симптоми: ProgressBar продължава дълго да реагира, прозорецът е муден, CPU натоварването на Main Thread е високо. Решение: дроселиране (времеви прозорец), агрегиране (брояч) или истинска Producer/Consumer структура, при която може да има само един „pending“ UI-ъпдейт (Coalescing).

Wenn es komplizierter wird: Ergebnisse sammeln, Fehler bündeln, Reihenfolgen garantieren

TParallel.For е идеален, когато итерациите са независими. В бизнес-софтуера обаче итерациите често са само „в голяма степен“ независими: четат файлове, извикват REST-APIs, записват редове в база данни. Тогава трябва да планирате три допълнителни неща внимателно:

  • Thread-sichere Ergebnis-Sammlung: Entweder pro Thread ein lokaler Buffer (am Ende zusammenführen) oder eine threadsichere Queue/Collection. Locks im Hot Path vermeiden.
  • Обработка на грешки: Изключенията от Worker-нишки трябва да се събират. На практика е доказано: запазете първото изключение и стартирайте Cancel, или събирайте всички изключения и в края ги покажете обобщено.
  • Подредба: Ако изходът изисква стабилен ред (напр. логове по индекс), паралелната обработка с последващо сортиране често е по-лесна от „потокобезопасно подредено вмъкване“.

За UI това означава: Не показвайте всяко едно съобщение за грешка незабавно. Това води до ада на модалните диалози. Събирайте грешките (напр. списък от низове) и в края покажете обобщение или лог, който може да се експортира.

Отстраняване на грешки: Как наистина да направите deadlock видим

Deadlock-ите в паралелен код са фрустриращи, защото могат да изглеждат различно в дебъгъра отколкото в релийз. Въпреки това има няколко много практични подхода:

Използвайте прозореца за нишки и стековете на извикванията

Ако 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-callback-овете изискват повече от няколко милисекунди, е необходимо ограничаване на честотата или опростяване.

Кога TParallel.For с Progress-UI наистина си струва?

Паралелизацията не е цел сама по себе си. Тя е полезна особено когато:

  • итерациите са достатъчно големи (милисекунди до секунди), така че overhead-ът на thread pool-а да се прехвърли на заден план,
  • задачата е CPU-интензивна (парсинг, компресия, хеширане) или има I/O, който се поддава добре на паралелизация (няколко файла, няколко HTTP-заявки с лимити),
  • имате ясна стратегия за Cancel и обработка на грешки,
  • изискванията на UI-то могат да се задоволят с агрегирано представяне на прогреса.

Тя е по-малко полезна, когато всяка итерация е изключително кратка (микрооперации) или когато всички итерации се сблъскват със същия тесен ресурс (серийна DB-транзакция, глобален lock, един единствен файл). В този случай по-бързият лост често е: подобряване на алгоритъма, пакетиране, намаляване на достъпа до данни или явното разкачване на тесния ресурс.

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

  • Main Thread не трябва да бъде блокиран: никакви Waits, никакви дълги цикли без message pump.
  • Worker-ите никога не засягат Controls: никакви VCL/FMX-достъпи извън UI-нишката.
  • UI-ъпдейтите са ограничени: брояч/таймер или коалесцирани Queue-ъпдейти вместо по итерация.
  • Никакви повиквания на Synchronize от вътре в locks.
  • 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, Datenprüfungen, Datei- und API-Jobs – всичко става по-отзивчиво, без да си навличате нови 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.

Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

  • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
  • 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.

Електронна поща

Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.