WaitForSingleObject (hEvent,
INFINITE); p>
for (int i = 0; i <
_SOME_MAGIC_VALUE; I ++){ p>
QueueUserAPC (APCProc,
hThread, i); p>
) p>
WaitForSingleObject (hThread,
1000); p>
CloseHandle (hThread); p>
return 0; p>
) p>
Незважаючи на простоту, приклад досить
складний, і не завжди можна передбачити, що буде на екрані після його
завершення. Давайте розберемо його у формі питання-відповідь. P>
Чому я Синхронізація потоки за допомогою події? p>
Якщо цього не зробити, то всі п'ять APC-запитів
виконуватися ще до того, як функція потоку trd1 отримає управління. Це
відбудеться тому, що сама система в процесі створення потоку використовує
механізм APC-викликів для ініціалізації потоку. З його допомогою, наприклад,
відбувається виклик всіх функцій DllMain з параметром DLL_THREAD_ATTACH, якщо,
звичайно, ви не викликали DisableThreadLibraryCalls для будь-якої бібліотеки. p>
Чому на екран виводиться дивний результат? p>
Thread id is 0x68c p>
APC Proc # 0 threadid: 68c p>
0 p>
APC Proc # 1 threadid: 68c p>
APC Proc # 2 threadid: 68c p>
APC Proc # 3 threadid: 68c p>
APC Proc # 4 threadid: 68c p>
1 p>
Як я вже казав, для кожного потоку система
організовує чергу з APC-запитів, так що в момент обробки першого запиту
потоком система встигає додати в чергу всі інші запити, які і
виконуються при наступному виклику SleepEx. Результат сильно залежить від
завантаженості системи, так що у вас він може бути іншим: наприклад, всі
запити встигнуть виконатися за одну ітерацію. p>
Чому, якщо закоментувати тіло APCProc, на екран
виводиться наступне? p>
Thread id is 0x7b4 p>
0 p>
1 p>
2 p>
3 p>
4 p>
Так як тепер ця процедура фактично нічого не робить,
система не встигає додати новий запит в чергу до завершення обробки
попереднього, так що кожен SleepEx обробляє «свій» APC-запит. p>
Тепер вам повинно бути зрозуміло, як використовувати
даний механізм для організації пулу потоків. Ось приблизний сценарій для
фіксованої кількості потоків в пулі: в головному потоці програми створюються
кілька робочих потоків, кожен з яких відразу переходить у стан
тривожного очікування спеціально створеного основним потоком події. Коли
приходить клієнтський запит, головний потік передає APC-запит одного з робітників
потоків. Робочий потік прокидається і виконує функцію, яку було поставлено в чергу
головним потоком. При цьому він не залишає функції WaitForSingleObjectEx. Тобто
виконання APC-запиту здійснюється як би всередині функції
WaitForSingleObjectEx. Після завершення виконання запиту управління
передається функції WaitForSingleObjectEx, яка, в свою чергу, передає
управління основному коду потоку, повертаючи WAIT_IO_COMPLETION. p>
При отриманні керування робочий потік повинен
проаналізувати значення, повернене цією функцією. Якщо воно дорівнює
WAIT_IO_COMPLETION, то причиною виходу з функції WaitForSingleObjectEx було
завершення обробки APC-запиту - потік при цьому повинен знову перейти в стан
очікування події. Якщо ж повертається значення WAIT_OBJECT_0, то причиною
виходу була установка події в сигнальний стан головним потоком
додатки. При цьому робочий потік повинен завершитися. p>
Це дуже проста схема (наприклад, робочі потоки замість
очікування можуть виконувати якусь іншу корисну роботу), але вона досить
непогано пояснює механізм використання APC для організації пулу. p>
SetWaitableTimer
h2>
Ця функція з'явилася з версії 4.0. Вона дозволяє
активувати таймер, який через заданий період часу в 100-наносекундних
інтервалах або при настанні заданого абсолютного часу переходить в
сигнальне стан. Крім цього, можна вказати процедуру завершення, яка
буде викликана за допомогою APC-запиту в цьому потоці. Для завершення процедури
можна вказати додатковий параметр. Функція хороша тим, що вона не прив'язана
до вікон і циклу вибірки повідомлень, як, наприклад, SetTimer. З її допомогою можна
використовувати таймери в будь-яких додатках, включаючи консольні та сервіси. Однак у
SetWaitableTimer є і деякі недоліки: p>
функція завершення завжди викликається в потоці,
що викликала SetWaitableTimer; p>
потік повинен бути в стані тривожного очікування,
щоб обробити APC-запит; p>
другу APC-запит почне оброблятися тільки після закінчення
обробки попереднього запиту, тобто запити обробляються послідовно. p>
Всі ці проблеми вирішує об'єкт "черга
таймерів ", про який мова піде пізніше. p>
Порт завершити введення/виводу
h2>
Це, безумовно, одна з наймогутніших і складних об'єктів
виконавчої системи. Він спеціально призначений для оптимізації обробки
клієнтських запитів в серверних застосуваннях. Він не тільки організовує чергу
запитів, а й ефективно управляє їх обробкою. p>
Основна ідея порту завершення введення/виводу (в
Надалі просто порту) полягає в тому, щоб ефективно витрачати
процесорний час при обробці клієнтських запитів. Обробка повинна вестися
паралельно декількома потоками, але суворо до певного моменту, коли
число потоків дорівнюватиме максимальному значенню. Після цього нові потоки
перестають створюватися, а запити ставляться в чергу до існуючих потоків.
Давайте розберемося в цьому детальніше. P>
Як працює порт h2>
При створенні порту вказується максимальна кількість
активних потоків, здатних обробляти клієнтські запити паралельно. Так
як кількість реально працюючих паралельно потоків на комп'ютері одно
кількості процесорів, то вказівка більшого максимальної кількості активних
потоків не вигідно. Чому? Справа в тому, що для виконання декількох потоків
на одному процесорі, системі доводиться постійно перемикати процесварок між
потоками, емуліруя, таким чином, паралельність, однак це перемикання,
зване перемиканням контекстів - досить дорога операція. Уникнути її
можна тільки одним способом - не створювати паралельно працюють потоки в
кількості більшій, ніж кількість процесорів. Таким чином, при створенні порту,
здавалося б, треба вказувати в якості максимальної кількості активних
потоків число процесорів в системі, але тут є одна тонкість. Припустимо, у
нас однопроцесорний комп'ютер і, відповідно, клієнтські запити ми
обробляємо в одному потоці. Що буде, якщо клієнтський запит прийде в момент
виконання синхронної операції з диском або в момент очікування якого-небудь
об'єкта цим потоком? Він буде чекати, поки потік не закінчить свою роботу, але
адже процесор у цей час не діє, тому що потік заблокований на
синхронної операції або на який-небудь об'єкт. Коли процесор не діє, а клієнтський
запит не обробляється - це погано. Ми приходимо до висновку про те, що завжди
повинен існувати резервний потік, який підхоплював б запити в момент,
коли «основний» потік виконує блокують операції, і процесор
не діє. p>
Робота з файлами (в самому широкому сенсі слова) дуже
тісно пов'язана з багатопоточність і обробкою запитів на сервері. Сокет або
pipe - це теж файли. Щоб обробляти запити через ці канали паралельно,
потрібний порт. Давайте розглянемо функцію створення порту і зв'язку його з файлом
(зачем-то розробники з Microsoft об'єднали дві ці функції в одну; в
виконавчій системі ці дві функції виконують сервіси NtCreateIoCompletion і
NtSetInformationFile, відповідно). P>
HANDLE CreateIoCompletionPort ( p>
HANDLE FileHandle,// хендл файлу p>
HANDLE ExistingCompletionPort,
//Хендл порту завершити введення/виводу p>
ULONG_PTR CompletionKey,// ключ завершення p>
DWORD NumberOfConcurrentThreads//
Максимальна кількість паралельних потоків p>
); p>
Для простого створення порту потрібно як перший
параметра передати INVALID_HANDLE_VALUE, а в якості другого і третього - 0.
Для зв'язування файлу з портом потрібно вказати перші три параметри і
проігнорувати четвертий. p>
Після того, як файл (під файлом тут мається на увазі
об'єкт підсистеми Win32, який реалізується за допомогою об'єкту "файл
виконавчої системи ", до таких відносяться файли, сокети, поштові
ящики, іменовані канали та ін.) пов'язаний з портом, закінчення всіх асинхронних
запитів вводу/виводу потрапляють в чергу порту і можуть бути оброблені пулом
потоків. Наступні функції можуть бути використані з портом для завершення
обробки асинхронних операцій введення/виводу: p>
ConnectNamedPipe - очікує підключення клієнта до
іменовані канали. p>
DeviceIoControl - низькорівневий введення/виведення. p>
LockFileEx - блокування регіону файлу. p>
ReadDirectoryChangesW - очікування змін до
директорії. p>
ReadFile - читання файлу. p>
TransactNamedPipe - Комбіноване читання і запис за
іменовані канали, які здійснюються за одну мережеву операцію. p>
WaitCommEvent - очікування події послідовного
інтерфейсу (СОМ-порт). p>
WriteFile - запис у файл. p>
Якщо ви не хочете, щоб закінчення асинхронного
введення/виводу оброблялося портом (наприклад, коли вам не важливий результат
операції), потрібно використати такий трюк [1]. Потрібно встановити поле hEvent
структури OVERLAPPED рівним описувач події до встановленого першого бітом.
Робиться це приблизно так: p>
OVERLAPPED
ov = (0); p>
ov.hEvent
= CreateEvent (...); p>
ov.hEvent
= (HANDLE) ((DWORD_PTR) (ov.hEvent) | 1); p>
І не забувайте скидати молодший біт при закритті
Хендли події. p>
Додавати потік до пулу (підключати його до обробки
запитів) можна за допомогою наступної функції: p>
BOOL
GetQueuedCompletionStatus ( p>
// хендл порту завершити введення/виводу p>
HANDLE CompletionPort, p>
// кількість переданих байт p>
LPDWORD lpNumberOfBytes, p>
// ключ завершення p>
PULONG_PTR lpCompletionKey, p>
// структура OVERLAPPED p>
LPOVERLAPPED * lpOverlapped, p>
// значення таймауту p>
DWORD dwMilliseconds p>
); p>
Ця функція блокує потік до тих пір, поки порт не
передасть потоку пакет запиту або не закінчиться таймаут. p>
Помістити пакет запиту в порт можна за допомогою функції
PostQueuedCompletionStatus. P>
BOOL PostQueuedCompletionStatus ( p>
HANDLE CompletionPort,
//Хендл порту завершити введення/виводу p>
DWORD dwNumberOfBytesTransferred,// кількість переданих байт p>
ULONG_PTR dwCompletionKey,
//Ключ завершення p>
LPOVERLAPPED lpOverlapped
//Структура OVERLAPPED p>
); p>
Пакет запиту не обов'язково повинен бути структурою
OVERLAPPED або похідною від неї [2]. P>
Давайте зберемо всю інформацію воєдино. Порт
завершення - об'єкт, що організує кілька черг з клієнтських запитів і
потоків, їх обробляють. Потік додається в чергу очікують на запит
потоків порту при виконанні функції GetQueuedCompletionStatus. При надходженні
запиту порт розблокує перший потік в черзі чекають потоків і передає йому
цей запит (у вигляді структури OVERLAPPED і ключа завершення). Потік при цьому
переміщається в чергу активних потоків (число активних потоків збільшується
на 1). Припустимо, у нас Максимальна кількість активних потоків дорівнює 1, тоді
при надходженні запиту інший потік з черги чекають
активований не буде. Після обробки клієнтського запиту потік знову викликає
GetQueuedCompletionStatus і ставиться в початок переліку адрес потоків. Чому
потік ставиться саме в початок списку? Справа в тому, що потоки беруться з початку
списку, і при низькій активності можуть використовуватися не всі потоки. При цьому
стеки і контексти не використовуваних потоків можуть бути вивантажені на диск за
непотрібність. p>
Якщо в процесі обробки запиту потік звернувся до
блокує функції, число активних потоків зменшується на 1, як якщо б потік
перейшов знову в чергу очікують потоків. Це дає можливість при приході
наступного клієнтського запиту задіяти наступний потік з черги
що очікують. Коли перший потік закінчить блокує операцію, число активних
потоків перевищить максимальний, і при наступному виклику функції
GetQueuedCompletionStatus один з цих потоків заблокується, а друга отримає
пакет запиту (якщо він є). p>
Черга p>
Запис додається при: p>
Запис видаляється за: p>
Список пристроїв,
асоційованих з портом p>
виклик
CreateIoCompletionPort p>
закриття хенду файлу p>
Черга клієнтських запитів
(FIFO) p>
завершення асинхронної
операції файлу, що асоціюється з портом, або виклику функції
PostQueuedCompletionStatus p>
передачі портом запиту
потоку на обробку p>
Черга очікують потоків p>
виконанні функції
GetQueuedCompletionStatus p>
початку обробки
клієнтського запиту потоком p>
Список працюючих потоків p>
початку обробки
клієнтського запиту потоком p>
виклик потоком
GetQueuedCompletionStatus або будь-яку блокує функції p>
Список призупинених
потоків p>
виклик потоком якої-небудь
блокує функції p>
виході потоку з будь-якої
блокує функції p>
Таблиця 1. Список черг порту завершення
введення/виводу [1]. p>
недокументовані можливості порту і його
низькорівневе пристрій p>
Як завжди це буває в Microsoft, порт завершення
володіє багатьма недокументовані можливості: p>
У порту завершити введення/виводу може бути ім'я, і
відповідно, він доступний для інших процесів. Абсолютно незрозуміло, чому
розробники вирішили приховати цю, на мій погляд, потрібну особливість порту. Назва
можна задати в параметрі ObjectAttributes функції NtCreateIoCompletion. p>
Друга особливість випливає з першої: з портом може
бути пов'язаний дескриптор безпеки, який також задається в параметрі
ObjectAttributes функції NtCreateIoCompletion. p>
Відкривається порт за допомогою функції NtOpenIoCompletion.
При виконанні функції потрібно вказати ім'я порту і рівень доступу. Як рівня
доступу можна вказувати всі стандартні і наступні спеціальні права [2]
(таблиця 2). p>
Символічне позначення p>
Константа p>
Опис p>
IO_COMPLETION_QUERY_STATE p>
1 p>
Необхідний для запиту
стану об'єкта "порт" p>
IO_COMPLETION_MODIFY_STATE p>
2 p>
Необхідний для зміни
стану об'єкта "порт" p>
Таблиця 2. p>
У порту можна запитувати кількість необроблених
запитів за допомогою функції NtQueryIoCompletion. Хоча в [3] стверджується, що
ця функція визначає, чи знаходиться порт в сигнальному стані, насправді
вона повертає кількість клієнтських запитів у черзі. Це досить важлива
інформація, яку чомусь знову вирішили від нас приховати. p>
Давайте більш детально розглянемо, як створюється і
функціонує порт завершити введення/виводу [4]. p>
При створенні порту функцією CreateIoCompletionPort
викликається внутрішній сервіс NtCreateIoCompletion. Об'єкт "порт"
представлений наступною структурою [5]: p>
typedef stuct _IO_COMPLETION p>
( p>
KQUEUE Queue; p>
) IO_COMPLETION; p>
Тобто, по суті, об'єкт "порт
завершення "є об'єктом" черга виконавчої системи "
(KQUEUE). Ось як представлена чергу: p>
typedef stuct _KQUEUE p>
( p>
DISPATCHER_HEADER Header; p>
LIST_ENTRY EnrtyListHead;// чергу пакетів p>
DWORD CurrentCount; p>
DWORD MaximumCount; p>
LIST_ENTRY
ThreadListHead;// чергу очікують потоків p>
) KQUEUE; p>
Отже, для порту виділяється пам'ять, і потім відбувається
його ініціалізація за допомогою функції KeInitializeQueue. (все, що стосується
такого супернизькою пристрої порту, взято з [4], інше - з DDK і [3 ]). p>
Коли відбувається зв'язування порту з об'єктом
"файл", Win32-функція CreateIoCompletionPort викликає
NtSetInformationFile. Клас інформації для цієї функції встановлюється як
FileCompletionInformation, а як параметр FileInformation передається
покажчик на структуру IO_COMPLETION_CONTEXT [5] або
FILE_COMPLETION_INFORMATION [3]. P>
typedef struct _IO_COMPLETION_CONTEXT p>
( p>
PVOID Port; p>
PVOID Key; p>
) IO_COMPLETION_CONTEXT; p>
typedef struct _FILE_COMPLETION_INFORMATION p>
( p>
HANDLE IoCompletionHandle; p>
ULONG CompletionKey; p>
) FILE_COMPLETION_INFORMATION, * PFILE_COMPLETION_INFORMATION; p>
Покажчик на цю структуру заноситься в поле
CompletionConext структури FILE_OBJECT (зміщення 0x6C). P>
Після завершення асинхронної операції введення/виводу для
асоційованого файлу диспетчер введення/виводу перевіряє поле CompletionConext
і, якщо воно не дорівнює 0, створює пакет запиту (зі структури OVERLAPPED та ключа
завершення) і поміщає його в чергу за допомогою виклику KeInsertQueue. Коли
потік викликає функцію GetQueuedCompletionStatus, насправді викликається
функція NtRemoveIoCompletion. NtRemoveIoCompletion перевіряє параметри і
викликає функцію KeRemoveQueue, яка блокує потік, якщо в черзі
відсутні запити, або поле CurrentCount структури KQUEUE більше або дорівнює
MaximumCount. Якщо запити є, і число активних потоків менше максимального,
KeRemoveQueue видаляє викликав її потік з черги чекають потоків і
збільшує число активних потоків на 1. При занесенні потоку в чергу
що очікують потоків поле Queue структури KTHREAD (зміщення 0xE0) встановлюється
рівним адресою черги (порту завершення). Навіщо це потрібно? Коли викликаються
функції блокування потоку (WaitForSingleObject тощо), планувальник перевіряє
це поле, і якщо воно не дорівнює 0, викликає функцію KeActivateWaiterQueue,
яка зменшує число активних потоків порту на 1. Коли потік пробуджується
після виклику блокуючих функцій, планувальник виконує ті самі дії, тільки
викликає при цьому функцію KeUnwaitThread, яка збільшує лічильник активних
потоків на 1. p>
Коли ви ставите запит в порт завершення функцією
PostQueuedCompletionStatus, насправді викликається функція NtSetIoCompletion,
яка після перевірки параметрів і перетворення Хендли порту в покажчик,
викликає KeInsertQueue. p>
Організуємо пул
h2>
Отже, ми знаємо, як працює порт завершення
введення/виводу, коли потоки додаються в пул і коли видаляються. Але скільки
потоків має бути в пулі? У два рази більше, ніж число процесорів. Це дуже
загальна рекомендація, і для деяких завдань вона не підходить. За великим рахунком
є тільки два критерії, за якими можна визначати, потрібно створювати новий
потік чи ні. Ці критерії - завантаженість процесора і число пакетів
запитів. Якщо число пакетів перевищує певну кількість, і завантаженість
процесора невисока, є сенс створити нову гілку. Якщо пакетів мало, або
процесор зайнятий більш ніж на 90 відсотків, додатковий потік створювати не
слід. Видаляти потік з пулу потрібно, якщо він давно не обробляв клієнтські
запити (просто підрахувати, скільки разів GetQueuedCompletionStatus повернула
управління з таймаут). При видаленні потоку потрібно стежити, щоб закінчилися
всі асинхронні операції введення/виводу, початі цим потоком. p>
Треба сказати, що визначення завантаженості
процесора, кількості пакетів в черзі порту і наявності у потоку незавершені
операцій введення/виводу - завдання не найпростіші. Наприклад, ви можете
використовувати WMI для визначення завантаженості процесора, але при цьому не
зможете визначити, чи є у потоку незавершені операції введення/виводу. Нижче
я наведу функції отримання перерахованих вище показників тільки
недокументовані способами (тут використовується заголовки ntdll.h з
[3]): p>
// Функція одержання
завантаженості процесора p>
double GetCPUUsage () p>
( p>
# define Li2Double (x)
((double) ((x). HighPart) * 4.294967296E9 p>
+ (double) ((x). LowPart)) p>
p>
typedef NTSTATUS (NTAPI
ZwQuerySystemInformation_t) ( p>
IN
NT:: SYSTEM_INFORMATION_CLASS SystemInformationClass, p>
OUT PVOID SystemInformation,
p>
IN ULONG
SystemInformationLength, p>
OUT PULONG ReturnLength
OPTIONAL p>
); p>
static
ZwQuerySystemInformation_t * ZwQuerySystemInformation = 0; p>
if (! ZwQuerySystemInformation) p>
( p>
ZwQuerySystemInformation =
(ZwQuerySystemInformation_t *) GetProcAddress ( p>
GetModuleHandle (_T ( "ntdll.dll")),
_T ( "NtQuerySystemInformation ")); p>
) p>
double dbIdleTime = 0; p>
static NT:: LARGE_INTEGER
liOldIdleTime = (0, 0); p>
static NT:: LARGE_INTEGER
liOldSystemTime = (0, 0); p>
// Отримуємо число процесорів p>
NT:: SYSTEM_BASIC_INFORMATION
sysinfo = (0); p>
NT:: NTSTATUS status =
ZwQuerySystemInformation (NT:: SystemBasicInformation, p>
& sysinfo, sizeof
sysinfo, 0); p>
p>
if (status! = NO_ERROR) p>
return -1; p>
p>
// Отримуємо системний час p>
NT:: SYSTEM_TIME_OF_DAY_INFORMATION timeinfo =
(0); p>
status =
ZwQuerySystemInformation (NT:: SystemTimeOfDayInformation, p>
& timeinfo, sizeof
timeinfo, 0); p>
p>
if (status! = NO_ERROR) p>
return -1; p>
// Отримуємо час простою p>
NT:: SYSTEM_PERFORMANCE_INFORMATION perfinfo = (0); p>
status = ZwQuerySystemInformation (NT:: SystemPerformanceInformation,
p>
& perfinfo, sizeof
perfinfo, 0); p>
p>
if (status! = NO_ERROR) p>
return -1; p>
// якщо це перший дзвінок, значення обчислити не можна p>
if (liOldIdleTime.QuadPart! = 0) p>
( p>
// Час простою p>
dbIdleTime =
Li2Double (perfinfo.IdleTime) - Li2Double (liOldIdleTime); p>
// Системний час p>
const double dbSystemTime =
Li2Double (timeinfo.CurrentTime) p>
--
Li2Double (liOldSystemTime); p>
dbIdleTime = dbIdleTime /
dbSystemTime; p>
dbIdleTime = 100.0 --
dbIdleTime * 100.0 p>
/
(double) sysinfo.NumberProcessors + 0.5; p>
) p>
// зберігаємо отримані значення p>
liOldIdleTime =
perfinfo.IdleTime; p>
liOldSystemTime = timeinfo.CurrentTime; p>
// Якщо це перший дзвінок, отримуємо завантаженість
CPU за останні p>
// 200 мілісекунди p>
if (dbIdleTime == 0) p>
( p>
Sleep (200); p>
dbIdleTime = GetCPUUsage (); p>
) p>
p>
return dbIdleTime; p>
) p>
// Повертає true, якщо
потік має незавершені операції введення/виводу p>
bool HasThreadIoPending (HANDLE hThread = GetCurrentThread ()) p>
( p>
typedef NTSTATUS (NTAPI
ZwQueryInformationThread_t) ( p>
IN HANDLE ThreadHandle, p>
IN NT:: THREADINFOCLASS
ThreadInformationClass, p>
OUT PVOID ThreadInformation,
p>
IN ULONG
ThreadInformationLength, p>
OUT PULONG ReturnLength
OPTIONAL p>
); p>
static
ZwQueryInformationThread_t * ZwQueryInformationThread = 0; p>
if (! ZwQueryInformationThread) p>
( p>
ZwQueryInformationThread =
(ZwQueryInformationThread_t *) GetProcAddress ( p>
GetModuleHandle (_T ( "ntdll.dll")),
_T ( "NtQueryInformationThread ")); p>
) p>
ULONG io = 0; p>
ZwQueryInformationThread (hThread, NT:: ThreadIsIoPending, & io, 4,
0); p>
return io> 0; p>
) p>
// Повертає кількість
необроблених запитів у черзі порту p>
DWORD GetIoCompletionLen (HANDLE hIoPort) p>
( p>
typedef NTSTATUS (NTAPI
ZwQueryIoCompletion_t) ( p>
IN HANDLE
IoCompletionHandle, p>
IN
NT:: IO_COMPLETION_INFORMATION_CLASS IoCompletionInformationClass, p>
OUT PVOID
IoCompletionInformation, p>
IN ULONG IoCompletionInformationLength,
p>
OUT PULONG ResultLength
OPTIONAL p>
); p>
static ZwQueryIoCompletion_t *
ZwQueryIoCompletion = 0; p>
if (! ZwQueryIoCompletion) p>
( p>
ZwQueryIoCompletion =
(ZwQueryIoCompletion_t *) GetProcAddress ( p>
GetModuleHandle (_T ( "ntdll.dll")),
_T ( "NtQueryIoCompletion ")); p>
) p>
NT:: IO_COMPLETION_BASIC_INFORMATION ioinfo = (0); p>
DWORD dwRetLen = 0; p>
ZwQueryIoCompletion (hIoPort,
NT:: IoCompletionBasicInformation, p>
& ioinfo, sizeof ioinfo,
& dwRetLen); p>
p>
return ioinfo.SignalState; p>
) p>
Як бачите, не проста ця справа - створювати
ефективний пул потоків, проте дещо хлопці з Microsoft можуть нам
запропонувати. У Windows2000 з'явилися нові функції, які повністю беруть на
себе всю чорнову роботу по створенню та видаленню потоків в пулі. Про них --
наступний розділ. p>
Вбудована підтримка пулу потоків
p>
У Windows 2000 з'явилися нові функції, які
умовно можна розділити на чотири групи: p>
приміщення запиту в чергу; p>
виклик функції при закінченні асинхронної операції
введення/виводу; p>
періодичний виклик функції; p>
виклик функції при переході об'єкта в сигнальне
стан. p>
Розглянемо їх по порядку. p>
Приміщення запиту в чергу
p>
Надіслати на виконання потоку з пула яку-небудь
функцію можна за допомогою сервісу QueueUserWorkItem. Ця на вигляд проста функція
робить дуже багато: вона створює порт завершення введення/виводу, створює і
знищує потоки в пулі і багато іншого. Ось її опис: p>
BOOL QueueUserWorkItem ( p>
LPTHREAD_START_ROUTINE
Function,// адреса функції p>
PVOID Context,
//Довільний параметр p>
ULONG Flags// прапори виконання p>
); p>
QueueUserWorkItem поміщає пакет запиту у вигляді адреси
функції і довільного параметра в чергу запитів порту завершення і відразу
ж повертає керування. Ось як виглядає функція, яка буде викликана одним
з потоків в пулі: p>
DWORD
WINAPI ThreadProc ( p>
LPVOID lpParameter// довільний параметр p>
); p>
Її прототип нічим не відрізняється від стартової процедури
потоку, так що тут вам все повинно бути ясно. Набагато цікавіше знати, що
ховається всередині функції QueueUserWorkItem. Давайте розбиратися. P>
При першому приміщенні запиту кількість потоків в пулі
дорівнює нулю, так що QueueUserWorkItem доводиться створювати потік і порт завершення.
Потім в порт поміщається пакет запиту, а потік викликає функцію
GetQueuedCompletionStatus. Після обробки запиту потік не руйнується, а
залишається ще якийсь час у пулі, так що наступний запит буде опрацьовано
набагато швидше. Якщо ви відправляєте запити занадто часто, і кількість
необроблених пакетів збільшується, QueueUserWorkItem створить для дзвінка
функції новий потік. Максимальна кількість потоків в пулі дорівнює кількості
процесорів, що не дуже добре, але є спосіб змусити функцію завжди
створювати новий потік. p>
ПРИМІТКА p>
Ті з вас, хто читав статтю Дж. Ріхтера
«New Windows 2000 Pooling Functions Greatly Simplify Thread Management» з
квітневого MSJ за 1999 рік, можуть посперечатися зі мною щодо розміру пулу. У
статті вказується, що кількість потоків у ньому дорівнює подвоєному кількості
процесорів в системі, однак це не так. Ви можете власноруч в цьому
переконатися, поставивши breakpoint на функцію _RtlpInitializeWorkerThreadPool
(адреса 0x77FA95CD на Windows 2000 Professional SP3) і викликавши функцію
QueueUserWorkItem. P>
Розглянемо прапори функції QueueUserWorkItem. p>
Константа p>
Значення p>
Опис p>
WT_EXECUTEDEFAULT p>
0 p>
Запит поміщається в простій
робочий потік p>
WT_EXECUTEINIOTHREAD p>
1 p>
Запит поміщається в потік
введення/виводу p>
WT_EXECUTEINPERSISTENTTHREAD p>
0x80 p>
Запит поміщається в потік,
який