Net-Base списание

02.08.2026

Windows Service во Delphi: уредно исклучување со TEvent и правилна имплементација на Stop-Timeout

Кога Windows-услугата при запирање „виси“, тоа ретко е случајност: најчесто нишки, I/O или Sleep-цикли блокираат без пат за прекинување. Овој практичен прилог покажува како во Delphi со TEvent да имплементираш чист Graceful Shutdown и како правилно да ракуваш со Stop-Timeouts...

02.08.2026

Од тема во магазинот до проектна пракса

Соодветни страници за услуги и технички информации поврзани со објавата

Еден Windows Service in Delphi често во секојдневната работа делува несензационално: работи во позадина, обработува jobs, запишува логови, комуницира со бази на податоци или REST-APIs. Сè додека некој не кликне „Заврши услуга“ – или не дојде Patch-Reboot – и сервисот не запре чисто. Тогаш конзолата за услуги по неколку минути покажува „Wird beendet…“, сервисот останува во Stop Pending-статус, и во најлош случај процесот се прекинува на силен начин. Токму тука има смисла да се третира Windows Service in Delphi Graceful Shutdown како свесна архитектонска тема: со јасен shutdown‑сигнал, дефинирани timeouts и нишки што навистина реагираат.

Овој напис не се занимава со интерни на фрејмворкот, туку со практичен образец: TEvent како stop‑сигнал (синхронизациски објект блиску до Kernel од System.SyncObjs), комбиниран со Stop-Timeout-стратегија која ги зема предвид и Windows Service Control Manager (SCM, односно Windows‑компонентата што ги стартува/запира услугите) и твоите сопствени worker‑нишки. Плус типични рабни случаи, пристапи за дебагирање и прашањето кога дополнителната логика навистина се исплатува.

Windows Service in Delphi Graceful Shutdown во пракса

Најчеста причина е едноставна: сервисот има барем една нишка која е заглавена во блокирачка операција и нема пат за откажување. Класични примери:

  • Polling‑цикли со Sleep: „while not Terminated do Sleep(1000)“. При стоп доаѓа сигналот, но нишката реагира дури по до 1 секунда (или 30 секунди…).
  • Блокирачки I/O: повици кон база на податоци, HTTP‑запроси, Named Pipes, чекања на фајл‑системот – сè што „само чека“, без да обрнува внимание на стоп‑сигнал.
  • Queue‑Consumer без Wakeup: worker чека на редица, но при стоп не е разбуден за да излезе.
  • Редослед на заклучувања / Deadlocks: при стоп се прави „cleanup“, додека други нишки сè уште држат заклучувања. Ова често се појавува само во стоп‑патот, бидејќи редоследот таму е различен од нормалната работа.

Windows SCM очекува дека сервисот ќе реагира брзо на команда за стоп и ќе го пријавува својот статус континуирано (преку SetServiceStatus; Delphi го капсулира тоа во Service‑компонентата). Ако го прифатиш стоп‑настанот, но не ги исклучиш чисто своите нишки, процесот останува жив – и Windows на крајот ќе одлучи дека „трае предолго“. Резултатот е или тврд прекин или сервис што заглавува во нејасен меѓупростор.

Основно начело: едно стоп‑сигнал што го разбира секој worker

Апстрактна графика: Worker-нишки чекаат на работа или стоп‑настан и уредно завршуваат
Кога worker‑нишките чекаат на „Работа или Стоп“, латенцијата при стоп опаѓа без polling.

Еден Graceful Shutdown функционира само ако имаш сигнал што:

  • може да биде набљудуван од сите релевантни нишки,
  • да дејствува и од блокирачки Wartezuständen,
  • во Stop-Pfad да е deterministisch (без „можеби некогаш ќе излезе“-најава),
  • да има јасна 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 делумно се грижи за тоа, но откако сам ќе блокираш подолго, ти треба стратегија како да овозможиш натамошни статус-ажурирања (или како да ја скратиш својата стоп-фаза).
  • Твојот сопствен timeout за исклучување: На пример, дефинираш „Дајте си 20 секунди за чисто завршување на тековните работи, потоа прекинуваат.“ Тоа е архитектонска одлука: конзистентност на податоците vs. принуден рестарт vs. оперативни барања.

Практично тоа значи: твојот сервис треба брзо да стигне во состојба во која не стартува нови единици на работа, и потоа само чека тековната работа да заврши — но не бесконечно. И оваа фаза на чекање треба да се врши во мали интервали, за да можеш да реагираш и евентуално да логираш.

Колку долго смее да трае стопот?

Нема магичен број што секогаш важи. За многу бизнис-сервиси реалистичен таргет е во опсег од 5–30 секунди: доволно време за „in-flight“ податоци, но доволно кратко за patch-прозори. Ако редовно ти треба подолго, тоа често укажува дека обработуваш преголеми единици наеднаш или дека екстерните зависности (DB/HTTP) работат без тајмаут.

Имплементација со TEvent: структура што останува стабилна во работа

Проверен распоред во Delphi-сервис изгледа вака (без да се исцрпуваат деталите на фрејмворкот):

  • Едно Stop-Event (TEvent, Manual Reset), кое се поставува при стопирање.
  • Една или повеќе работни нишки, кои во својата главна петља редовно проверуваат дали е поставен Stop.
  • Опционално едно Work-Event или редица (Queue) која сигнализира работа. Работниците потоа чекаат на „Work или Stop“.
  • Една Shutdown-Phase, која ги „join“-ува работниците (т.е. чека додека не завршат), но со timeout.

Клучното не е дали користиш TThread, omnithreadlibrary или свој пул, туку дека твоите работници не работат „слепо“. Worker-Loop треба структурно да изгледа вака: Чекање на настан(и) → обработка во мали блокови → помеѓу блоковите проверка на Stop → прецизно ослободување на ресурси.

Замка: Terminate само по себе не е доволно

Mногу Delphi-нишки се „прекинуваат“ со Terminate. Но тоа е само еден флаг. Ако нишката е заглавена во блокирачко API, прво ништо не се случува. Затоа сопственото Stop-Event е толку корисно: можеш да го вградиш во повици за чекање и целно да предизвикаш wakeups.

Замка: FreeOnTerminate := True во контекст на сервис

Во сервисите често се гледа FreeOnTerminate := True. Тоа може да функционира, но го отежнува контролирањето на shutdown-от, бидејќи често губиш чиста референца за да почекаш крај на нишката и да протоколираш состојби на грешки. За контролирана логика на стоп е обично постабилно експлицитно да ги поседуваш нишките и во shutdown-от детерминистички да чекате и да ги ослободите.

Блокирачки операции: Како да ги направиш stop-способни

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

Тешкиот дел не е самиот настан, туку местата каде твојот сервис се блокира. Три типични класи:

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

Ако работиш периодично („проверка на секои 10 секунди“), не користи Sleep(10000), туку чекај на настан со таймаут. Тогаш Stop-Event може веднаш да го прекине чекањето. Тоа ја намалува латенцијата при стопирање и спречува впечатокот „Service reagiert nicht“.

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

Ако имаш Producer/Consumer-архитектура (н.пр. задачи се ставаат во ред), ти треба сигнал кој ќе ги разбуди конзументите. Често тоа е уште еден TEvent (Work available). Конзументот потоа чека на две handles: „Work“ или „Stop“. При Stop го сетираш Stop-Event и евентуално и Work-Event, за да се осигураш дека сите конзументи ќе излезат од Wait.

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

При пристапи до база на податоци или HTTP-повици се одлучува дали твојот сервис ќе застане чисто. За оперативност важи: Никаков повик без таймаут. Таймаутот не е луксуз, туку предуслов за управливост. Дополнително треба помеѓу Retries/Backoff-фазите секогаш да проверуваш Stop. Инакo добиваш класичниот случај: „Service stoppt nicht, weil er gerade 10 Retries mit Sleep macht“.

Во некои библиотеки можеш експлицитно да предизвикаш прекини (н.пр. Query-Abbruch). Ако тоа не е можно, барем конфигурирај така што таймаутите ќе бидат доволно кратки за да не го пробијат shutdown-таймаутот.

Stop Pending korrekt: Status, Logging und Erwartungsmanagement

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

Кога сервисот запира, од оперативен агол е важно да се разбере каде застанува. За тоа ти требаат две работи:

  • Лог-маркери во стоп-патот: „Stop angefordert“, „keine neuen Jobs“, „warte auf Worker“, „Worker X beendet“, „Shutdown fertig“.
  • Мерливи времиња: Колку долго трае стопот? Која фаза троши време? Овде често е доволна монотона мерка на време како GetTickCount64 или TStopwatch (монотона = не е изопачена од промени на системското време).

Ако во стоп-патеката запишеш само еден единствен лог-запис „Stopping…“, дебагирањето на терен останува игра на претпоставки. Во сервис-операции логовите често се единственото што го добиваш без интеракција.

Кои логови се навистина корисни во сервиси?

  • Service-PID, време на старт, Version/Build (без прекумерен overhead).
  • Број активни Worker, број in-flight Jobs.
  • Активни надворешни зависности: „DB-Call во тек“, „HTTP-Request во тек“, „Datei-Flush во тек“ (само агрегирано, не секој детаљ).
  • Stop-Timeout достигнат: кои Worker се уште отворени?

Дебагирање на терен: направете го репродуцирачко наместо да гатате

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

Тестирање на сервис под контрола

  • Стоп за време на активна обработка (не во Idle).
  • Стоп за време на надворешен прекин: DB кратко недостапна, HTTP-ендпоинт бавен, Fileshare исчезнат.
  • Стоп веднаш по старт (Race-Conditions: Worker уште во изградба).

Event Viewer и Service Control Manager сигнали

Windows пишува Dienstereignisse, но тие често се груби. Подобро е ако твојот сервис сам пишува во лог-фајл или во Windows Event Log. Важно е: логирањето треба да функционира и во стоп-патеката. Ако го ослободиш логерот во shutdown премногу рано или ако Flush е блокиран, ќе ги изгубиш точно тие критични траги.

Да се направат видливи зависнати нишки

Доколку повторно гледаш „Stop Timeout“, вреди да се проверат состојбите на нишките (на пр., со Debugger/Procdump во тест-окружување). Често ќе најдеш нишка во Wait-состојба на некој Handle што никогаш не се сигнализира, или во мрежен повик без timeout. Поправката ретко е „повеќе Sleep“, туку чист пат за преразвршување/прекинување.

Кога навистина се исплаќа напорот?

Минималистички сервис што има само еден тајмер и нема надворешни зависности понекогаш „едноставно ќе запре“. Но штом еден од следниве услови важи, чистиот Graceful Shutdown речиси секогаш се исплаќа:

  • Сервисот обработува задачи со странични ефекти (запишување датотеки, DB-трансакции, API-повици).
  • Постојат повеќе нишки или пул.
  • Сервисот зависи од мрежни ресурси (DB, REST, Message Broker, Fileshares).
  • Операцијата бара планирани прозорци за одржување (рестартирања, ажурирања, failover).

Вредноста не е „елеганција“, туку оперативна сигурност: помалку груби прекини на процесот, помалку неконсистентни привремени состојби, помалку рачни интервенции.

Практични замки: што често тргнува наопаку при shutdown

1) Stop е поставен, но сепак пристигнуваат нови задачи

Ако прифаќаш влезна работа (на пр. преку Socket, Datei-Trigger, Timer), во стоп-патеката прво мораш да го запреш прифатот на нова работа: затвори Listener-ите, деактивирај Timer-ите, запре Scheduler. Инаку ќе трчаш по крајот затоа што сè уште стартуваат нови Jobs.

2) Cleanup се блокира (Flush, Close, Finalize)

„Само уште брзо да флашнеме сè“ може да биде опасно во сервис-контекст ако целта (мрежен диск, Remote-Log, DB) во моментот висне. Затоа: Cleanup да, но со ограничено време. Ако е потребно, мораш да одлучиш кои податоци ќе ги изгубиш во-memory, наместо да го блокираш целосниот стоп.

3) Локови и редослед

При Stop често пристапуваш до истите структури на податоци како и Worker-ите (Queues, Caches, States). Ако Stop-нитката држи Locks и потоа чека крај на Worker-ите, додека Worker-ите му треба истиот Lock, добиваш Stop-Deadlock. Противмерки: држи кратко време на задржување на Lock-от, во Stop-патот не „чекај под Lock“, дефинирај јасен редослед.

4) Паралелност при двојно повикување на Stop

Во практика Stop може да се активира повеќе пати (на пр. Stop + Shutdown, или Stop се јавува повторно). Твојот Stop-пат треба да биде идемпотентен: поставување на Stop-Event е во ред, но двојната Join/Free-логика мора да биде добро заштитена (на пр. со атомско знаме).

Оперативен поглед: што администраторите и IT-раководителите очекуваат од сервисот

За оперативата и администрацијата на крајот не е важно колку „убав“ е кодот, туку дали сервисот:

  • при Stop надежно завршува (планирано, без замрзнувања),
  • при Stop не создава неконзистентни податоци (на пр. наполовина запишани датотеки, отворени трансакции),
  • во случај на грешка испорачува корисни логови,
  • при прозорци за одржување и Deployments е предвидлив.

Тоа е и причината зошто темата Stop-Timeout не е само „работа за развивачи“: влијае на циклусите на патчување, времињата за опоравување и прашањето дали автоматизираните Deployments воопшто се возможни.

Конкретни насоки за робустен дизајн при исклучување

Ако сакаш пракматично да го стандардизираш прашањето, овие насоки се покажале како проверени:

  • Глобален Stop-Event, Manual Reset, создаден рано во животниот циклус на сервисот, ослободен доцна.
  • Без Sleep во Worker-циклуси без Stop-способна алтернатива (Wait со Timeout).
  • Сите надворешни повици со Timeouts (DB, HTTP, Fileshares). Постави Timeouts така што ќе се вклопуваат во твојот Shutdown-Timeout.
  • Stop-Timeout како конфигурација (на пр. во INI/Registry), за да операцијата може да реагира без потреба за повторна компилација.
  • Модел на фази: прво graceful (текуќи задачи да завршат), потоа опционално „soft abort“ (блокирање на нови чекори), и на крај тврд Exit како последна мерка.
  • Добри Stop-логови со фази и мерење на време.

Финале: TEvent + Stop-Timeout не е луксуз, туку управливост

Заглавен Stop ретко е поединечна грешка – најчесто е архитектонска празнина: работа тече во нишки или блокирачки повици кои не познаваат заеднички Stop-сигнал. Со јасен Stop-Event (TEvent, Manual Reset), stop-способни Waits наместо Sleep, последователни Timeouts за надворешни зависности и дефиниран Shutdown-Timeout ќе добиеш сервис кој е предвидлив во секојдневната работа.

Овој код е особено исплатлив ако твојот сервис работи во продукциски околини со прозорци за одржување, автоматизирани Deployments или критични секундарни ефекти. Тогаш „Graceful Shutdown“ не е козметика, туку градежен елемент за стабилен оперативен тек и помалку ескалации при следното рестартирање.

Ако сакате да го подесите вашиот Stop-пат правилно или да ја проверите постоечка Delphi-услуга за робустна Shutdown-логика и оперативна безбедност, техничкиот Sparrings-Call често е најбрзиот пат до јасни мерки: контактирајте.

За оваа тема важни се и Delphi Windows сервис и Tevent Delphi. Статијата ги поставува овие аспекти на разбирлив начин и покажува на што треба да се обрне внимание во секојдневието.

Разговарајте за проект или иницијатива за модернизација со Net-Base.

Следен чекор

Кога од темата ќе стане реален проект, архитектурата, постојниот систем и експлоатацијата треба рано да се разгледаат заедно.

Не поддржуваме само при поединечни прашања, туку и кога од исечоци од изворен код, legacy-теми или идеи за портали треба да прерасне во робустен корпоративен проект.

  • Постоечката состојба, целната слика и техничките ризици се проценуваат заедно.
  • REST, пристапот до податоци, порталите и Rollout не се одложуваат за подоцнежна фаза.
  • Ќе увидите рано кој пат е економски и оперативно одржлив.

Сподели објава

Споделете го овој пост директно.

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта се достапни веднаш. За Instagram подготвуваме линк и краток текст.

Е-пошта

Instagram се отвора во нов таб. Линкот и краткиот текст претходно се копираат во меѓуспремникот.