Net-Base Журнал

02.08.2026

Windows Service в Delphi: як правильно реалізувати Graceful Shutdown з TEvent і Stop-Timeout

Коли Windows-сервіс під час зупинки «зависає», це рідко буває випадковістю: зазвичай блокуються потоки, I/O або sleep-цикли без шляху для переривання. У цьому практичному матеріалі показано, як у Delphi за допомогою TEvent реалізувати плавне завершення роботи (graceful shutdown) та коректно обробляти таймаути зупинки...

02.08.2026

Від теми журналу до практики проєкту

Відповідні сторінки послуг і технічні сторінки до публікації

Одна Windows служба в Delphi у повсякденному використанні часто виглядає непомітно: працює у фоні, обробляє завдання, пише логи, звертається до баз даних або REST-API. До того моменту, коли хтось натисне «Зупинити службу» — або настане патч-ребут — і служба не зупиниться коректно. Тоді консоль служб кілька хвилин показує «Завершується…», служба зависає в стані Stop Pending, і в найгіршому випадку процес завершується примусово. Саме тут варто розглядати Windows Service in Delphi Graceful Shutdown як усвідомлену архітектурну проблему: з чітким сигналом для зупинки, визначеними таймаутами й потоками, які справді реагують.

У цій статті йдеться не про внутрішні механізми фреймворків, а про практичний шаблон: TEvent як сигнал зупинки (ядрo-орієнтований об’єкт синхронізації з System.SyncObjs), у поєднанні зі стратегією таймауту зупинки, яка враховує і Windows Service Control Manager (SCM, тобто ту компоненту Windows, що запускає/зупиняє служби), і ваші власні робочі потоки. Далі типові крайові випадки, підходи до налагодження та питання, коли додаткова логіка справді виправдана.

Windows служба в Delphi — коректне завершення роботи на практиці

Найпоширеніша причина проста: у службі є принаймні один потік, який застряг у блокуючій операції і не має шляху для припинення. Класичні випадки:

  • Опитувальні цикли з Sleep: «while not Terminated do Sleep(1000)». Після команди Stop сигнал надходить, але потік реагує лише через до 1 секунди (або 30 секунд…).
  • Блокуючий I/O: виклики до баз даних, HTTP-запити, іменовані канали, очікування файлової системи — усе, що «просто чекає», не звертаючи уваги на сигнал зупинки.
  • Споживач черги без механізму пробудження: робочий потік чекає на чергу, але при зупинці його не розбуджують, щоб він вийшов.
  • Порядок блокувань / дедлоки: при зупинці виконується «очищення», тоді як інші потоки ще тримають блокування. Це часто трапляється лише на шляху зупинки, бо порядок дій там відрізняється від нормальної роботи.

Windows SCM очікує, що служба швидко відреагує на команду зупинки і постійно звітує про свій статус (через SetServiceStatus; Delphi інкапсулює це в сервісній компоненті). Якщо ви приймаєте подію Stop, але не коректно завершуєте потоки, процес залишається живим — і Windows рано чи пізно вирішить, що «занадто довго». Результат — або примусове завершення, або служба, що зависла в невизначеному стані.

Основний принцип: сигнал зупинки, який розуміє кожен робочий потік

Абстрактна графіка: робочі потоки чекають на роботу або подію зупинки і завершують роботу впорядковано
Коли робочі потоки чекають на «роботу або зупинку», латентність зупинки знижується без опитування.

Коректне завершення роботи можливе лише за наявності сигналу, який:

  • може бути відстежений усіма релевантними потоками,
  • також проявляє ефект із блокуючих станів очікування,
  • у шляху зупинки детермінований (без надії «може він колись вийде»),
  • має чітку Timeout-Strategie.

In Delphi ist TEvent dafür ein sehr brauchbares Werkzeug: ein Event-Objekt, das intern über Windows-Handles umgesetzt ist (vergleichbar mit CreateEvent/SetEvent). Du kannst es als „Stop requested“-Signal verwenden. Jeder Worker wartet dann nicht einfach blind, sondern wartet „auf Arbeit oder auf Stop“.

TEvent richtig wählen: ManualReset vs. AutoReset

Bei Stop-Signalen willst du üblicherweise Manual Reset (manuell zurücksetzbar): Einmal gesetzt, bleibt das Event „signaled“, bis du es zurücksetzt. Damit ist sichergestellt, dass jeder Thread, der später in eine Wartephase kommt, das Stop-Signal trotzdem erkennt. Auto Reset wäre hier riskant, weil es das Signal nach einem wartenden Thread automatisch zurücksetzt und andere Threads das Stop-Signal verpassen könnten.

Delphi-Service-Lebenszyklus: Wo Stop wirklich ankommt

Ein Delphi-Windows- und Linux-Services basiert typischerweise auf TService (VCL/RTL). Der SCM schickt Kommandos (Start, Stop, Pause, Continue). Delphi ruft dann entsprechende Events/Methoden auf (je nach Template z. B. OnStart, OnStop, OnExecute).

Wichtig für die Architektur:

  • OnStop ist kein Ort für langes Warten ohne Status-Updates. Es ist der Ort, an dem du den Shutdown anstößt und dann kontrolliert wartest – mit Timeout.
  • OnExecute ist oft eine Schleife. Wenn du dort „endlos“ arbeitest, muss die Schleife auf ein Stop-Signal reagieren.
  • Worker-Threads (TThread oder Thread-Pools) müssen auf dasselbe Stop-Signal reagieren, sonst ist der Service logisch gestoppt, aber physisch noch nicht fertig.

Sauberes Muster: Stop-Event + Join der Worker + harter Fallback

Das praxistaugliche Muster besteht aus vier Schritten:

  1. Stop anfordern: Stop-Event setzen, keine neuen Jobs mehr annehmen.
  2. Wakeups auslösen: Wenn Worker auf Queues oder Sleeps warten, müssen sie „aufwachen“ können (z. B. über Event/Queue-Signal).
  3. Geordnet beenden: Worker beenden ihre Loops, schließen Ressourcen (DB-Verbindungen, Dateien, Handles) und melden „fertig“.
  4. Timeout und Fallback: Wenn nicht alles rechtzeitig endet, musst du eine Entscheidung treffen: weiter warten (mit Status-Update) oder kontrolliert abbrechen/hart beenden (je nach Risiko).

Der Kern ist: kein Thread darf ausschließlich auf Zeit warten (Sleep) oder ausschließlich auf I/O blockieren, ohne parallel ein Stop-Signal zu berücksichtigen. Stattdessen arbeitest du mit Wartefunktionen, die mehrere Signale berücksichtigen (z. B. „Stop-Event oder Work-Event“), oder du kapselst I/O in Timeouts plus Stop-Checks.

Stop-Timeout richtig denken: SCM-Timeout vs. eigener Shutdown-Timeout

Hier passieren in Projekten die meisten Missverständnisse. Es gibt zwei verschiedene Timeout-Ebenen:

  • Очікування SCM: Windows очікує, що ви в стані SERVICE_STOP_PENDING регулярно звітуватимете про прогрес. Інакше створюється враження зависання. Delphi частково це обробляє, але якщо ви самі довго блокуєтесь, потрібна стратегія, як і надалі дозволяти оновлення статусу (або як скоротити фазу зупинки).
  • Ваш власний таймаут вимкнення: Ви визначаєте, наприклад: „Ми даємо собі 20 секунд, щоб акуратно завершити поточні завдання, потім припиняємо роботу.“ Це архітектурне рішення: узгодженість даних проти примусового перезавантаження проти вимог експлуатації.

Практично це означає: ваш сервіс повинен швидко перейти в стан, у якому він не запускає нові одиниці роботи, а потім лише чекає на завершення поточної роботи — але не безкінечно. І ця фаза очікування має виконуватися невеликими інтервалами, щоб ви могли реагувати та за потреби логувати.

Скільки може тривати зупинка?

Не існує універсального числа, яке пасувало б завжди. Для багатьох бізнес-сервісів реалістичним є цільовий діапазон 5–30 секунд: достатньо часу для „in-flight“ даних, але достатньо коротко для вікон оновлень. Якщо вам регулярно потрібно більше часу, це часто вказує на те, що ви обробляєте занадто великі одиниці за раз або що зовнішні залежності (DB/HTTP) працюють без таймауту.

Реалізація з TEvent: побудова, що залишається стабільною в експлуатації

Перевірена схема в Delphi-сервісі виглядає так (без подробиць по фреймворку):

  • Одне Stop-Event (TEvent, Manual Reset), яке встановлюється під час зупинки.
  • Один або кілька Worker-Threads, які в головному циклі регулярно перевіряють на Stop.
  • Опціонально Work-Event або черга, що сигналізує про роботу. Тоді Worker очікують на „Work або Stop“.
  • Фаза вимкнення, яка робить join над Worker-ами (тобто чекає їх завершення), але з таймаутом.

Ключове — не те, чи ви використовуєте TThread, omnithreadlibrary або власний пул, а те, щоб ваші Worker не працювали «вслепу». Цикл Worker має структурно виглядати так: очікування на подію(ї) → робота невеликими частинами → між частинами перевірка Stop → коректне звільнення ресурсів.

Підводний камінь: одного Terminate недостатньо

Багато Delphi-потоків «переривають» за допомогою Terminate. Але це лише прапорець. Якщо потік зараз заблокований у якійсь блокуючій API, спочатку нічого не відбувається. Тому власне Stop-Event дуже корисне: його можна інтегрувати у виклики очікування і цілеспрямовано викликати пробудження.

Підводний камінь: FreeOnTerminate в контексті сервісу

У сервісах часто зустрічається FreeOnTerminate := True. Це може працювати, але ускладнює контроль над завершенням, бо ви часто не маєте чистої посилання, щоб чекати на завершення потоку і протоколювати помилки. Для контрольованої логіки зупинки зазвичай стабільніше явно володіти потоками та детерміновано чекати їх і звільняти під час завершення.

Блокуючі операції: як зробити їх здатними до зупинки

Troubleshooting-Szene: Netzwerkverbindung als Ursache für blockierende Calls und Timeouts
Блокуючий I/O без тайм-аута — найпоширеніша причина зависань сервісу під час зупинки.

Складність полягає не в самій події, а в місцях, де ваш сервіс блокується. Три типові класи:

1) Sleep/Polling ersetzen: Wait mit Stop-Event

Якщо ваш процес працює періодично («перевіряти кожні 10 секунд»), не використовуйте Sleep(10000), а очікуйте на подію з тайм-аутоm. Тоді ваш Stop-Event може негайно припинити очікування. Це зменшує затримку при зупинці та запобігає відчуттю «сервіс не відповідає».

2) Queue-Consumer: Work-Event + Stop-Event kombinieren

Якщо у вас архітектура Producer/Consumer (наприклад, задачі кладуться в чергу), вам потрібен сигнал, який розбудить Consumer. Часто це додатковий TEvent (Work available). Тоді Consumer очікує на два Handles: «Work» або «Stop». При зупинці ви встановлюєте Stop-Event і, за потреби, також Work-Event, щоб усі Consumer гарантовано вийшли з очікування.

3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade

Під час доступу до бази даних або HTTP-викликів вирішується, чи коректно зупиняється ваш сервіс. У робочому середовищі діє правило: жодного виклику без тайм-аута. Тайм-аут — не розкіш, а передумова керованості. Додатково слід перевіряти Stop між фазами повторних спроб/Backoff. Інакше отримаєте класичну ситуацію: «сервіс не зупиняється, бо зараз робить 10 Retries з Sleep».

В деяких бібліотеках ви можете явно тригерити скасування (наприклад, скасування запиту). Якщо це неможливо, ви повинні принаймні налаштувати тайм-аути так, щоб вони були достатньо короткими і не перевищували Shutdown-Timeout.

Stop Pending korrekt: Status, Logging und Erwartungsmanagement

Zeitmessung und Log-Analyse zur Diagnose von Stop-Timeouts bei Windows-Services
Фазові логи та вимірювання часу роблять тайм-аути при зупинці відтворюваними та пояснюваними.

Коли сервіс зупиняється, з операційної точки зору важливо розуміти, де він зависає. Для цього вам потрібні дві речі:

  • Log-маркери у шляху зупинки: «Запит на зупинку», «немає нових задач», «чекаю на Worker», «Worker X завершився», «Shutdown завершено».
  • Вимірювані інтервали часу: Скільки триває зупинка? Яка фаза споживає час? Часто достатньо монотонного часового виміру, такого як GetTickCount64 або TStopwatch (монотонний = не спотворюється змінами системного часу).

Якщо в шляху зупинки ти записуєш лише один запис у лог «Stopping…», налагодження в полі перетворюється на вгадування. В експлуатації служби логи часто є єдиним, що ти отримуєш без взаємодії.

Які логи в службах дійсно корисні?

  • PID служби, час запуску, версія/збірка (без зайвого оверхеду).
  • Кількість активних воркерів, кількість завдань у виконанні (in-flight).
  • Активні зовнішні залежності: «DB-Call виконується», «HTTP-Request виконується», «flush файлу виконується» (тільки агреговано, не кожна деталь).
  • Досягнуто таймауту зупинки: які воркери досі відкриті?

Налагодження в полі: робити відтворюваним замість гадати

Проблеми зі зупинкою часто виникають лише в продакшні: інше навантаження, інша латентність, інші права, інші вікна для патчів. Кілька перевірених у практиці важелів:

Тестувати сервіс під контролем

  • Зупинка під час активної обробки (не в режимі простою).
  • Зупинка під час зовнішньої несправності: БД тимчасово недоступна, HTTP-ендпоінт повільний, файловий шар відсутній.
  • Зупинка безпосередньо після старту (race-conditions: воркери ще ініціалізуються).

Сигнали Event Viewer та Service Control Manager

Windows записує події служби, але вони часто грубі. Краще, коли сам сервіс пише в лог-файл або в Windows журнал подій. Важливо: логування повинно працювати в шляху зупинки. Якщо ти звільняєш логер занадто рано під час завершення або Flush блокується, ти втрачаєш саме ті вирішальні сліди.

Зробити завислі потоки видимими

Якщо ти повторно бачиш «Stop Timeout», варто подивитися на стани потоків (наприклад, через дебагер/Procdump у тестовому середовищі). Часто знаходиш потік у стані очікування на дескриптор, який ніколи не сигналізується, або в мережевому виклику без таймауту. Виправлення рідко полягає в «ще більшому Sleep», скоріше в чистому шляху переривання.

Коли зусилля справді виправдані?

Мінімалістичний сервіс, який має лише таймер і не має зовнішніх залежностей, іноді може «просто зупинитися». Але щойно виконується будь-яка з наведених умов, коректне завершення (graceful shutdown) майже завжди виправдане:

  • Сервіс обробляє завдання з побічними ефектами (запис файлів, транзакції БД, API-виклики).
  • Є декілька потоків або пул потоків.
  • Сервіс залежить від мережевих ресурсів (БД, REST, Message Broker, файлові шари).
  • Операційна команда вимагає планованих вікон для техобслуговування (перезавантаження, оновлення, failover).

Додана вартість — не «елегантність», а надійність експлуатації: менше жорстких аварійних завершень процесів, менше неконсистентних проміжних станів, менше ручних втручань.

Практичні підводні камені: що зазвичай йде не так при завершенні

1) Встановлено Stop, але нові завдання все одно надходять

Якщо ти приймаєш вхідну роботу (наприклад, через сокет, тригер файлу, таймер), у шляху зупинки треба спочатку припинити прийом нової роботи: закрити listener, відключити таймер, зупинити планувальник. Інакше ти будеш наздоганяти завершення, оскільки все ще стартують нові завдання.

2) Очищення блокується (Flush, Close, Finalize)

«Ще швидко все флашнути» може бути небезпечним у контексті сервісу, якщо ціль (мережевий диск, віддалений лог, БД) зараз зависла. Тому: очищення — так, але з обмеженим часом. У крайньому разі потрібно вирішити, які дані в пам’яті ти готовий втратити, замість того щоб блокувати повну зупинку.

3) Блокування та порядок

Під час Stop ви часто звертаєтесь до тих самих структур даних, що й Worker (Queues, Caches, States). Якщо Stop-потік утримує блокування і потім чекає на завершення Worker, тоді як Worker потребують того самого блокування, виникає deadlock при Stop. Заходи протидії: зменшувати час утримання блокувань, не чекати «під блокуванням» у Stop-шляху, визначити чіткий порядок.

4) Nebenläufigkeit beim doppelten Stop

На практиці Stop може бути ініційований кілька разів (наприклад, Stop + Shutdown або повторний Stop). Ваш Stop-шлях має бути ідемпотентним: встановлення Stop-Event допустиме, але подвійна Join/Free-Logik має бути надійно захищена (наприклад, через ein Atomik-Flag).

Operativer Blick: Was Admins und IT-Leads vom Service erwarten

Для експлуатації та адміністрування врешті важливе не те, наскільки «гарний» код, а чи сервіс:

  • при Stop надійно завершується (передбачувано, без зависань),
  • при Stop не створює неконсистентних даних (наприклад, половинні файли, відкриті транзакції),
  • у випадку помилки дає корисні логи,
  • під час вікон обслуговування та деплойментів є передбачуваним.

Саме тому питання Stop-Timeout — це не лише «справа розробників»: воно впливає на цикли патчів, час відновлення та на те, чи можливі автоматизовані деплої взагалі.

Konkrete Leitplanken für ein robustes Shutdown-Design

Якщо ви прагнете прагматично стандартизувати підхід, ці орієнтири себе виправдали:

  • Глобальний Stop-Event, Manual Reset — створювати рано в життєвому циклі сервісу, звільняти пізно.
  • Жодного Sleep у Worker-циклах без альтернативи, що реагує на Stop (наприклад, Wait з таймаутом).
  • Усі зовнішні виклики з таймаутами (DB, HTTP, Fileshares). Вибирайте таймаути так, щоб вони вкладалися у ваш Shutdown-Timeout.
  • Stop-Timeout як конфігурація (наприклад, у INI/Registry), щоб експлуатація могла реагувати без перекомпіляції.
  • Багаторівнева модель: спочатку коректне завершення (завершити поточні задачі), далі опційний «soft abort» (не запускати нові кроки), а врешті жорсткий Exit як останній вихід.
  • Якісні Stop-логи з фазами та вимірюванням часу.

Fazit: TEvent + Stop-Timeout ist kein Luxus, sondern Steuerbarkeit

Зависла зупинка рідко є поодинокою помилкою — зазвичай це архітектурна прогалина: робота виконується в потоках або блокуючих викликах, які не знають про спільний сигнал Stop. З чітким Stop-Event (TEvent, Manual Reset), очікуваннями, що реагують на Stop замість Sleep, послідовними таймаутами для зовнішніх залежностей і визначеним Shutdown-Timeout ви отримаєте сервіс, який у повсякденній експлуатації є передбачуваним.

Ці підходи особливо виправдані, якщо ваш сервіс працює в продуктивному середовищі з вікнами обслуговування, автоматизованими деплойментами або критичними побічними ефектами. Тоді «graceful shutdown» — це не косметика, а елемент стабільної експлуатації та зменшення ескалацій під час наступного перезавантаження.

Якщо ви хочете впорядкувати ваш Stop-шлях або перевірити існуючий Delphi-сервіс на предмет стійкої логіки завершення та безпеки експлуатації, технічний sparrings-дзвінок часто є найшвидшим шляхом до конкретних заходів: зв’язатися.

Для цієї теми також важливі Delphi Windows Service і Tevent Delphi. Стаття упорядковує ці аспекти зрозуміло і показує, на що звертати увагу в щоденній експлуатації.

Проєкт або модернізаційне завдання обговорити з Net-Base.

Наступний крок

Якщо тема перетворюється на реальний проєкт, архітектуру, наявні системи та експлуатацію слід розглядати разом на ранньому етапі.

Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, доступ до даних, портали та Rollout не відсуваються на пізніший етап.
  • Ви заздалегідь бачите, який шлях є економічно та операційно життєздатним.

Поділитися дописом

Поділитися цим дописом безпосередньо

LinkedIn, X, XING, Facebook, WhatsApp та E‑Mail доступні негайно. Для Instagram ми безпосередньо готуємо посилання та короткий текст.

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

Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.