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

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

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

 

Глава 11. Пулы потоков.docx 255

Табл. 11-1. Значения параметра WaltResuit

Значение

Описание

 

WAIT_OBJECT_0

Ваша функция обратного вызова получает эти значение, если

 

 

объект ядра, переданный функции SetThreadpoolWait, не осво-

 

 

бодится до истечения заданного периода ожидания

 

WAIT_TIMEOUT

Ваша функция обратного вызова получает эти значение, если

 

 

объект ядра, переданный функции SetThreadpoolWait, освобо-

 

 

дится до истечения заданного периода ожидания

 

WAIT_ABANDONED_0

Ваша функция обратного вызова получает эти значение, если

 

 

объект ядра, переданный функции SetThreadpoolWait, ссылает-

 

 

ся на мьютекс, от которого отказался другой поток (см. стр.

 

 

267)

 

После исполнения потоком вашей функции обратного вызова соответствующий объект ожидания становится неактивным. Чтобы еще раз исполнить эту функцию обратного вызова при освобождении того же самого объекта ядра, необходимо снова зарегистрировать объект ожидания, вызвав функцию SetThreadpoolWait.

Предположим, вы зарегистрировали объект, позволяющий ожидать на объекте ядра «процесс». Но объект «процесс», однажды освободившись, остается свободным. В этом случае не имеет смысла повторно регистрировать тот же объект ожидания с описателем одного и того же процесса. Однако способы повторного использования объекта ожидания все же существуют: вызовите SetThreadpoolWait, передав описатель другого объекта ядра либо просто NULL, чтобы удалить объект из пула.

И последнее об этом. Заставить поток ждать на объекте ожидания можно при помощи функции WaitForThreadpoolWaitCallbacks, а чтобы освободить занятую объектом ожидания память, следует вызвать CloseThreadpoolWait. Эти функции работают аналогично функциям WaitForThreadpoolWork и CloseThreadpodlWork, о

которых шла речь выше.

Примечание. Ваша функция обратного вызова ни в коем случае не должна вызвать WaitForThreadpoolWork и передавать ей ссылку на свой собственный рабочий элемент, иначе вы получите взаимную блокировку. Дело в том, что поток блокируется в ожидании своего завершения, а завершиться он в данный момент не сможет. Также нельзя закрывать | описать объекта ядра, переданный SetThreadpoolWait, пока пул потоков ожидает на нем. И последнее: не стоит освобождать зарегистрированное событие вызовом PulseEvent, поскольку нет гарантии, что пул потоков действительно будет ждать это событие в момент вызова PubeEvent.

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

Сценарий 4. Вызов функций по завершении запросов асинхронного ввода-вывода

Впредыдущей главе рассказывалось об эффективном выполнении асинхронных операций ввода-вывода в Windows с помощью портов завершения ввода-вывода.

Вэтой главе я покажу, как создать пул, потоки которого ожидают на объекте «порт завершения ввода-вывода». Преимущество этого решения в том, что пул потоков берет управление созданием и разрушением объектов на себя, а порт является объектом, на котором ожидают автоматически созданные потоки пула. Открывая файл или устройство, свяжите его с портом завершения ввода-вывода, приписанным к пулу потоков. Далее нужно сообщить потоку, какую функцию он должен вызвать после завершения асинхронной операции ввода-вывода на связанном с портом устройстве. Прежде всего, напишите функцию, соответствующую прототипу следующего вида:

VOID CALLBACK OverlappedCompletionRoutine(

 

PTP_CALLBACK_INSTANCE pInstance,

// см. раздел "Обработка завершения

 

 

// обратного вызова"

PVOID

pvContext,

 

PVOID

pOverlapped,

 

ULONG

IoResult,

 

ULONG.PTR

NumberOfBytesTransferred,

PTP_I0

plo);

 

По завершении операции ввода-вывода этой функции будет передан (через napaMerppOverlapped) указатель на структуру OVERLAPPED, использованную для вызова ReadFile или WriteFile, инициировавшего операцию ввода-вывода Результат операции ввода-вывода передается через параметр IoResult. Если операция завершилась успешно, этот параметр содержит значение NO_ERROR. Число переданных байтов передается через параметр NumberOfBytesTransferred, а указатель на элемент ввода-вывода этого пула потоков — через параметр plo. Параметр pInstance подробнее разъясняется в следующем разделе. I Далее вы должны создать для пула объект ввода-вывода. Вызовите функцию CreateThreadpoolIo и передайте ей описатель файла или другого устройства (открытого вызовом CreateFile с флагом FILE_FLAG_OVERLAPPED), которое нужно связать с портом завершения ввода-вывода пула потоков:

РТР_IO CreateThreadpoolIo(

 

 

HANDLE

hDevice,

 

PTP_WIN32_I0_CALLBACK

pfnloCallback,

PVOID

pvContext,

PTP_CALLBACK_ENVIRON

pcbe);

// см. раздел "Настройка пула потоков"

Подготовив все необходимое, свяжите файл или устройство, инкапсулированное в объекте ввода-вывода, с портом пула потоков. Для этого вызовите следующую функцию:

Глава 11. Пулы потоков.docx 257

VOID StartThreadpoolIo(PTP_IO pio);

Учтите, что функцию StartThreadpoolIo необходимо вызывать перед каждым вызовом ReadFile или WriteFile, иначе обратный вызов (OveriappedCom pletzonRoutine) не состоится.

Чтобы отменить обратный вызов после генерации запроса ввода-вывода, вызовите следующую функцию:

VOID CancelThreadpoolIo(PTP_IO pio);

Эту функцию также следует вызвать, если сгенерировать запрос ввода-вывода не удалось (функция ReadFile или WriteFile вернула FALSE либо GetLastError вернула значение, отличное от ERROR_IO_PENDING).

Закончив работу с файлом или устройством, вызовите CloseHandle, чтобы закрыть его описатель и разорвать его связь с пулом потоков. Это делается вызовом следующей функции:

VOID CloseThreadpoolIo(PTP_IO pio);

Поток также можно заставить ждать исполнения запроса ввода-вывода, вызвав такую функцию:

VOID WaitForThreadpoolIoCallbacks(

PTP_I0 pio,

BOOL bCancelPendingCallbacks);

Если передать TRUE в параметре bCancelPendingCallbacks, процедура обратного вызова вызвана не будет (если она уже не вызвана). Достигнутый результат будет аналогичен вызову CancelThreadpoolIo.

Обработка завершения обратного вызова

Пул потоков позволяет задать для функции обратного действия, кото-рые должны быть выполнены после ее завершения. Функция обратного вызова принимает недокументированный параметр pInstance типа PTP_CALLBACK_INSTANCE, который используется одной из следующих функции:

VOID LeaveCriticalSectionWhenCallbackReturns( PTP_CALLBACK_INSTANCE pci, PCRITICAL_SECTION pci);

VOID ReleaseMutexWhenCallbackReturns(PTP_CALLBACK_INSTANCE pci, HANDLE mut); VOID ReleaseSemaphoreWhenCallbackReturns(PTP_CALLBACK_INSTANCE pci,

HANDLE sem, DWORD crel);

VOID SetEventWhenCallbackReturns(PTP_CALLBACK_INSTANCE pci, HANDLE evt); VOID FreeLibraryWhenCallbackReturns(PTP_CALLBACK_INSTANCE pci, HMODULE mod);

Несложно догадаться, что параметр pInstance идентифицирует экземпляр рабочего элемента, таймера или объекта ввода-вывода, который пул потоков обрабатывается в настоящее время. В таблице 11-2 для перечисленных функций приводятся соответствующие действия, которые потоки пула выполняют по завершении обратного вызова.

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

Табл. 11 -2. Обработчики завершения обратного вызова и их действия

Функция

По завершении обратного вызова поток пула...

LeaveCriticalSectionWhen-

…вызывает функцию LeaveCriticalSection и передает ей структуру

CallbackReturns

CRITICAL_SECTION

ReleaseMutexWhenCall-

…вызывает функцию ReleaseMutex и передает ей описатель задан-

backReturns

ный параметром HANDLE

ReleaseSemaphoreWhen-

…вызывает функцию ReleaseSemaphore и передает ей описатель

CallbackReturns

заданный параметром HANDLE

SetEventWhenCallbackRe-

…вызывает функцию SetEvent и передает ей описатель заданный

turns

параметром HANDLE

FreeLibraryWhenCallback-

…вызывает функцию FreeLibrary и передает ей значение параметра

Returns

HMODULE

Первые четыре функции позволяют уведомить поток о том, что другой поток пула закончил некоторую операцию. Последняя функция (FreeLibraryWhenCallbackReturns) дает возможность выгрузить динамически подключаемую библиотеку (DLL) после возврата управления функцией обратного вызова. Это особенно удобно, когда функция обратного вызова реализована в DLL, которую следует выгрузить, когда функция обратного вызова закончит свою работу. Естественно, функция обратного вызова не может вызвать FreeLibrary сама: ее код будет выгружен из адресного пространства процесса, и, когда FreeLibrary вернет управление функции обратного вызова, возникнет нарушение доступа.

Внимание! После завершения функции обратного вызова поток пула может выполнить только одно действие. Поэтому нельзя, например, сразу освободить событие и мьютекс после исполнения функции обратного вызова. В данной ситуации последний вызов отменяет эффект предыдущего.

Существуют еще две функции, обрабатывающие завершение обратного вызо-

ва:

BOOL CallbackMayRunLong(PTP_CALLBACK_INSTANCE pel);

VOID DisassociateCurrentThreadFromCallback(PTP_CALLBACK_INSTANCE pci);

Функция CallbackMayRunLong не обрабатывает завершение функции обратного вызова, а, скорее, уведомляет пул потоков о том, как эта функция будет исполнена. Функция обратного вызова должна вызвать CallbackMayRunLong, если предполагается, что ее исполнение займет много времени. Пул потоков неохотно создает новые потоки, поэтому исполнение длительных операций в очереди пула потоков может страдать от нехватки свободных потоков. Если CallbackMayRunLong возвращает TRUE, в пуле есть потоки, которые смогут обработать стоящие в очереди элементы. Если же эта функция возвращает FALSE, в пуле нет потоков, способных обработать

Глава 11. Пулы потоков.docx 259

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

Функция обратного вызова вызывает довольно сложную функцию DisassociateCurrentThreadFromCallback, чтобы уведомить пул потоков о логическом завершении своей работы. Это позволяет любым потокам пула, заблокированным при вызове функций WaitForThreadpoolWorkCallbacks, WaitForThreadpoolTimerCattbacks, WaitForThreadpoolWaitCallbacks или WaitForThreadpoolIoCallbacks, не ждать реального завершения функции обратного вызова.

Настройка пула потоков

Функции CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait и CreateThreadpoolIo принимают параметр PTP_CALLBACK_ENVIRON. Если передать в нем NULL-значение, рабочие элементы будут ставиться в очередь пула потоков по умолчанию, оптимально настроенного для работы с большинством приложений.

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

Чтобы создать новый пул потоков для своего приложения, вызовите следующую функцию:

PTP_P00L CreateThreadpool(PVOID reserved);

Несложно догадаться, что параметр reserved зарезервирован, и в нем следует передавать NULL. Возможно, он будет использоваться в следующих версиях Windows. Эта функция возвращает значение PTP_POOL со ссылкой на новый пул потоков. Теперь можно задать максимальное и минимальное число потоков в пуле вызовом следующих функций:

BOOL SetThreadpoolThreadMinimum(PTP_POOL pThreadPool. DWORD cthrdMin); BOOL SetThreadpoolThreadMaximum(PTP_POOL pThreadPool, DWORD cthrdMost);

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

равно 1, а максимальное — 500.

В редких случаях Windows отменяет запрос, например, если исполняющий его поток завершается. Возьмем функцию RegNottfyChangeKeyWtue. Вызывая ее, поток передает описатель события, которое Windows освобождает при изменении системного реестра. Если поток, вызвавший, RegNotifyChangeKeyValue, завершится, Windows перестанет освобождать это событие.

Пул создает и разрушает потоки по своему усмотрению для максимально эффективной работы Так есть вероятность (и немалая), вызвавший функцию RegNotifyChangeKeyValue, будет уничтожен пулом, после чего

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

Windows перестанет уведомлять приложение об изменениях реестра. Наверное, лучшее решение этой проблемы состоит в создании специального потока (вызо-

вом CreateThread), который вызовет функцию RegNotifyChangeKeyValue и про-

должит работу. Есть и другое решение. Оно заключается в создании пула потоков, у которого максимальное и минимальное число потоков идентично. Потоки такого пула никогда не уничтожаются. Один из потоков такого пула вызывает функцию RegNotifyChangeKeyValue, и Windows бесперебойно уведомляет ваше приложение об изменениях реестра.

Если настроенный пул потоков больше не нужен вашему приложению, следует уничтожить этот пул вызовом функции CloseThreadpooh

VOID CloseThreadpool(PTP_POOL pThreadPool);

После вызова CloseThreadpool постановка новых элементов в очередь этого пула невозможна. Все его потоки заканчивают текущие операции и завершаются сами, а все элементы очереди пула, обработка которых еще не началась, отменяются.

Создав собственный пул и указав для него максимальное и минимальное число потоков, инициализируйте структуру данных, представляющую среду обратных вызовов (callback environment). Эта структура содержит ряд дополнительных параметров настройки рабочих элементов.

В заголовочном файле WinNT.h структура данных, представляющая среду обратных вызовов потока, определена следующим образом:

typedef struct _TP_CALLBACK_ENVIRON {

 

 

TP_VERSION

Version;

 

PTP_P00L

Pool;

 

PTP_CLEANUP_GROUP

CleanupGroup;

 

PTP_CLEANUP_GROUP_CANCEL_CALLBACK

CleanupGroupCancelCallback;

PVOID

RaceDll;

 

struct _ACTIVATION_CONTEXT

*ActivationContext;

PTP_SIMPLE_CALLBACK

FinalizationCallback;

union {

 

 

DWORD

Flags;

 

struct {

 

 

DWORD

LongFunction : 1;

DWORD

Private

: 31;

}s;

}u;

}TP_CALLBACK_ENVIRON, *PTP_CALLBACK_ENVIRON;

Технически вы можете анализировать эту структуру и вручную манипулировать ее полями, но делать этого не следует. Считайте ее недокументированной и используйте для работы с ее полями соответствующие функции, объявленные в файле WinBase.h. Чтобы инициализировать эту структуру, вызовите следующую функцию:

Глава 11. Пулы потоков.docx 261

VOID InitializeThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);

Эта встраиваемая (inline) функция записывает 0 во все поля за исключением Version, в которое записывается 1. Среду обратных вызовов, ставшую ненужной, как и любую другую структуру, необходимо корректно разрушить вызовом функ-

ции DestroyThreadpoolEnvironment

VOID DestroyThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);

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

VOID SetThreadpoolCallbackPool(PTP_CALLBACK_ENVIRON pcbe, PTP_P00L pThreadPool);

Если SetThreadpoolCallbackPool не вызвана, то в поле Pool структуры TP_CALLBACK_ENVIRON остается NULL, и рабочие элементы попадают в очередь пула потоков по умолчанию. Функция SetThreadpoolCallbackRunsLong «сообщает» среде обратных вызовов, что обработка элементов очереди, как правило, занимает длительное время. В результате пул быстрее создает потоки, стараясь полностью обработать элементы и жертвуя ради этого производительностью:

VOID SetThreadpoolCallbackRunsLong(PTP_CALLBACK_ENVIRON pcbe);

Функция SetThreadpoolCallbackLibrary гарантирует, что заданная DLL не будет выгружена из адресного пространства процесса, пока у пула потоков остаются необработанные элементы:

VOID SetThreadpoolCallbackLibrary(PTP_CALLBACK_ENVIRON pcbe, PVOID mod);

В сущности, SetThreadpoolCallbackLibrary предотвращает потенциальные взаимные блокировки. Подробнее об этой довольно сложной функции см. в доку-

ментации Platform SDK.

Корректное разрушение пула потоков и группы очистки

Пулы потоков обрабатывают множество рабочих элементов, поступающих в их очереди из различных источников. По этой причине сложно узнать, когда пул потоков уже завершил обработку своей очереди может быть корректно разрушен. Для координации уничтожения пулов потоков предназначен механизм под названием «группы очистки» (cleanup groups). Учтите, что сказанное в этом разделе не касается пула потоков по умолчанию, поскольку уничтожить его нельзя: он живет, пока жив процесс, а после завершения процесса Windows автоматически уничтожает пул по умолчанию и освобождает все занятые им ресурсы.

Выше уже говорилось о том, как инициализировать структуру TP_CALLBACK_ENVIRON, которая используется для постановки элемен-

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

тов в очередь пользовательского пула потоков. Чтобы корректно уничтожить такой пул, необходимо сначала создать группу очистки, вызвав функцию CreateThreadpoolCleanupGroup:

PTP_CLEANUP_GROUP CreateThreadpoolCleanupGroup();

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

VOID SetThreadpoolCallbackCleanupGroup(

PTP_CALLBACK_ENVIRON pcbe,

PTP_CLEANUP_GROUP ptpcg,

PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng);

Эта функция устанавливает поля CleanupGroup и CleanupGroupCancelCallback

структуры PTP_CALLBACK_ENVIRON. Параметр pfng функции SetThreadpoolCallbackCleanupGroup идентифицирует адрес функции обратного вызова, которая исполняется в случае отмены группы очистки. Если этому параметру присвоено значение, отличное от NULL, заданная функция обратного вызова должна соответствовать следующему прототипу:

VOID CALLBACK CleanupGroupCancelCallback(

PVOID pvObjectContext,

PVOID pvCleanupContext);

Если при вызове CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait или CreateThreadpoolIo в последнем параметре для структуры PTP_CALLBACK_ENVIRON передается значение, отличное от NULL, происходит следующее. Созданный этим вызовом объект добавляется в группу очистки среды обратных вызовов соответствующего пула, уведомляя этот пул о потенциальном добавлении рабочего элемента в его очередь. Если после обработки этих элементов будет вызвана функция CloseThreadpoolWork, CloseThreadpoolTimer, CloseThreadpoolWait или CloseThreadpoolIo, соответствующий объект неявно уда-

ляется из соответствующей группы очистки.

Теперь для уничтожения пула потоков приложение может вызвать функцию:

VOID CloseThreadpoolCleanupGroupMembers(

PTP_CLEANUP_GROUP ptpcg,

BOOL bCancelPendingCallbacks,

PVOID pvCleanupContext);

Она работает аналогично различным WaitForThreadpool-функциям (таким, как WaitForThreadpoolWork), о которых рассказывалось выше в этой главе. Когда поток вызывает CloseThreadpoolCleanupGroupMembers, он бло-

кируется, пока не будут разрушены все объекты в группе очистки пула, к которому этот поток относится. При желании, передав TRUE в параметре

Глава 11. Пулы потоков.docx 263

bCancelPendingCallbacks, можно просто отменить все необработанные элементы. Функция вернет управление, как только завершится обработка текущих элементов. Если в параметре bCancelPendingCallbacks передано значении TRUE, а в па-

раметре pfng функции SetThreadpoolCallbackCleanupGroир — адрес функции

CleanupGroupCancelCallback, ваша функция обратного вызова будет вызвана по разу для каждого из отменяемых элементов. Для определения контекста исполь-

зуется параметр pvContextCreateThreadpool-функций. У функции CleanupGroupCancelCallback этот параметр содержит текст, переданный одноименный пара-

метр функции CloseThreadpoolCleanupGroupMembers.

Если вызвать CloseThreadpoolCleanupGroupMembers, передав ей FALSE в па-

раметре bCancelPendingCallbacks, функция будет ждать завершения обработки всех оставшихся в очереди элементов. Заметьте также, что в параметре pvCleanupContext можно передать NULL, ведь ваша функция CleanupGroupCancelCallback все равно не будет вызвана.

После обработки либо отмены всех рабочих элементов вызовите функцию CloseThreadpoolCleanupGroup, чтобы освободить ресурсы группы очистки:

VOID WINAPI CloseThreadpoolCleanupGroup(PTP_CLEANUP_GROUP ptpcg);

В завершение вызовитеDestroyThreadpoolEnvironment и CloseThreadpool

пул потоков будет корректно уничтожен.

Оглавление

 

Г Л А В А 1 2 Волокна ..........................................................................................................

422

Работа с волокнами..............................................................................................................

423

Программа-пример Counter................................................................................................

426

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