Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6262
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

326 Часть II. Приступаем к работе

Функция SignalObjectAndWait

SignedObjectAndWait переводит в свободное состояние один объект ядра и ждет другой объект ядра, выполняя все это как одну операцию на уровне атомарного доступа:

DWORD SignalObjectAndWait(

HANDLE hObjectToSignal,

HANDLE hObjectToWaitOn,

DWORD dwMilliseconds,

BOOL bAlertable);

Параметр hObjectToSignal должен идентифицировать мьютекс, семафор или событие; объекты любого другого типа заставят SignalObjectAndWait вернуть

WAIT_FAILED, а функцию GetLastError — ERROR_INVALID_HANDLE. Функ-

ция SignalObjectAndWait проверяет тип объекта и выполняет действия, аналогичные тем, которые предпринимают функции ReleaseMutex, ReleaseSemaphore (со счетчиком, равным 1) или ResetEvent.

Параметр hObjectToWaitOn идентифицирует любой из следующих объектов ядра: мьютекс, семафор, событие, таймер, процесс, поток, задание, уведомление об изменении файла или консольный ввод. Параметр dwMilliseconds, как обычно, определяет, сколько времени функция будет ждать освобождения объекта, а флаг bAlertable указывает, сможет ли поток в процессе ожидания обрабатывать посылаемые ему АРС-вызовы.

Функция возвращает одно из следующих значений: WAIT_OBJECT_0, WAIT_TIMEOUT, WAIT_FAILED, WAIT_ABANDONED (см. раздел о мьютексах) или WAIT_IO_COMPLETION.

SignalObjectAndWait — удачное добавление к Windows API по двум причинам. Во-первых, освобождение одного объекта и ожидание другого — задача весьма распространенная, а значит, объединение двух операций в одной функции экономит процессорное время. Каждый вызов функции, заставляющей поток переходить из кода, который работает в пользовательском режиме, в код, работающий в режиме ядра, требует примерно 200 процессорных тактов, перепланировка потока также занимает немало тактов процессора (на платформах x86), и поэтому для выполнения, например, такого кода:

ReleaseMutex(hMutex);

WaitForSingleObject(hEvent, INFINITE);

понадобится около 400 тактов. В высокопроизводительных серверных приложениях SignalObjectAndWait дает заметную экономию процессорного времени. Использование функции SignalObjectAndWait в высокопроизводительных серверных приложениях экономит много времени.

Во-вторых, без функции SignalObjectAndWait ни у одного потока не было бы возможности узнать, что другой поток перешел в состояние ожидания. Знание таких вещей очень полезно для функций типа PulseEvent. Как я уже говорил в этой главе, PulseEvent переводит событие в свободное состояние

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 327

и тут же сбрасывает его. Если ни один из потоков не ждет данный объект, событие не зафиксирует этот импульс (pulse). Я встречал программистов, которые пишут вот такой код:

//выполняем какие-то операции … SetEvent(hEventWorkerThreadDone); WaitForSingleObject(hEventKoreWorkToBeDone, INFINITE);

//выполняем еще какие-то операции …

Этот фрагмент кода выполняется рабочим потоком, который проделывает ка- кие-то операции, а затем вызывает SetEvent, чтобы сообщить (другому потоку) об окончании своих операций. В то же время в другом потоке имеется код:

WaitForSingleObject(hEventWorkerThreadDone);

PulseEvent(hEventMoreWorkToBeDone);

Приведенный ранее фрагмент кода рабочего потока порочен по самой своей сути, так как будет работать ненадежно. Ведь вполне вероятно, что после того, как рабочий поток обратится к SetEvent, немедленно пробудится другой поток и вызовет PulseEvent. Проблема здесь в том, что рабочий поток уже вытеснен и пока еще не получил шанса на возврат из вызова SetEvent, не говоря уж о вызове WaitForSingleObject. В итоге рабочий поток не сможет своевременно освободить событие hEventMoreWorkToBeDone.

Но если вы перепишете код рабочего потока с использованием функции SignalObjectAndWait:

//выполняем какие-то операции … SignalObjectAndWait(hEventWorkerTnreadDone, hEventMoreWorkToBeDone, INFINITE, FALSE);

//выполняем еще какие-то операции …

то код будет работать надежно, поскольку освобождение и ожидание реализуются на уровне атомарного доступа. И когда пробудится другой поток, вы сможете быть абсолютно уверены, что рабочий поток ждет события hEventMoreWorkToBeDone, а значит, он обязательно заметит импульс, «приложенный» к событию.

Обнаружение взаимных блокировок с помощью Wait Chain Traversal API

Разработка многопоточных приложений — одна из сложнейших задач программирования, а поиск в них багов, возникающих из-за бесконечного ожидания и блокировок, особенно взаимных блокировок, еще сложнее. Функции для анализа т.н. цепочек ожидания, собранные в API Wait Chain Traversal (WCT), являются новинкой Windows Vista. Эти функции позволяют генерировать списки блокировок и выявлять взаимные блокировки потоков, принадлежащих не только к одному, но и к разным процессам. С помощью API-функций WCT Windows отслеживает синхронизирующие механизмы, а также потенциальные источники блокировок (см. табл. 9-4).

328 Часть II. Приступаем к работе

Табл. 9-4. Типы синхронизирующих механизмов, отслеживаемых WCT

Потенциальные источники

Описание

блокировок

 

Критическая секция

Windows отслеживает критические секции и потоки, которые

 

ими владеют

Мьютексы

Windows отслеживает мьютексы (включая те, от которых отка-

 

зались владевшие им потоки) и потоки, которым они принад-

 

лежат

Процессы и потоки

Windows отслеживает потоки, ждущие завершения других по-

 

токов и процессов

Вызовы SendMessage

Система отслеживает потоки, ожидающие завершения вызова

 

SendMessage

Вызовы и инициализация объ-

Вызовы CoCreateInstance и методов объектов СОМ также от-

ектов СОМ

слеживаются

Advanced Local Procedure Call

Механизма ALPC — новый незадокументированный механизм

(ALPC)

межпроцессной коммуникации, заменивший LPC в Windows

 

Vista

Внимание! WCT не отслеживает синхронизирующие блокировки SRWLock (см. главу 8). Учтите также, что многие объекты ядра, включая события, семафоры и ожидаемые таймеры, также не отслеживаются, поскольку любой поток, освободив такой объект, может в произвольный момент времени пробудить другой поток, ждущий этот объект.

Программа-пример LockCop

Программа-пример LockCop (09-LockCop.exe) показывает, как с помощью WCTфункций написать весьма полезную утилиту. Файлы с исходным кодом и ресурсами этой программы см. в каталоге 09-LockCop архива, доступного на веб-сайте поддержки этой книги. Если запустить LockCop и выбрать в списке Processes отображение программ, в которых возникли взаимные блокировки, откроется окно (рис. 9-1) со списком потоков, попавших во взаимные блокировки.

Сначала LockCop перечисляет работающие в настоящее время процессы с использованием ToolHelp32 (см. главу 4), записывая идентификаторы и идентификаторы процессов в поле со списком Processes. При выборе процесса выводится список его потоков, попавших во взаимную блокировку, с указанием идентификаторов этих потоков, а также их цепочки ожидания (wait chain). На сайте MSDN цепочка ожидания определяется как «последовательность потоков и синхронизирующих объектов, в которой за потоком идет объект, ожидаемый этим потоком и принадлежащий следующему потоку в этой последовательности».

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 329

Рис. 9-1. LockCop в действии

Чтобы разобраться в работе цепочки ожидания, разберем ее на примере. На рис. 9-1 поток 3212 находится во взаимной блокировке, причины которой видны при анализе его цепочки ожидания:

поток 3212 блокирован в ожидании критической секции (назовем ее CS1);

эта критическая секция (CS1) принадлежит другому потоку (потоку с идентификатором 2260), ждущему другую критическую секцию (CS2);

вторая критическая секция (CS2) принадлежит первому потоку с идентификатором 3212.

Подведем итоги: поток 3212 ждет, когда поток 2260 освободит нужную ему

критическую секцию, в то время как поток 2260 ждет освобождения потоком 3212 другой критической секции — налицо типичная взаимная блокировка (см. стр.

238).

В сущности, отображаемые LockCop сведения генерируются различными WCT-функциями. Чтобы немного облегчить работу с ними, я создал C++-класс CWCT (см. файл WaitChainTraversal.h), упрощающий анализ цепочки ожидания. Вам нужно породить от CWCT свой класс и переопределить пару виртуальных методов (выделены на листинге жирным шрифтом). После этого можно будет во время выполнения вызвать функцию ParseThreads с передачей ей идентификатора нужного вам процесса:

class CWCT

{

public:

CWCT();

~CWCT();

330Часть II. Приступаем к работе

//Перебираем потоки, работающие в заданном процессе,

//генерируя для каждого из них цепочку ожидания. void ParseThreads(DWORD PID);

protected:

//Этот метод вызывается для анализа каждого потока.

//Его следует переопределить.

//Примечание. Если nodeCount = 0, проанализировать поток не удалось virtual void 0nThread(DW0RD TID, B00L bDeadlock, DWORD nodeCount);

//Этот метод вызывается для каждого звена цепочки ожидания

//Его следует переопределить.

virtual void OnChainNodeInfo(DWORD rootTID, DWORD currentNode, WAITCHAIN_NODE_INF0 nodeInfo);

// возвращаем число звеньев цепочки ожидания для текущего потока

DWORD GetNodesInChain();

//Получаем идентификатор анализируемого процесса

DWORDGetPID();

private:

void InitC0M();

void ParseThread(DWORD TID);

private:

//Описатель сеанса

WCT HWCT _hWCTSession;

//Описатель модуля 0LE32.DLL HM0DULE _hOLE32DLL;

DWORD _PID;

DWORD _dwNodeCount;

};

При создании экземпляра CWCT вызывается функция RegisterWaitChainCOMCallback для регистрации контекста СОМ у WCT (детали реализации см. в коде метода InitCOM), после чего открывается цепочка ожидания вызовом следующей функции:

HWCT OpenThreadWaitChainSession(

DWORD dwFlags,

PWAITCHAINCALLBACK callback);

Чтобы вызвать ее асинхронно, передайте в параметре dwFlags значение о, в противном случае передайте флаг WCT_ASYNC_OPEN_FLAG. В первом

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 331

случае также передается второй параметр с указателем на функцию обратного вызова. На сильно загруженном компьютере построение длинной цепочки ожидания может занять значительно время. В подобной ситуации имеет смысл использовать асинхронный вызов, поскольку при атом можно отменить построение цепочки вызовом CloseThreadWaitChainSession. Однако класс CWCT генерирует цепочку ожидания синхронно, поэтому в dwFlags передается 0, а второй параметр устанавливается в NULL. Деструктор CWCT закрывает сеанс WCT, передавая функ-

ции CloseThreadWaitChainSession описатель сеанса, полученный от OpenThreadWaitChainSession.

Перебор потоков заданного процесса выполняется функцией ParseThreads, в которой задействован ToolHelp API (см. главу 4):

void CWCT::ParseThreads(DWORD PID) {

_PID = PID;

// Создаем список всех потоков заданного процесса

CToolhelp th(TH32CS_SNAPTHREAD, PID); THREADENTRY32 te = { sizeof(te) }; BOOL fOk = th.ThreadFirst(&te);

for (; fOk; fOk = th.ThreadNext(&te)) {

// Анализируем только потоки заданного процесса if (te.th32OwnerProcessID == PID) {

ParseThread(te.th32ThreadID);

}

}

}

Метод ParseThread — ключевой для анализа цепочек ожидания:

void CWCT::ParseThread(DWORD TID) {

WAITCHAIN_NODE_INFO

chain[WCT_MAX.NODE_COUNT];

DWORD

dwNodesInChain;

BOOL

bOeadlock;

dwNodesInChain = WCT_MAX_NODE_COUNT;

// получаем цепочку ожидания для текущего потока

if (!GetThreadWaitChain(_hWCTSession, NULL, WCTP_GETINF0_ALL_FLAGS, TID, &dwNodesInChain, chain, &bDeadlock)) {

_dwNodeCount = 0; OnThread(TID, FALSE, 0); return;

}

332Часть II. Приступаем к работе

//начинаем обработку цепочки ожидания для текущего потока

_dwNodeCount = min(dwNodesInChain, WCT_MAX_NODE_COUNT); OnThread(TID, bDeadlock, dwNodesInChain);

//для каждого узла цепочки вызываем виртуальный метод onChainNodeInfo

for (

DWORD current = 0;

current < min(dwNodesInChain, WCT_MAX_N0DE_C0UNT); current++) {

OnChainNodeInfo(TID. current, chain[current]);

}

}

Функция GetThreadWaitChain заполняет массив структур WAITCHAIN_NODE_INFO, каждая из которых содержит описание заблокированного потока либо синхронизирующего объекта, вызвавшего блокировку:

B00L WINAPI GetThreadWaitChain( HWCT hWctSession,

DW0RD_PTR pContext, DWORD dwFlags, DWORD TID,

PDW0RD pNodeCount, PWAITCHAIN_N0DE_INF0 pNodeInfoArray, LPB00L pbIsCycle

);

Описатель, который вернула функция OpenThreadWaitChainSession, передается в параметре hWctSession. В случае асинхронного сеанса любая дополнительная информация передается через параметр pContext. Параметр dwFlags управляет работой функции при анализе потоков, принадлежащих разных процессам, и содержит один из флагов, перечисленных в таблице 9-5.

Табл. 9-5. Флаги функции GetThreadWaitChain

Значение dwFlags

Описание

WCT_OUT_OF_PROC_FLAG (0x1)

Если этот флаг не установлен, в цепочке ожида-

 

ния не будет сведений о потоках и объектах из

 

процессов, отличных от процесса, в котором рабо-

 

тает текущий поток. Этот флаг устанавливают для

 

приложений с несколькими процессами либо про-

 

грамм, порождающих процессы и ожидающих их

 

завершения

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 333

Табл. 9-5. (окончание)

Значение dwFlags

Описание

WCT_OUT_OF_PROC_CS_FLAG (0x4)

Включает сбор сведений о критических секциях из

 

процессов, отличных от процесса, в котором рабо-

 

тает текущий поток. Этот флаг устанавливают для

 

приложений с несколькими процессами либо про-

 

грамм, порождающих процессы и ожидающих их

 

завершения

WCT_OUT_OF_PROC_COM_FLAG (0x2)

Важен для работы с серверами СОМ для многопо-

 

точного окружения

WCTP_GETINFO_ALL_FLAGS

Эквивалент одновременной установки все вышепе-

 

речисленных флагов

Параметр TID содержит идентификатор потока, с которого вы хотите начать цепочку ожидания. Описание цепочки ожидания возвращается в последних трех параметрах:

DWORD, на которое ссылается параметр pNodeCount, содержит число звеньев цепочки;

сами звенья хранятся в массиве, который передается через параметр pNodeInfoArray;

при обнаружении взаимной блокировки булева переменная, на которую ссылается параметр pbIsCycle, устанавливается в TRUE.

При обработке функцией ParseThread каждого потока заданного процесса, вызывается переопределенный вами метод OnThread с передачей идентификатора потока в первом параметре; при обнаружении взаимной блокировки bDeadLock устанавливается в TRUE, а nodeCount содержит число звеньев цепочки ожидания этого потока (значение 0 присваивается этому параметру при сбоях, например, изза отказа в доступе). Переопределенный вами метод OnChainNodeInfo вызывается для обработки каждого из звеньев цепочки ожидания, при этом в параметре rootTID передается идентификатор потока, переданный OnThread; в currentNode передается номер (начиная с 0) текущего звена, а параметре nodeInfo — описание этого звена в виде структуры WAITCHAW_NODE_INFO, объявленной в заголовочном файле wct.h.

typedef struct _WATTCHAIN_N0DE_INF0

{

WCT_OBJECT_TYPE ObjectType; WCT_OBJECT_STATUS ObjectStatus;

union { struct {

WCHAR ObjectName[WCT_OBJNAME_LENGTH];

LARGE_INTEGER Timeout; // в этой версии не реализовано

334 Часть II. Приступаем к работе

BOOL Alertable;

// в этой версии не реализовано

} LockObject;

 

struct {

 

DW0R0 ProcessId;

 

DWORD ThreadId;

 

 

 

DWORD WaitTime;

 

DWORD ContextSwitches;

 

} ThreadObject;

 

};

 

} WAITCHAIN_NODE_INFO, *PWAITCHAIOODE_INFO;

Тип звена определяется полем ObjectType, принимающим значения перечис-

лимого WCT_OBJECT_TYPE (см. табл. 9-6).

Табл. 9-6. Типы объектов, представляющих звенья цепи ожидания

WCT_OBJECT_TYPE

Описание

WctThreadType

Заблокированный поток

WctCriticalSectionТуре

Принадлежащий потоку объект — критическая секция

WctSendMessageТуре

Причина блокирования — вызов SendMessage

WctMutexType

Принадлежащий потоку объект — мьютекс

WctAlpcType

Причина блокирования — ALPC-вызов

WctComType

Ожидание завершения вызова СОМ

WctThreadWaitType

Ожидание завершения потока

WctProcessWaitType

Ожидание завершения процесса

WctComActivationType

Ожидание завершения вызова CoCreateInstance

WctUnknownТуре

Зарезервирован для будущих расширений API

Группа ThreadObject в составе этой структуры имеет смысл, только если ObjectType = WctThreadType, во всех остальных случае используется группа LockObject. Цепочка ожидания потока всегда начинается с звена типа WctThreadType, соответствующего значению параметра rootTID, переданному при вызове OnChainNodeInfo.

Поле ObjectStatus содержит сведения о состоянии потока, если ObjectType = WctThreadType. В противном случае это поле содержит информацию о состоянии блокировки — звене цепочки ожидания. Описание состояния определено как пе-

речислимое WCT_OBJECT_STATUS:

typedef enum _WCT_OBJECT_STATUS

 

{

 

WctStatusNoAccess = 1,

// Отказано в доступе к объекту

WctStatusRunning,

// Состояние потока

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 335

WctStatusBlocked,

// Состояние потока

WctStatusPidOnly,

// Состояние потока

WctStatusPidOnlyRpcss,

// Состояние потока

WctStatusOwned,

// Состояние синхронизирующего объекта

WctStatusNotOwned,

// Состояние синхронизирующего объекта

WctStatusAbandoned,

// Состояние синхронизирующего объекта

WctStatusUnknown,

// Для всех объектов

WctStatusError,

// Для всех объектов

WctStatusMax

 

} WCT_OBJECT_STATUS;

 

Чтобы проиллюстрировать работу утилиты LockCop, я прилагаю к ней программу 09-BadLock, которая генерирует массу взаимных блокировок и бесконечно ожидающих потоков. Она поможет понять, как WCT заполняет структуру WAITCHAIN_NODE_INFO для разных типов блокировок.

Примечание. Утилита LockCop удобна для диагностики взаимных блокировок и ситуаций с бесконечно ожидающими потоками. Однако Windows Vista налагает на LockCop одно ограничение: в этой операционной системе не поддерживается функция WaitForMultipleObjects. Если ваш код вызывает эту функцию, чтобы заставить поток ждать сразу несколько объектов, LockCop обнаружит бесконечные циклы, но, вызывая OnThread по завершении метода GetThreadWaitChain, он не «увидит» явные блокировки.

Соседние файлы в предмете Программирование на C++