Смекни!
smekni.com

Эффективная многопоточность (стр. 4 из 5)

Встроенная поддержка пула потоков

В Windows 2000 появились новые функции, которые условно можно разделить на четыре группы:

помещение запроса в очередь;

вызов функции при окончании асинхронной операции ввода/вывода;

периодический вызов функции;

вызов функции при переходе объекта в сигнальное состояние.

Рассмотрим их по порядку.

Помещение запроса в очередь

Передать на выполнение потоку из пула какую-либо функцию можно с помощью сервиса QueueUserWorkItem. Эта с виду простая функция делает очень много: она создает порт завершения ввода/вывода, создает и уничтожает потоки в пуле и многое другое. Вот ее описание:

BOOL QueueUserWorkItem( LPTHREAD_START_ROUTINE Function, // адресфункцииPVOID Context, // произвольный параметр ULONG Flags // флаги выполнения);

QueueUserWorkItem помещает пакет запроса в виде адреса функции и произвольного параметра в очередь запросов порта завершения и сразу же возвращает управление. Вот как выглядит функция, которая будет вызвана одним из потоков в пуле:

DWORD WINAPI ThreadProc( LPVOID lpParameter // произвольныйпараметр);

Ее прототип ничем не отличается от стартовой процедуры потока, так что здесь вам все должно быть ясно. Гораздо интереснее знать, что скрывается внутри функции QueueUserWorkItem. Давайте разбираться.

При первом помещении запроса количество потоков в пуле равно нулю, так что QueueUserWorkItem приходится создавать поток и порт завершения. Затем в порт помещается пакет запроса, а поток вызывает функцию GetQueuedCompletionStatus. После обработки запроса поток не разрушается, а остается еще некоторое время в пуле, так что следующий запрос обработается намного быстрее. Если вы отправляете запросы слишком часто, и количество необработанных пакетов увеличивается, QueueUserWorkItem создаст для вызова функции новый поток. Максимальное количество потоков в пуле равно количеству процессоров, что не очень хорошо, но есть способ заставить функцию всегда создавать новый поток.

ПРИМЕЧАНИЕТе из вас, кто читал статью Дж. Рихтера «New Windows 2000 Pooling Functions Greatly Simplify Thread Management» из апрельского MSJ за 1999 год, могут поспорить со мной насчет размера пула. В статье указывается, что количество потоков в нем равно удвоенному количеству процессоров в системе, однако это не так. Вы можете собственноручно в этом убедиться, поставив breakpoint на функцию _RtlpInitializeWorkerThreadPool (адрес 0x77FA95CD на Windows 2000 Professional SP3) и вызвав функцию QueueUserWorkItem.

Рассмотрим флаги функции QueueUserWorkItem.

Константа Значение Описание
WT_EXECUTEDEFAULT 0 Запрос помещается в простой рабочий поток
WT_EXECUTEINIOTHREAD 1 Запрос помещается в поток ввода/вывода
WT_EXECUTEINPERSISTENTTHREAD 0x80 Запрос помещается в поток, который не завершается после обработки запроса, поэтому он может сохранять свое состояние, например в TLS.
WT_EXECUTELONGFUNCTION 0x10 Запрос с данным флагом всегда помещается в новый поток

Таблица 3. Флаги функции QueueUserWorkItem.

Если вы не выполняете асинхронных запросов ввода/вывода в функции ThreadProc, не используете TLS (Thread Local Storage) или функций, которые его используют, а продолжительность выполнения операции невелика – указывайте флаг WT_EXECUTEDEFAULT.

Предположим, вы начали асинхронную операцию ввода/вывода в своей функции ThreadProc. Для того чтобы она завершилась, поток в котором она началась, не должен быть разрушен. Однако флаг WT_EXECUTEDEFAULT этого не гарантирует. С этим флагом поток может быть удален, даже если у него имеются незавершенные асинхронные операции. Для того чтобы поток завершался только после окончания всех начатых асинхронных операций, нужно указать флаг WT_EXECUTEINIOTHREAD.

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

И наоборот, если вам нужно каждый раз выполнять длительную операцию, укажите флаг WT_EXECUTELONGFUNCTION. Для каждой такой операции создается новый поток, который после ее обработки удаляется.

Характеристика Значение
Начальное коли-чество потоков в пуле 0
Когда поток удаляется Поток не имеет незавершенных операций ввода/вывода и простаивает некоторое время
Способ ожидания, используемый потоком Тревожное (alertable) ожидание
Поток просыпается при Приходе APC-запроса

Таблица 4. Описание работы функции QueueUserWorkItem

Вызов функции при окончании асинхронной операции ввода/вывода

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

BOOL BindIoCompletionCallback( // хендлфайла HANDLE FileHandle, // функцияобработкизавершениязапроса LPOVERLAPPED_COMPLETION_ROUTINE Function, // зарезервированоULONG Flags );

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

VOID CALLBACK FileIOCompletionRoutine( DWORD dwErrorCode, // кодзавершения DWORD dwNumberOfBytesTransfered, // количествопереданныхбайтов LPOVERLAPPED lpOverlapped // структура OVERLAPPED);

Хотя прототип этой функции идентичен функции, вызываемой при окончании операций, начатых ReadFileEx и WriteFileEx, не стоит их путать. При использовании BindIoCompletionCallback эта функция вызывается с помощью порта завершения ввода/вывода, тогда как при использовании ReadFileEx и WriteFileEx функция вызывается с помощью APC.

Совершенно непонятно, почему в Microsoft решили не использовать флаги для этой функции, но факт остается фактом. И хотя Рихтер в своей статье, которая упоминалась выше, утверждает, что можно указать флаг WT_EXECUTEINIOTHREAD, это неправда. Вы можете сами посмотреть дизассемблером в ntdll.dll, например, функцию RtlSetIoCompletionCallback и убедиться, что третий параметр в ней просто не используется.

Как видно из прототипа, вы не можете передавать какого-либо дополнительного параметра функции FileIOCompletionRoutine, что может вызвать определенные проблемы. Самым распространенным решением является передача в функцию начала асинхронной операции структуры, производной от OVERLAPPED. Тогда в дополнительных членах структуры можно передавать какую угодно информацию – сама структура OVERLAPPED не копируется, везде передается только указатель на нее.

Характеристика Значение
Начальное коли-чество потоков в пуле 0
Когда поток удаляется Поток простаи-вает некоторое время
Способ ожидания, используемый потоком GetQueuedCompletionStatus
Поток просыпается при Постановке пакета запроса в очередь порта

Таблица 5. Описание характеристик работы функции BindIoCompletionCallback

Периодический вызов функции

В самом начале статьи я обещал рассказать о новых «таймерных» функциях. До выхода Windows 2000 имелось три механизма периодического вызова пользовательских функций: «оконный» таймер, Multimedia-таймер и ожидающий таймер. У каждого из них были серьезные недостатки, к тому же они не поддерживали обработку запросов в пуле. Новые функции по созданию очереди таймеров более универсальны.