Сценарий 1: асинхронный вызов функций
Допустим, у Вас есть серверный процесс с основным потоком, который ждет клиентский запрос. Получив его, он порождает отдельный поток для обработки этого запроса. Тем самым основной поток освобождается для приема следующего клиентского запроса. Такой сценарий типичен в клиент-серверных приложениях. Хотя он и так то незатейлив, при желании его можно реализовать с использованием новых функций пула потоков.
Получая клиентский запрос, основной поток вызывает:
BOOL QueueUserWorkItem( PTHREAD_START_ROUTINE pfnCallback, PVOID pvContext, ULONG dwFlags);
Эта функция помещает "рабочий элемент" (work item) в очередь потока в пуле и тут же возвращает управление. Рабочий элемент — это просто вызов функции (па которую ссылается параметр pfnCallback), принимающей единственный параметр, pvContext. В конечном счете какой-то поток из пула займется обработкой этого элемента, в результате чего будет вызвана Ваша функция. У этой функции обратного вызова, за реализацию которой отвечаете Вы, должен быть следующий прототип:
DWORD WINAPI WorkItemFunc(PVOID pvContext);
Несмотря па то, что тип возвращаемого значения определен как DWORD, на самом деле оно игнорируется.
Обратите внимание, что Вы сами никогда не вызываете CreateThread. Она вызывается из пула потоков, автоматически создаваемого для Вашего процесса, а к функции WorkItemFunc обращается один из потоков этого пула. Кроме того, данный поток не уничтожается сразу после обработки клиентского запроса, а возвращается в пул, оставаясь готовым к обработке любых других элементов, помещаемых в очередь. Ваше приложение может стать гораздо эффективнее, так как Вам больше не придется создавать и уничтожать потоки для каждого клиентского запроса. А поскольку потоки связаны с определенным портом завершения, количество одновременно работающих потоков не может превышать число процессоров более чем в 2 раза. За счет этого переключения контекста происходят реже.
Многое в пуле потоков происходит скрытно от разработчика: QueueUserWorkItem проверяет число потоков, включенных в сферу ответственности компонента поддержки других операций (нс относящихся к вводу-выводу), и в зависимости от текущей нагрузки (количества рабочих элементов в очереди) может передать емудругие потоки.
После этого QueueUserWorkltem выполняет операции, эквивалентные вызову PostQueuedCompletionStatus, пересылая информацию о рабочем элементе в порт за вершения ввода-вывода. В конечном счете поток, ждущий на этом объекте, извлекает Ваше сообщение (вызовом GetQueuedCompletionStatus) и обращается к Вашей функции. После того как она возвращает управление, поток вновь вызывает GetQueuedComplettonStatus, ожидая появления следующего рабочего элемента.
Пул рассчитан на частую обработку асинхронного ввода-вывода — всякий раз, когда поток помещает в очередь запрос на ввод-вывод к драйверу устройства. Пока драйвер выполняет его, поток, поставивший запрос в очередь, не блокируется и может заниматься другой работой. Асинхронный ввод-вывод — ключ к созданию высокоэффективных, масштабируемых приложений, так как позволяет одному потоку обрабатывать запросы от множества клиентов по мере их поступления; ему не приходится обрабатывать их последовательно или останавливаться, ожидая завершения ввода-вывода.
Но Windows накладывает одно ограничение на запросы асинхронного ввода-вывода, если поток, послав такой запрос драйверу устройства, завершается, данный запрос теряется н никакие потоки о его судьбе не уведомляются. В хорошо продуманном пуле, число потоков увеличивается и уменьшается в зависимости от потребностей его клиентов. Поэтому, если поток посылает запрос и уничтожается из-за сокращения пула, то уничтожается и этот запрос. Как правило, это не совсем то, что хотелось бы, и здесь нужно найти какое-то решение.
Если Вы хотите поместить в очередь рабочий элемент, который выдает запрос на асинхронный ввод-вывод, то не сможете передать этот элемент компоненту поддер жки других операций в пуле потоков. Его примет лишь компонент поддержки ввода вывода. Последний включает набор потоков, которые не завершаются, пока есть хотя бы один запрос на ввод-вывод; поэтому для выполнения кода, выдающего запросы на асинхронный ввод-вывод, Вы должны пользоваться только этими потоками.
Чтобы передать рабочий элемент компоненту поддержки ввода-вывода, Вы може те по-прежнему пользоваться функцией QueueUserWorkltem, но в параметре dwFlags
следует указать флаг WT_EXECUTEINIOTHREAD. А обычно Вы будете указывать в этом параметре флаг WT_EXECUTEDEFAULT (0) — он заставляет систему передать рабочий элемент компоненту поддержки других операций (не связанных с вводом-выводом).
В Windows есть функции (вроде RegNotifyChangeKeyValue), которые асинхронно выполняют операции, не относящиеся к вводу-выводу. Они также требуют, чтобы вызывающий поток не завершался преждевременно. С этой целью Вы можете исполь зовать флаг WT_EXECUTETNPERSISTENTTHREAD, который заставляет поток таймера выполнять поставленную в очередь функцию обратного вызова для рабочего элемен та. Так как этот компонент существует постоянно, асинхронная операция в конечном счете обязательно будет выполнена Вы должны позаботиться о том, чтобы функция обратного вызова выполнялась быстро и не блокировала работу компонента поддер жки таймера.
Хорошо продуманный пул должен также обеспечивать максимальную готовность потоков к обработке запросов. Если в пуле четыре потока, а в очереди сто рабочих элементов, то единовременно можно обработать только четыре элемента Это не проблема, если на обработку каждого элемента уходит лишь несколько миллисекунд, но в ином случае программа не сумеет своевременно обслуживать запросы.
Естественно, система не настолько умна, чтобы предвидеть, чем будет заниматься функция Вашего рабочего элемента, но если Вам заранее известно, что па это уйдет длительное время, вызовите QueueUserWorkltem с флагом WT_EXECUTELONGFUNC TION — он заставит пул создать новый поток, если остальные потоки будут в это вре мя заняты. Так, добавив в очередь 10 000 рабочих элементов (с флагом WT_EXECUTE LONGFUNCTION), Вы получите 10 000 новых потоков в пуле. Чтобы избежать этого, делайте перерывы между вызовами QueueUserWorkltem, и тогда часть потоков успсст завершиться до порождения новых.
Ограничение на количество потоков в пуле накладывать нельзя, иначе может воз никать взаимная блокировка потоков. Представьте очередь из 10 000 элементов, заб локированных 10 001-м и ждущих его освобождения.
Установив предел в 10 000, Вы запретите выполнение 10001-го потока, и в результате целых 10 000 потоков оста нутся навечно заблокированными.
Используя функции пула, будьте осторожны, чтобы нс доводить дело до тупико вых ситуаций. Особую осторожность проявляйте, если функция Вашего рабочего эле мента использует критические секции, семафоры, мьютексы и др. — это увеличивает вероятность взаимной блокировки. Вы должны всегда точно знать, поток какого ком понента пула выполняет Ваш код. Также будьте внимательны, если функция рабочего элемента содержится в DLL, которая может быть динамически выгружена из памяти. Поток, вызывающий функцию из выгруженной DLL, приведет к нарушению доступа. Чтобы предотвратить выгрузку DLL при наличии рабочих элементов в очереди, со здайте контрольный счетчик для таких элементов: его значение должно увеличиваться перед вызовом QueueUserWorkItem и уменьшаться после выполнения функции рабо чего элемента. Выгрузка DLL допустима только после того, как этот счетчик обиулится.