Порт завершения ввода-вывода
октября 12 2009 by admin in Обязательный материалВаша служба при инициализации должна создать порт завершения ввода-вывода, обратившись к функции CreateNewCompletionPort. Затем приложение должно создать пул потоков для обработки клиентских запросов. Сколько потоков должно быть в пуле? Вопрос не из легких — я отвечу на него в разделе «Сколько потоков в пуле?». А сейчас примем за правило умножать число процессоров на два — скажем, для двухпроцессорной машины следует создавать пул из 4 потоков.
Все потоки в пуле должны выполнять одни и те же действия. Обычно сначала поток выполняет некоторые инициализационные действия, а затем входит в цикл, который должен быть прерван, когда процесс получает соответствующие указания. Внутри этого цикла поток переводит себя в состояние ожидания завершения ввода-вывода с помощью порта завершения. Это позволяет сделать GetQueuedCompletionStatus-.
BOOL GetQueuedCompletionStatus( HANDLE hCompPort, PDWORD pdwNumBytes, PULONG_PTR CompKey, OVERLAPPED** ppOverlapped, DWORD dwMilliseconds);
Параметр hCompPort указывает порт завершения, состояние которого будет отслеживаться потоком. Многие службы используют единственный порт и получают все уведомления о завершении ввода-вывода через него. В принципе GetQueuedCompletionStatus приостанавливает работу вызвавшего ее потока до момента появления записи в очереди указанного порта завершения ввода-вывода или до наступления таймаута (время таймаута указывается параметром dwMilliseconds).
Третья структура данных, связанная с портом завершения ввода-вывода, — очередь ожидающих потоков. Когда поток из пула потоков обращается к GetQueuedCompletionStatus, его идентификатор заносится в эту очередь, что позволяет объекту ядра, управляющему портом завершения ввода-вывода всегда знать, какие потоки в данный момент готовы обработать завершенные запросы ввода-вывода. Когда в очереди завершения порта появляется запись, порт инициирует запуск одного из потоков, содержащихся в очереди ожидающих потоков. Этот поток получает фрагмент данных — запись из очереди завершения ввода-вывода, содержащий число переданных байт, ключ завершения и адрес структуры OVERLAPPED. Эта информация возвращается потоку параметрами pdwNumBytes, рСотрКеу и ppOverlapped, передаваемыми GetQueuedCompletionStatus.
Определить причину возврата из GetQueuedCompletionStatus не так просто. Вот пример, как это сделать корректно:
DWORD dwNumBytes; UL0NG_PTR CompKey; OVERLAPPED* pOverlapped;
// hlOCP инициализируется где-то в другом месте программы BOOL fOk = GetQueuedCompletionStatus(hIOCP,
idwNumBytes, &CompKey, &pOverlapped, 1000); DWORD dwError = GetLastError();
if (fOk) {
// Обработка нормального завершения запроса ввода-вывода } else {
if (pOverlapped != NULL) {
// Обработка сбоя при выполнении запроса ввода-вывода // dwError содержит информацию о причине сбоя } else {
if (dwError == WAIT.TIMEOUT) {
// Таймаут при ожидании завершения ввода-вывода > else {
// Неверное обращение к GetQueuedCompletionStatus // dwError содержит описание причины возврата } > }
Как и следовало ожидать, записи удаляются из очереди завершения ввода-вывода согласно стратегии «первым пришел — первым обслужен» Однако, что не столь очевидно, потоки, обратившиеся к GetQueuedCompletionStatus, запускаются в соответствии со стратегией «последним пришел — первым обслужен» (LIFO, last-in first-out). Это опять-таки делается для улучшения производительности. Например, в очереди «стоят» четыре потока. Если появляется единственная запись в очереди завершения ввода-вывода, ее обрабатывает последний поток, обратившийся к GetQueuedCompletionStatus. Завершив обработку, поток обращается к GetQueuedCompletionStatus и попадает в очередь ожидания ввода-вывода. Если после этого появляется еще одна запись завершения ввода-вывода, ее обрабатывает тот же поток, что обрабатывал первую запись.
Если выполнение запросов происходит настолько медленно, что их может обрабатывать единственный поток, система продолжает инициировать выполнение именно этого потока, а остальные три потока остаются приостановленными. При использовании такого алгоритма LIFO, ресурсы памяти (например, стек) потоков, не получающих управление, могут содержаться в дисковом файле подкачки и не занимать оперативную память процессора. Получается, что иметь множество потоков для одного порта завершения не так уж плохо. Если есть несколько ожидающих потоков и меньшее число завершенных запросов ввода-вывода, лишние потоки в любом случае хранят большую часть своих ресурсов в файле подкачки.