Net-Base Журнал

02.08.2026

Сервис Windows в Delphi: корректная реализация Graceful Shutdown с использованием TEvent и Stop-Timeout

Если Windows-сервис зависает при остановке, это редко случайность: чаще всего блокируются потоки, операции ввода‑вывода или циклы Sleep без пути для прерывания. В этом практическом материале показано, как в Delphi с помощью TEvent реализовать корректное плавное завершение работы, правильно обрабатывать таймауты остановки...

02.08.2026

От темы в журнале к проектной практике

Соответствующие страницы услуг и технологий к статье

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

В этой статье речь не о внутренностях фреймворков, а о практичном шаблоне: TEvent как сигнал остановки (ядросвязанное синхронизирующее объект из 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 ohne Wakeup: рабочий поток ожидает элементы в очереди, но при остановке его не пробуждают, чтобы он мог завершиться.
  • Порядок блокировок / deadlocks: при остановке выполняется «Cleanup», в то время как другие потоки всё ещё удерживают блокировки. Это часто проявляется только на пути остановки, потому что порядок там отличается от нормальной работы.

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

Grundprinzip: Ein Stop-Signal, das jeder Worker versteht

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

Ein Graceful Shutdown funktioniert nur, wenn du ein Signal hast, das:

  • von allen relevanten Threads beobachtet werden kann,
  • даже при наличии блокирующих состояний ожидания оказывает действие,
  • в пути остановки детерминированно срабатывает (никакой надежды «может, он когда-нибудь выйдет»),
  • имеет ясную стратегию таймаутов.

В Delphi для этого очень пригоден TEvent: объект события, который внутри реализован через Windows-Handles (аналогично CreateEvent/SetEvent). Его можно использовать как «Stop requested»-сигнал. Каждый воркер тогда не просто слепо ждёт, а ожидает «работу или остановку».

TEvent richtig wählen: ManualReset vs. AutoReset

Для сигналов остановки обычно нужен Manual Reset (ручной сброс): один раз установленный, Event остаётся «signaled», пока вы его не сбросите. Это гарантирует, что любой поток, который позже войдёт в фазу ожидания, всё равно обнаружит сигнал остановки. Auto Reset в этом случае опасен, поскольку он автоматически сбрасывает сигнал после одного ожидающего потока, и другие потоки могут пропустить сигнал остановки.

Delphi-Service-Lebenszyklus: куда действительно доходит сигнал Stop

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

Важно для архитектуры:

  • OnStop — не место для длительного ожидания без обновлений статуса. Это место, где вы инициируете завершение и затем контролируемо ждёте — с таймаутом.
  • OnExecute часто представляет собой цикл. Если вы там работаете «бесконечно», цикл должен реагировать на сигнал Stop.
  • Worker-Threads (TThread или пул потоков) должны реагировать на тот же сигнал Stop, иначе сервис логически остановлен, но физически ещё не завершён.

Чистая схема: Stop-Event + Join воркеров + жёсткий запасной вариант

Практическая схема состоит из четырёх шагов:

  1. Запрос остановки: установить Stop-Event, не принимать новых задач.
  2. Вызвать пробуждение: если воркеры ожидают в очередях или в Sleep, они должны иметь возможность «проснуться» (например, через Event/Queue-Signal).
  3. Упорядоченное завершение: воркеры завершают свои циклы, закрывают ресурсы (DB‑подключения, файлы, Handles) и сообщают «готово».
  4. Таймаут и запасной план: если не всё завершается вовремя, нужно принять решение: продолжать ждать (с обновлением статуса) или контролируемо прервать/жёстко завершить (в зависимости от риска).

Суть в том, что ни один поток не должен исключительно ожидать по таймеру (Sleep) или исключительно блокироваться на I/O без параллельной проверки Stop-сигнала. Вместо этого используйте функции ожидания, которые учитывают несколько сигналов (например, «Stop-Event или Work-Event»), либо инкапсулируйте I/O в таймауты с периодическими проверками Stop.

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

Здесь в проектах происходит большинство недопониманий. Существуют два уровня таймаутов:

  • Требование SCM: Windows ожидает, что вы в статусе SERVICE_STOP_PENDING будете регулярно сообщать о ходе выполнения. Иначе создаётся впечатление зависания. Delphi частично этим занимается, но как только вы сами надолго блокируетесь, вам нужна стратегия, как продолжать отправлять обновления статуса (или как держать фазу остановки короткой).
  • Ваш собственный таймаут завершения: вы задаёте, например, «Мы даём себе 20 секунд на корректное завершение запущенных задач, затем прерываемся.» Это архитектурное решение: целостность данных vs принудительная перезагрузка vs эксплуатационные требования.

На практике это означает: ваш сервис должен быстро перейти в состояние, в котором он не запускает новых единиц работы, а затем только ожидает завершения текущей работы — но не бесконечно. И эта фаза ожидания должна выполняться малыми интервалами, чтобы вы могли реагировать и при необходимости логировать.

Как долго может длиться остановка?

Универсального числа не существует. Для многих бизнес‑сервисов реалистичным целевым диапазоном является 5–30 секунд: достаточно времени для «in-flight» данных, но достаточно коротко для окон патчей. Если вам регулярно нужно больше времени, это часто указывает на то, что вы обрабатываете слишком крупные единицы за раз или что внешние зависимости (DB/HTTP) работают без таймаута.

Реализация с TEvent: структура, остающаяся стабильной в эксплуатации

Проверенная структура в сервисе Delphi выглядит так (без углубления в детали фреймворка):

  • Событие Stop-Event (TEvent, Manual Reset), которое устанавливается при остановке.
  • Один или несколько Worker-Threads, которые в своём основном цикле регулярно проверяют Stop.
  • Опционально Work-Event или очередь, сигнализирующая о работе. Воркеры тогда ожидают «Work или Stop».
  • Фаза Shutdown-Phase, которая выполняет «join» воркеров (то есть ждёт их завершения), но с таймаутом.

Ключевое не в том, используете ли вы TThread, omnithreadlibrary или собственный пул, а в том, чтобы ваши воркеры не работали «вслепую». Цикл воркера структурно должен выглядеть так: ожидание события(ий) → работа малыми кусками → проверка 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), а ждите события с таймаутом. Тогда ваше Stop-Event сможет немедленно прервать ожидание. Это уменьшает задержку при остановке и исключает ощущение «сервис не отвечает».

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

Если у вас архитектура Producer/Consumer (например, задачи помещаются в очередь), нужен сигнал для пробуждения consumer’ов. Часто это ещё одно TEvent (Work available). Потребитель затем ждёт два Handles: «Work» или «Stop». При остановке вы устанавливаете Stop-Event и, при необходимости, также Work-Event, чтобы все consumer’ы гарантированно вышли из ожидания.

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

При обращениях к базе данных или HTTP-вызовах определяется, сможет ли сервис корректно остановиться. Для эксплуатации правило: ни один вызов без таймаута. Таймаут — не роскошь, а необходимое условие управляемости. Дополнительно между фазами повторных попыток/backoff вы всегда должны проверять Stop. Иначе классическая ситуация: «сервис не останавливается, потому что он выполняет 10 повторных попыток с Sleep».

В некоторых библиотеках можно явно инициировать отмену (например, прерывание запроса). Если это невозможно, по крайней мере настройте таймауты так, чтобы они были достаточно короткими и не превышали отведённый shutdown-таймаут.

Stop Pending korrekt: Status, Logging und Erwartungsmanagement

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

Когда сервис останавливается, с точки зрения эксплуатации важно понять, где он зависает. Для этого вам нужны две вещи:

  • Маркеры логов в пути остановки: «Запрос на остановку», «новых задач нет», «ожидание Worker’ов», «Worker X завершён», «завершение shutdown».
  • Замеры времени: Сколько длится остановка? Какая фаза «съедает» время? Часто достаточно монотонной метрики времени, например GetTickCount64 или TStopwatch (монотонная = не искажённая изменениями системного времени).

Если в пути остановки вы записываете в лог только одну запись «Stopping…», отладка в полевых условиях превращается в игру в догадки. В сервисной эксплуатации логи часто — единственное, что вы получаете без взаимодействия.

Какие логи действительно полезны в сервисах?

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

Отладка в полевых условиях: делать воспроизводимой, а не гадать

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

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

  • Остановка во время активной обработки (не в простое).
  • Остановка при внешней неисправности: БД кратковременно недоступна, HTTP-эндпоинт медленный, файловый шар недоступен.
  • Остановка сразу после старта (гонки условий: воркеры ещё в процессе инициализации).

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

Windows записывает события службы, но они часто грубые. Лучше, если ваш сервис сам пишет в лог-файл или в Windows Event Log. Важно: логирование должно работать в пути остановки. Если вы освобождаете логгер слишком рано при завершении или Flush блокируется, вы потеряете именно те решающие следы.

Обнаружение зависших потоков

Если вы регулярно видите «Stop Timeout», стоит посмотреть состояния потоков (например, через дебаггер/Procdump в тестовой среде). Часто вы обнаружите поток в состоянии ожидания на дескрипторе, который никогда не сигнализируется, или в сетевом вызове без таймаута. Решение редко заключается в «ещё больше Sleep», чаще требуется корректный путь прерывания.

Когда усилия действительно оправданы?

Минималистичный сервис, у которого только таймер и нет внешних зависимостей, иногда может «просто остановиться». Но как только выполняется хотя бы одно из перечисленного, аккуратный graceful shutdown почти всегда оправдан:

  • Сервис обрабатывает задачи с побочными эффектами (запись файлов, транзакции в БД, вызовы API).
  • Используется несколько потоков или пул.
  • Сервис зависит от сетевых ресурсов (БД, REST, брокер сообщений, файловые шар(ы)).
  • Операционная команда требует планируемые окна обслуживания (перезагрузки, обновления, failover).

Польза не в «элегантности», а в надёжности эксплуатации: меньше жестких принудительных завершений процессов, меньше неконсистентных промежуточных состояний, меньше ручных вмешательств.

Практические подводные камни: что часто идёт не так при shutdown

1) Сигнал остановки установлен, но новые задачи всё равно поступают

Если вы принимаете входящую работу (например, по сокету, триггеру файла, таймеру), в пути остановки вы сначала должны прекратить приём новой работы: закрыть слушатель, отключить таймер, остановить планировщик. Иначе вы будете догонять конец, потому что всё ещё будут запускаться новые задачи.

2) Очистка блокируется (Flush, Close, Finalize)

«Ещё быстро всё flushen» может быть опасно в контексте сервиса, если цель (сетевой диск, удалённый лог, БД) в данный момент зависает. Поэтому: очистка — да, но с ограничением по времени. В крайнем случае вы должны решить, какие данные можете потерять в памяти, вместо того чтобы блокировать полный стоп.

3) Блокировки и порядок

При остановке вы часто обращаетесь к тем же структурам данных, что и Worker (Queues, Caches, States). Если Stop-Thread держит блокировки и затем ждёт завершения Worker-ов, в то время как Worker-ы требуют ту же блокировку, возникает deadlock при остановке. Контрмеры: минимизировать время удержания блокировок, в пути остановки не ожидать «под блокировкой», задать чёткий порядок.

4) Nebenläufigkeit beim doppelten Stop

На практике Stop может срабатывать несколько раз (например, Stop + Shutdown, или Stop приходит повторно). Путь остановки должен быть идемпотентным: установить Stop-Event допустимо, но логика двойного Join/Free должна быть надёжно защищена (например, с помощью атомарного флага).

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

Для эксплуатации и администрирования в конце важен не «красивый» код, а то, выполняет ли сервис:

  • bei Stop verlässlich endet (предсказуемо, без зависаний),
  • bei Stop keine inkonsistenten Daten produziert (например, половинчатые файлы, открытые транзакции),
  • im Fehlerfall brauchbare Logs liefert,
  • bei Wartungsfenstern und Deployments berechenbar ist.

Именно поэтому тема Stop-Timeout — не просто «разработческая тема»: она влияет на циклы патчей, времена восстановления и на вопрос, возможны ли вообще автоматизированные деплойменты.

Konkrete Leitplanken für ein robustes Shutdown-Design

Если вы хотите прагматично стандартизировать этот аспект, зарекомендовали себя следующие ориентиры:

  • Ein globales Stop-Event, Manual Reset, создаётся рано в жизненном цикле сервиса, освобождается поздно.
  • Kein Sleep в циклах Worker без альтернативы, поддерживающей Stop (Wait с таймаутом).
  • Alle externen Calls mit Timeouts (DB, HTTP, Fileshares). Подбирайте таймауты так, чтобы они вписывались в ваш Shutdown-таймаут.
  • Stop-Timeout als Konfiguration (например, в INI/Registry), чтобы эксплуатация могла реагировать без перекомпиляции.
  • Stufenmodell: сначала плавное завершение (довести до конца выполняющиеся задания), затем опционально «soft abort» (не запускать новые шаги), затем жёсткий выход как крайняя мера.
  • Gute Stop-Logs с фазами и измерением времени.

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

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

Этот подход особенно оправдан, если ваш сервис работает в продакшне с окнами обслуживания, автоматизированными деплойментами или критичными побочными эффектами. Тогда «плавное завершение» — не косметика, а элемент стабильной эксплуатации и уменьшения числа эскалаций при следующем перезагрузке.

Если вы хотите корректно настроить путь остановки или проверить существующий Delphi-сервис на предмет надёжной логики завершения и эксплуатационной безопасности, технический консультационный звонок часто самый быстрый путь к конкретным мерам: свяжитесь.

Для этой темы также важны Delphi Windows Service и Tevent Delphi. Статья упорядочивает эти аспекты понятным образом и показывает, на что обращать внимание в повседневной работе.

Обсудить проект или модернизационное задание с Net-Base.

Следующий шаг

Если из темы становится реальный проект, архитектуру, существующее состояние и эксплуатацию следует рассматривать совместно на ранней стадии.

Мы поддерживаем не только при отдельных вопросах, но и тогда, когда из фрагментов исходного кода, унаследованных проблем или идей портала должен сформироваться надёжный корпоративный проект.

  • Текущее состояние, целевое состояние и технические риски оцениваются совместно.
  • REST, доступ к данным, порталы и развертывание не переносятся на более поздние этапы.
  • Вы заранее видите, какой путь экономически и операционно жизнеспособен.

Поделиться записью

Поделиться этой записью напрямую

LinkedIn, X, XING, Facebook, WhatsApp и электронная почта доступны немедленно. Для Instagram мы готовим ссылку и краткий текст.

Электронная почта

Instagram открывается в новой вкладке. Ссылка и короткий текст предварительно копируются в буфер обмена.