Как порт завершения ввода-вывода управляет пулом потоков
октября 12 2009 by admin in Обязательный материалПочему так полезны порты завершения ввода-вывода? Во-первых, при создании порта завершения ввода-вывода вы указываете число потоков, способных работать параллельно. Как я говорил, обычно это значение устанавливается равным количеству процессоров на обслуживающей запросы машине. Когда в очереди завершенных запросов появляются записи, порт завершения ввода-вывода должен запустить ожидающие потоки. Однако порт завершения запустит ровно столько потоков, сколько вы указали: если завершились четыре запроса ввода-вывода и четыре потока обратились для ожидания к функции GetQueuedCompletionStatus, порт завершения позволит продолжить работу только двум, а два других будут приостановлены. Обработав запись завершения ввода-вывода, каждый из потоков вновь обратится к GetQueuedCompletion-Status. Система определит, что есть еще необработанные записи и запустит для их обработки те же потоки.
«Но какой смысл иметь дополнительные потоки, ожидающие в пуле, если порт завершения всегда использует только указанное вами число параллельно работающих потоков?» — спросите вы. Отвечаю. Допустим, я работаю на двухпроцессорной машине и, создавая порт завершения ввода-вывода, указываю, что можно использовать не более двух потоков для параллельной обработки записей. Но при этом я создаю 4 потока (удвоенное число процессоров) в пуле потоков. Может показаться, что создаются два лишних потока, которые всегда будут ожидать и никогда и ничего не будут обрабатывать.
Но порт завершения очень умен. Разблокировав поток, он помещает идентификатор потока в четвертую связанную с портом завершения структуру данных — список освобожденных потоков (рис. 2-1). Это позволяет порту запоминать, какие потоки разблокированы, и отслеживать их выполнение. Если же освобожденный поток вызывает какую-то функцию, переводящую его в состояние ожидания, порт завершения это обнаруживает и обновляет внутренние структуры порта, перемещая идентификатор потока из списка освобожденных в список приостановленных потоков (пятая, последняя структура данных, относящаяся к порту завершения ввода-вывода).
Это делается для того, чтобы число записей в списке освобожденных потоков соответствовало указанному при создании порта числу параллельных потоков. Если освобожденный поток почему-то перешел в состояние ожидания, список освобожденных потоков сокращается, и порт завершения освобождает другой ожидающий поток. Если приостановленный поток продолжает работу, он переносится из списка приостановленных в список освобожденных потоков. Это значит, что теперь в списке освобожденных потоков записей больше максимально допустимого числа параллельных потоков.
Когда поток вызывает GetQueuedCompletionStatus, он «назначается» указанному порту завершения. Система предполагает, что назначенные потоки работают на порт завершения. Порт завершения инициирует потоки, находящиеся в пуле, только если число работающих назначенных потоков меньше допустимого числа параллельных потоков порта завершения.
Назначение можно аннулировать одним из следующих способов: завершить работу потока;
обратиться из потока к функции GetQueuedCompletionStatus, передав ей описатель другого порта завершения ввода-вывода; уничтожить порт завершения ввода-вывода, которому назначен поток.
Теперь свяжем все, о чем мы говорили. Пусть мы снова работаем на двухпроцессорной машине. Мы создаем порт завершения, позволяющий параллельно использовать не более двух потоков и создаем четыре потока, ожидающих завершения запросов ввода-вывода. Если в очередь ставятся три завершившихся запроса ввода-вывода, для их обработки инициируются только два потока, что ускоряет переключение контекстов за счет уменьшения числа работающих потоков. Теперь, если один из работающих потоков вызовет функции Sleep, WaitForSingleObject, WaitForMultipleObjects, SignalObjectAndWait, запросит синхронный ввод-вывод или обратиться к другой функции, которая приостановит работу потока, порт завершения ввода-вывода это обнаружит и сразу инициирует третий поток. Задача порта завершения ввода-вывода — загрузить работой центральный процессор.
В какой-то момент первый поток продолжит выполнение. При этом число работающих потоков превысит число процессоров в системе. Однако порт завершения узнает об этом и не позволит запускать дополнительные потоки, пока число работающих потоков не станет меньше числа процессоров. Архитектура порта ввода-вывода предполагает, что число работающих потоков может превышать число процессоров лишь короткое время, и быстро уменьшится, как только потоки перейдут к своему очередному циклу и снова вызовут GetQueuedCompletionStatus. Вот почему пул потоков должен содержать больше потоков, чем установленное для порта завершения значение максимально допустимого числа параллельных потоков.