Вопросы, связанные с взаимодействием потоков
октября 12 2009 by admin in СлужбыПисать службы трудно, поскольку, хотя HandlerEx, получающая запросы на выполнение операций, исполняется основным потоком, реальные действия по обработке этих запросов зачастую выполняются потоком службы. Допустим, вам нужно написать службу для обработки клиентских запросов, поступающих через именованный канал. Поток вашей службы ожидает подсоединения клиента. Если же ваш поток HandlerEx получает код SERVICE_CONTROL_STOR как остановить службу? Многие разработчики просто вызывают TerminateThread из HandlerEx, принудительно уничтожая поток службы. Вы уже должны знать, что TerminateThread — пожалуй, одна из худших функций, которую можно вызвать, так как у потока не остается шансов освободить ресурсы. При ее вызове не уничтожается стек потока, поток не может освободить никакие объекты ядра, которые он, возможно, ожидает, DLL не узнает об уничтожении потока и т. д. Правильный способ останова службы — как-то вывести ее из состояния ожидания, после чего она должна увидеть, что ее хотят остановить, корректно освободить ресурсы и вернуть управление из функции ServiceMain. Чтобы служба выполнялась подобным образом, вам следует реализовать в том или ином виде межпотоковое взаимодействие между вашими функциями HandlerEx и ServiceMain. Вы можете реализовать любой механизм межпотокового взаимодействия: очереди АРС, сокеты или оконные сообщения. Я всегда применяю порты завершения ввода-вывода.
Чтобы обновлять свое текущее состояние, служба должна периодически вызывать SetServiceStatus. Оповещения о состоянии — еще один сложный аспект кодирования служб. Разработчики служб часто дискутируют о том, где помещать вызов SetServiceStatus. Вот некоторые возможные варианты.
HandlerEx вначале обращается к SetServiceStatus, чтобы оповестить о выполняемой операции, а затем через межпотоковое взаимодействие передает соответствующий управляющий код потоку ServiceMain. Последний, выполнив необходимую работу, через межпотоковое взаимодействие сообщает функции HandlerEx о завершении операции. Теперь HandlerEx снова вызывает SetServiceStatus, оповещая о horom состоянии службы.
HandlerEx через межпотоковое взаимодействие передает код потоку Service-Main. Тот выполняет начальное обращение к SetServiceStatus, оповещая о выполняемой операции, делает необходимую работу, а затем снова вызывает SetServiceStatus, оповещая о новом состоянии службы.
HandlerEx выполняет начальное обращение к SetServiceStatus, чтобы оповестить о выполняемой операции, а затем через межпотоковое взаимодействие передает код потоку ServiceMain. Тот выполняет необходимую работу и вызывает SetServiceStatus, оповещая о новом состоянии службы.
У каждого сценария свои достоинства и недостатки. Я долго экспериментировал со всеми этими возможностями и могу с полной уверенностью рекомендовать последний вариант. Вот мои аргументы.
Прежде всего, функцию управления службой вызывает SCP, a SCM передает службе соответствующий управляющий код. После этого SCP начинает ждать, когда служба вызовет SetServiceStatus, указывая, что служба получила этот код. Если HandlerEx службы не выполнится в течение 30 секунд, SCM выводит SCP из состояния ожидания и функция SCP, управляющая службой, сообщает о неудачном завершении.
Кроме того, HandlerEx выполняется основным потоком службы. (Все службы в одном процессе имеют свои функции HandlerEx, выполняемые основным потоком.) Если HandlerEx не возвращает управления, ожидая завершения потока ServiceMain, другие службы в этом процессе не смогут получать запросов операций и уведомлений. В результате будет казаться, что все остальные службы не отвечают, что недопустимо (по-моему).
Так что я предпочитаю третий метод: HandlerEx выполняет начальное обращение к SetServiceStatus, межпотоковое взаимодействие используется для передачи кода потоку ServiceMain, который выполняет необходимые действия и обращается к SetServiceStatus, оповещая о новом состоянии. Однако здесь есть одна проблема: существует потенциальная опасность состояния гонок. Представьте, что HandlerEx службы получает код SERVTCE_CONTROL_PAUSE, отвечает кодом SERVICE_PAUSE_PENDING, а затем передает полученный код потоку ServiceMain. Тот начинает его обработку, как вдруг поток HandlerEx прерывает работ)' потока Sen'iceMain и получает управляющий код SERVICE_CONT-ROL_STOP. Теперь HancUerEx возвращает значение SERVICE_STOP PENDING и выдает новый код поток)' Sen'iceMain. Получив вновь процессорное время, поток ServiceMain заканчивает обработку кода SERVICE_CONTROL_PAUSE и выдает оповещение SERVICE^PAUSED. Затем он обнаруживает выданный ему на обработку код SERVICE_CONTROL_STOP, останавливает службу и оповещает о состоянии SERVICE_STOPPED. В конечном счете SCM получает последовательность обновлений:
SERVICE_PAUSE_PENDING SERVICE_STOP_PENDING SERVICE_PAUSED SERVICE_STOPPED
Эта последовательность совершенно непонятна, и администратор будет поставлен в тупик. А между тем служба прекрасно работает. Вы бы удивились, узнав, сколько служб, выдающих такую последовательность, мне приходилось видеть. Их разработчики никогда не решают эту проблему, поскольку весьма маловероятно, что администратор будет выдавать запросы операций так часто, но это может произойти! Для решения этой проблемы следует применить какой-либо механизм синхронизации потоков. В демонстрационном приложении-службе TimeService в конце этой главы есть класс C++ CGate, позволяющий эффективно решить эту проблему.
Начиная работать со службами, я думал, что за предотвращение состояния гонок отвечает SCM. Но мои эксперименты показали, что во время посылки управляющих кодов SCM бездействует. SCM даже ничего не делает, чтобы гарантировать нормальную доставку службе управляющего кода. Я имею в виду следующее: попробуйте послать приостановленной службе код SERVICE_CONT-ROL_PAUSE. Вам не удастся сделать это через модульный компонент Services, поскольку он, видя, что служба приостановлена, отключает кнопку Pause. А вот с утилитой SC.exe ничто не помешает вам послать код приостановки уже приостановленной службе. Я ожидал, что SCM вернет ошибку утилите SC.exe, но SCM просто вызвал функцию HandlerF.x службы, передав ей код SERVICE_CONT-ROL_PAUSE. Ваши службы должны корректно обрабатывать такие ложные управляющие коды.
Я видел много служб, не учитывающих возможность посылки одного кода несколько раз подряд. Скажем, я знаю службу, закрывающую описатель именованного канала при ее приостановке. Продолжая работу, она создает другой объект ядра, случайно получая для пего то же значение описателя, которое имел изначальный именованный канал. Затем служба получаст другой управляющий код приостановки и вызывает CloseHandle, передавая ей значение описателя старого канала. Поскольку это значение оказывается тем же, что описатель другого объекта ядра, этот объект уничтожается, и дальше служба начинает работать непостижимым образом! Я уж не говорю, сколько удовольствия доставляет разбираться с помощью отладчика во всей этой путанице.
Чтобы решить проблем)' многократного останова, приостановки и продолжения службы, прежде всего проверьте, не находится ли уже ваша служба в желаемом состоянии. Если это так, не вызывайте SetServiceStatus и не выполняйте ту часть программы, что изменяет состояние, — просто верните управление.
Я заметил некоторые особенности поведения служб. Получив код SERVI-CE_CONTROL_PAUSE, HandlcrEx вызывает SetServiceStatus оповещая о состоянии SERVICEJPAUSE_PENDING, вызывает SuspendThread, чтобы «приостановить» поток службы, а затем снова вызывает SetServiceStatus, чтобы сообщить о состоянии SERVICE_PAUSED. Такая последовательность вызовов предотвращает состояние гонок, поскольку все делается в одном потоке, но что делается? Означает ли перевод потока в состояние ожидания приостановку службы? Ладно, полагаю, что на этот вопрос можно ответить утвердительно. Но все же, что значит «приостановить службу»? Ответ зависит от службы.
Если я пишу службу, обрабатывающую клиентские запросы в сети, для меня приостановка будет означать, что я прекращаю воспринимать новые запросы. Но как быть с запросом, который я уже обрабатываю? Возможно, мне следует закончить его обработку, чтобы мой клиент не завис навсегда. Если моя функция HandlerEx просто вызовет SuspendThread, поток службы может быть на любой стадии выполнения. Может быть, поток будет находиться внутри функции malloc, пытаясь выделить некоторую память. Если другая служба, работающая в том же процессе, тоже вызовет malloc, она тоже будет «подвешена», так как доступ к динамически выделяемой памяти осуществляется последовательно. Это именно то, чего бы нам так не хотелось!
Ах да! А как насчет такой вещи: считаете ли вы, что можно позволить остановить приостановленную службу? Я считаю, что можно, да и Microsoft, видимо, тоже, раз они позволяют мне в компоненте Services нажать на кнопку Stop, даже когда служба приостановлена. Но как я могу остановить приостановленную службу — ведь ее поток находится в состоянии ожидания. Только, пожалуйста, не говорите «с помощью TerminaicThread»\
Это только некоторые моменты, которые делают разработку служб делом сложным и запутанным.