Net-Base Списание

02.08.2026

Windows услуга в Delphi: Правилна реализация на Graceful Shutdown с TEvent и Stop-Timeout

Когато Windows-услуга виси при спиране, това рядко е случайно: най-често нишки, I/O или Sleep-цикли блокират без път за прекъсване. Този практически материал показва как в Delphi с TEvent да реализираш чисто контролирано спиране (graceful shutdown) и правилно да обработваш таймаутите за спиране...

02.08.2026

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

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

Един Windows Service in Delphi в ежедневието често изглежда неособено: работи на заден план, обработва задачи, записва логове, разговаря с бази данни или с REST-APIs. Докато някой не кликне „Dienst beenden“ — или не е необходим Patch-Reboot — и услугата не спре чисто. Тогава конзолата на услугите показва „Wird beendet…“ с минути, услугата виси в статуса Stop Pending, а в най-лошия случай процесът бива принудително прекратен. Точно тук си струва да разгледаш Windows Service in Delphi Graceful Shutdown като съзнателна архитектурна тема: с ясен Shutdown-сигнал, дефинирани таймаути и нишки, които действително реагират.

В този материал не говорим за детайли на фреймуърка, а за приложим шаблон: TEvent като Stop-сигнал (ядрено-близък синхронизационен обект от 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, очаквания на файловата система — всичко, което „просто чака“, без да се интересува от Stop-сигнал.
  • Queue-Consumers без wakeup: Worker чака на опашка, но при спиране не се събужда, за да излезе.
  • Поръчка на заключвания/Deadlocks: при спиране се прави „Cleanup“, докато други нишки все още държат заключвания. Това обикновено се случва само в Stop-пътя, защото там поръчката е различна от нормалната работа.

Windows SCM очаква, че една услуга ще отговори бързо на команда за спиране и ще докладва статуса си последователно (чрез SetServiceStatus; Delphi капсулира това в service-компонентата). Ако приемеш Stop-събитието, но нишките ти не се свалят чисто, процесът остава жив — и Windows в един момент решава, че „твърде се бави“. Резултатът е или принудително прекратяване, или услуга, която виси в неясен междинен свят.

Основен принцип: един Stop-сигнал, който всеки Worker разбира

Abstrakte Grafik: Worker-Threads warten auf Work oder Stop-Event und beenden geordnet
Когато Worker-и чакат „работа или стоп“, латентността при спиране намалява без polling.

Един Graceful Shutdown работи само ако имаш сигнал, който:

  • може да бъде наблюдаван от всички релевантни нишки,
  • действа дори при блокиращи състояния на изчакване,
  • в Stop-пътя да е детерминистичен (без „може би някога ще излезе“-надежда),
  • има ясна стратегия за таймаут.

В Delphi TEvent е много подходящ инструмент за това: Event-обект, който вътрешно е реализиран чрез Windows-Handles (сравнимо с CreateEvent/SetEvent). Можеш да го използваш като сигнал „Stop requested“. Всеки Worker тогава не чака заслепено, а чака „за работа или за Stop“.

TEvent richtig wählen: ManualReset vs. AutoReset

При Stop-сигнали обикновено искаш Manual Reset (възможно за ръчно нулиране): веднъж зададено, Event-ът остава „signaled“, докато не го нулираш. Това гарантира, че всеки Thread, който по-късно влезе в изчакваща фаза, ще разпознае Stop-сигнала. Auto Reset би било рисковано тук, защото автоматично нулира сигнала след един чакащ Thread и други Thread-ове може да изпуснат Stop-сигнала.

Delphi-Service-Lebenszyklus: Wo Stop wirklich ankommt

Един Delphi-Windows- und Linux-Services обикновено се базира на TService (VCL/RTL). SCM изпраща команди (Start, Stop, Pause, Continue). Delphi след това извиква съответни събития/методи (в зависимост от шаблона, напр. OnStart, OnStop, OnExecute).

Важно за архитектурата:

  • OnStop не е място за продължително чакане без обновяване на статуса. Това е мястото, където инициираш Shutdown и след това контролирано изчакваш – с таймаут.
  • OnExecute често е цикъл. Ако там работиш „безкрайно“, цикълът трябва да реагира на Stop-сигнал.
  • Worker-Threads (TThread или Thread-Pools) трябва да реагират на същия Stop-сигнал, иначе сервисът е логически спрян, но физически все още не е завършен.

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

Практически приложимият модел се състои от четири стъпки:

  1. Искане на Stop: задаване на Stop-Event, повече не се приемат нови задачи.
  2. Иницииране на wakeups: ако Worker чакат в опашки или са в Sleep, те трябва да могат да „събудят“ (напр. чрез Event/Queue-сигнал).
  3. Подредено завършване: Worker-ите прекратяват цикли, затварят ресурси (DB-връзки, файлове, Handles) и съобщават „fertig“.
  4. Таймаут и резервен механизъм: ако не всичко приключи навреме, трябва да вземеш решение: да чакаш по-нататък (с обновление на статуса) или да прекратиш контролирано/твърдо (в зависимост от риска).

Същината е: нито един Thread не бива да чака единствено по време (Sleep) или да блокира единствено върху I/O, без паралелно да проверява Stop-сигнал. Вместо това използваш функции за изчакване, които взимат предвид няколко сигнала (напр. „Stop-Event oder Work-Event“), или капсулираш I/O в таймаути плюс проверки за Stop.

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

Тук в проектите се появяват повечето недоразумения. Съществуват две различни нива на таймаути:

  • SCM-Erwartung: Windows erwartet, dass du im Status SERVICE_STOP_PENDING regelmäßig Fortschritt meldest. Sonst wirkt es so, als hängst du. Delphi kümmert sich teilweise darum, aber sobald du selbst länger blockierst, brauchst du eine Strategie, wie du weiter Status-Updates ermöglichst (oder wie du deine Stop-Phase kurz hältst).
  • Dein eigener Shutdown-Timeout: Du definierst z. B. „Wir geben uns 20 Sekunden, um laufende Jobs sauber abzuschließen, dann brechen wir ab.“ Das ist ein Architekturentscheid: Datenkonsistenz vs. Reboot-Zwang vs. Betriebsvorgaben.

Практически това означава: Сервисът ти трябва бързо да влезе в състояние, в което той вече не стартира нови единици работа, и след това само чака текущата работа да приключи – но не безкрайно. И тази фаза на чакане трябва да работи на малки интервали, за да можеш да реагираш и при нужда да логваш.

Колко дълго може да трае спирането?

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

Implementierung mit TEvent: Aufbau, der im Betrieb stabil bleibt

Един утвърден подход в Delphi-сервиса изглежда така (без да се влизат детайлите на Framework-ите):

  • Едно Stop-Event (TEvent, Manual Reset), което се задава при Stop.
  • Ein oder mehrere Worker-Threads, die in ihrer Hauptschleife regelmäßig auf Stop prüfen.
  • Optional ein Work-Event oder eine Queue, die Arbeit signalisiert. Worker warten dann auf „Work oder Stop“.
  • Eine Shutdown-Phase, die Worker „joined“ (also wartet, bis sie beendet sind), aber mit Timeout.

Важното не е дали използваш TThread, omnithreadlibrary или собствен пул, а че твоите Worker не работят „на сляпо“. Ein Worker-Loop sollte strukturell so aussehen: Warten auf Ereignis(e) → Arbeit in kleinen Chunks → zwischen Chunks Stop prüfen → Ressourcen sauber freigeben.

Fallstrick: Terminate allein reicht nicht

Много Delphi-Threads werden mit Terminate „abgebrochen“. Das ist aber nur ein Flag. Wenn der Thread gerade in einer blockierenden API steckt, passiert erst einmal nichts. Darum ist ein eigenes Stop-Event so hilfreich: Du kannst es in Warteaufrufe integrieren und gezielt Wakeups auslösen.

Fallstrick: FreeOnTerminate im Service-Kontext

В Services sieht man oft FreeOnTerminate := True. Това може да функционира, но прави Shutdown по-труден за контрол, защото често вече нямаш чиста референция, за да изчакаш края на нишката и да регистрираш грешки. За контролирана Stop-логика обикновено е по-стабилно да притежаваш нишките експлицитно и при Shutdown детерминистично да чакаш и да ги освобождаваш.

Blockierende Operationen: So machst du sie stop-fähig

Сцена за отстраняване на проблеми: мрежова връзка като причина за блокиращи повиквания и таймаути
Блокиращо I/O без таймаут е най-честата причина за задържане при спиране на услугата.

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

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

Ако работиш периодично („проверка на всеки 10 секунди“), не използвай Sleep(10000), а изчакай събитие с таймаут. Тогава Stop-Event може да прекрати изчакването незабавно. Това намалява латентността при спиране и предотвратява усещането „услугата не отговаря“.

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

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

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

При достъп до база данни или HTTP повиквания се решава дали услугата ти ще спре чисто. За експлоатацията важи: Никакво повикване без таймаут. Таймаутът не е лукс, а предпоставка за управляемост. Освен това между фазите с опити/Backoff винаги трябва да проверяваш Stop. В противен случай получаваш класическия случай: „Услугата не спира, защото в момента прави 10 Retries със Sleep“.

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

Stop Pending korrekt: Status, Logging und Erwartungsmanagement

Измерване на време и анализ на логове за диагностика на Stop-таймаути при Windows-услуги
Фазови логове и измерване на време правят Stop-таймаутите възпроизведими и обясними.

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

  • Log-Marker в стоп-пътя: „Заявено спиране“, „Няма нови задачи“, „Изчакване на Worker“, „Worker X приключи“, „Прекратяване завършено“.
  • Измерими времена: Колко дълго продължава спирането? Коя фаза изяжда времето? Често е достатъчно монотонно времево измерване като GetTickCount64 или TStopwatch (монотонно = не е изкривено от промени в системното време).

Ако в пътеката за спиране запишеш само един лог запис „Stopping…“, дебъгването на място остава игра на догадки. При работа на услуга логовете често са единственото, което получаваш без взаимодействие.

Кои логове в услугите са наистина полезни?

  • PID на услугата, време на стартиране, версия/билд (без ненужно допълнително натоварване).
  • Брой активни worker-и, брой текущо изпълнявани задачи.
  • Активни външни зависимости: „DB-Call в ход“, „HTTP-Request в ход“, „запис към файл (flush) в ход“ (само агрегирано, не всяка подробност).
  • Stop-timeout достигнат: кои worker-и все още са активни?

Дебъгване на място: направете го възпроизводимо, вместо да гадаете

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

Тествайте услугата под контрол

  • Спиране по време на активна обработка (не в празен ход).
  • Спиране при външна повреда: DB временно недостъпна, HTTP крайна точка бавна, файлов дял недостъпен.
  • Спиране непосредствено след стартиране (race conditions: worker-и все още в процес на инициализация).

Event Viewer und Service Control Manager Signale

Windows записва събития на услугата, но те често са груби. По-добре е, ако самата ти услуга пише в лог файл или Windows Event Log. Важно е следното: логването трябва да работи още в пътеката за спиране. Ако освободиш логъра прекалено рано по време на спиране или ако записването (flush) се блокира, ще загубиш точно тези решаващи следи.

Направете висящите нишки видими

Ако повтарящо виждаш „Stop Timeout“, струва си да погледнеш състоянията на нишките (напр. чрез Debugger/Procdump в тестова среда). Често намираш нишка в състояние на изчакване върху handle, което никога не се сигнализира, или в мрежово повикване без timeout. Решението рядко е „още малко sleep“, а по-скоро чист път за прекъсване.

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

Една минималистична услуга, която има само таймер и няма външни зависимости, понякога може „просто да спре“. Но щом има поне едно от следните, почти винаги си заслужава чист Graceful Shutdown:

  • Услугата обработва Jobs mit Seiteneffekten (запис на файлове, DB-транзакции, API-извиквания).
  • Има mehrere Threads или пул.
  • Услугата зависи от Netzwerkressourcen (DB, REST, Message Broker, файлови дялове).
  • Оперативната поддръжка изисква planbare Wartungsfenster (рестарти, ъпдейти, Failover).

Ползата не е „елегантност“, а експлоатационна устойчивост: по-малко груби прекъсвания на процеси, по-малко неконсистентни междинни състояния, по-малко ръчни намеси.

Практически капани: какво често се обърква при спиране

1) Заявено е спиране, но въпреки това постъпват нови задачи

Ако приемаш входяща работа (напр. през сокет, файл-тригър, таймер), трябва в пътеката за спиране първо да спреш приема на нова работа: затвори listener-а, деактивирай таймерите, спри scheduler-а. В противен случай ще преследваш края, тъй като все още се стартират нови задачи.

2) Почистване блокира (Flush, Close, Finalize)

„Само още бързо да изпразня всичко“ може да е опасно в контекста на услуга, ако целта (мрежово устройство, отдалечен лог, DB) в момента зацикля. Затова: почистване да, но с ограничено време. При нужда трябва да решиш кои данни ще загубиш в паметта, вместо да блокираш целия стоп.

3) Заключвания и ред на операциите

Beim Stop greifst du häufig auf dieselben Datenstrukturen zu wie die Worker (Queues, Caches, States). Wenn der Stop-Thread Locks hält und dann auf Worker-Ende wartet, während Worker denselben Lock brauchen, hast du einen Stop-Deadlock. Gegenmittel: Lock-Hold-Zeiten klein halten, im Stop-Pfad nicht „unter Lock warten“, klare Reihenfolge definieren.

4) Nebenläufigkeit beim doppelten Stop

In der Praxis kann Stop mehrfach getriggert werden (z. B. Stop + Shutdown, oder Stop kommt erneut). Dein Stop-Pfad sollte idempotent sein: Stop-Event setzen ist okay, aber doppelte Join/Free-Logik muss sauber geschützt werden (z. B. über ein atomaren Flag).

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

Für Betrieb und Administration zählt am Ende nicht, wie „schön“ der Code ist, sondern ob der Dienst:

  • bei Stop verlässlich endet (planbar, ohne Hänger),
  • bei Stop keine inkonsistenten Daten produziert (z. B. halbe Dateien, offene Transaktionen),
  • im Fehlerfall brauchbare Logs liefert,
  • bei Wartungsfenstern und Deployments berechenbar ist.

Das ist auch der Grund, warum das Thema Stop-Timeout nicht nur „Entwicklerkram“ ist: Es beeinflusst Patchzyklen, Recovery-Zeiten und die Frage, ob automatisierte Deployments überhaupt möglich sind.

Konkrete Leitplanken für ein robustes Shutdown-Design

Wenn du das Thema pragmatisch standardisieren willst, haben sich diese Leitplanken bewährt:

  • Ein globales Stop-Event, Manual Reset, früh im Service-Lebenszyklus erstellt, spät freigegeben.
  • Kein Sleep in Worker-Loops ohne Stop-fähige Alternative (Wait mit Timeout).
  • Alle externen Calls mit Timeouts (DB, HTTP, Fileshares). Timeouts so wählen, dass sie in deinen Shutdown-Timeout passen.
  • Stop-Timeout als Konfiguration (z. B. in INI/Registry), damit Betrieb reagieren kann, ohne neu zu kompilieren.
  • Stufenmodell: Erst graceful (laufende Jobs zu Ende), dann optional „soft abort“ (keine neuen Schritte), dann harter Exit als letzter Ausweg.
  • Gute Stop-Logs mit Phasen und Zeitmessung.

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

Ein hängender Stop ist selten ein Einzelfehler – meist ist es ein Architekturloch: Arbeit läuft in Threads oder blockierenden Calls, die kein gemeinsames Stop-Signal kennen. Mit einem klaren Stop-Event (TEvent, Manual Reset), stop-fähigen Waits statt Sleep, konsequenten Timeouts für externe Abhängigkeiten und einem definierten Shutdown-Timeout bekommst du einen Service, der im Alltag berechenbar ist.

Der Code lohnt sich besonders, wenn dein Service in Produktionsumgebungen mit Wartungsfenstern, automatisierten Deployments oder kritischen Seiteneffekten arbeitet. Dann ist „Graceful Shutdown“ nicht Kosmetik, sondern ein Baustein für stabilen Betrieb und weniger Eskalationen beim nächsten Reboot.

Wenn du euren Stop-Pfad einmal sauber aufsetzen oder einen bestehenden Delphi-Service auf robuste Shutdown-Logik und Betriebssicherheit überprüfen willst, ist ein technischer Sparrings-Call oft der schnellste Weg zu klaren Maßnahmen: Kontakt aufnehmen.

Für dieses Thema sind auch Delphi Windows Service und Tevent Delphi wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Следваща стъпка

Когато темата прерасне в реален проект, архитектурата, съществуващите активи и експлоатацията трябва да се разглеждат заедно още в ранния етап.

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

  • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
  • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последващи задачи.
  • Вие виждате навреме кой път е икономически и оперативно жизнеспособен.

Сподели публикацията

Споделете тази публикация директно

LinkedIn, X, XING, Facebook, WhatsApp и електронна поща са налични веднага. За Instagram подготвяме директно връзка и кратък текст.

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

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