Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6266
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

Глава 11. Пулы потоков.docx 245

SubmitThreadpoolWork(g_pWorkItem);

SubmitThreadpoolWork(g_pWorkItem);

SubmitThreadpoolWork(g_pWorkItem);

AddMessage(TEXT("4 tasks are submitted."));

}

//////////////////////////////////////////////////////////////////////////////

void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {

switch (id) { case IDOK: case IDCANCEL:

EndDialog(hWnd, id); break;

case IDC_BTN_START_BATCH: OnStartBatch(); break;

}

}

BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {

// Keep track of main dialog window for error messages g_hDlg = hWnd;

return(TRUE);

}

//////////////////////////////////////////////////////////////////////////////

INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

switch (uMsg) {

chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand); case WM_APP_COMPLETED: {

TCHAR szMsg[MAX_PATH+1]; StringCchPrintf(

szMsg, _countof(szMsg),

TEXT("____Task #%u was the last task of the batch____"), lParam); AddMessage(szMsg);

246 Часть II. Приступаем к работе

// Don't forget to enable the button

Button_Enable(GetDlgItem(hWnd, IDC_BTN_START_BATCH), TRUE);

}

break;

}

return(FALSE);

}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR pCmdLine, int) {

// Create the work item that will be used by all tasks g_pWorkItem = CreateThreadpoolWork(TaskHandler, NULL, NULL); if (g_pWorkItem == NULL) {

MessageBox(NULL, TEXT("Impossible to create the work item for tasks."), TEXT(""), MB_ICONSTOP);

return(-1);

}

DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, Dlg_Proc,

_ttoi(pCmdLine));

// Don't forget to delete the work item CloseThreadpoolWork(g_pWorkItem);

return(0);

}

//////////////////////////////// End of File /////////////////////////////////

Перед созданием главного окна создается один рабочий элемент. Если эта операция заканчивается неудачей, программа завершается, предварительно отображая сообщение с описанием ошибки. По щелчку кнопки Start один и тот же рабочий элемент ставится вызовом функции SubmitThreadpoolWork в очередь пула потоков по умолчанию. При этом кнопка Start деактивируется, чтобы пользователь не запустил обработку еще одного пакета. Функция обратного вызова, исполняемая потоками пула, атомарно увеличивает значение глобального счетчика операций с помощью Interlockedlncrement (см. главу 8) и заносит в журнал записи о начале и завершении обработки.

Непосредственно перед завершением функции TaskHandler значение счетчика операций атомарно уменьшается с помощью функции InterlockedDecrement. При обработке последнего элемента главному окну отправляется специальное сообщение, в результате чего в журнал записыва-

Глава 11. Пулы потоков.docx 247

ется завершающее сообщение, после чего вновь активируется кнопка Start. Узнать о том, что обработка пакета завершена, можно и другим способом. При обработке последнего элемента порождается поток, просто вызывающий функцию

WmtForThreadpooWorkCallbacks(g_pWorkItem, FALSE) Когда эта функция вернет управление, можно считать, что пул потоков обработал все элементы очереди.

Сценарий 2. Вызов функций через определенные интервалы времени

Иногда какие-то операции приходится выполнять через определенные промежутки времени. В Windows имеется объект ядра «ожидаемый таймер», который позволяет легко получать уведомления по истечении заданного времени. Многие программисты создают такой объект для каждой привязанной к определенному времени задаче, но это ошибочный путь, ведущий к пустой трате системных ресурсов. Вместо этого вы можете создать единственный ожидаемый таймер и каждый раз перенастраивать его на другое время ожидания. Однако такой код весьма непрост. К счастью, теперь эту работу можно поручить новым функциям пула потоков. Чтобы исполнить некоторую операцию в заданное время, определите функцию обратного вызова с прототипом следующего вида:

VOID CALLBACK TimeoutCallback(

PTP_CALLBACK_INSTANCE plnstance, // см. раздел "Обработка завершения

// обратного вызова"

PVOID pvContext, PTP_TIMER pTimer);

Далее сообщите пулу потоков, когда следует вызвать эту функцию:

PTP_TIMER CreateThreadpooiTimer(

 

PTP_TIMER_CALLBACK pfnTimerCallback,

 

PVOID pvContext,

 

PTP_CALLBACK_ENVIRON pcbe);

// см. раздел "Настройка пула потоков

Эта функция работает аналогично CreateThreadpoolWork о которой говорилось в предыдущем разделе. Параметр pfnTimerCallback содержит адрес функции, объявленной в соответствии с прототипом TtmeoutCallback, значение параметра pvContext передается функции обратного вызова. При вызове вашей функции TimerCallback в ее параметр рTimer записывается указатель на объект, созданный функцией CreateThreadpoolTimer.

Чтобы зарегистрировать таймер у пула потоков, вызовите функцию SetThreadpoolTimer.

VOID SetThreadpoolTimer(

PTP_TIMER pTimer, PFILETIME pftDueTime,

248 Часть II. Приступаем к работе

DWORD msPeriod,

DWORD msWindowLength);

Ее параметр pTimer задает объект TP_TIMER, созданный функцией CreateThreadpoolTimer. Параметр pftDueTime определяет, когда функция обратного вызова будет исполнена в первый раз. Чтобы указать относительный интервал срабатывания таймера (он будет отсчитываться от времени первого срабатывания), следует задать отрицательное значение (в миллисекундах). Специальное значение -1 вызывает немедленное срабатывание таймера. Абсолютные интервалы задают в виде положительных значений (с шагом в 100 наносекунд, время отсчитывается от 1 января 1600 г).

Чтобы таймер сработал только один раз, установите параметр msPeriod в 0. Если же требуется, чтобы поток периодически вызвал вашу функцию, присвойте параметру msPeriod значение, отличное от нуля (оно задает интервал между вызовами функции TimerCallback). Параметр msWindowLength вносит небольшой элемент случайности в исполнение функции обратного вызова. Эта функция вызывается в любой момент интервала, длительность которого задана параметром msWindowLength; отсчитывается этот интервал от текущего времени. Это удобно, когда в программе есть несколько таймеров, срабатывающих примерно с одинаковой частотой, и требуется избежать одновременного срабатывания таймеров. Таким образом, msWindowLength позволяет обойтись без вызова Sleep со случайным аргументом в функциях обратного вызова.

Кроме этого, параметр msWindowLength позволяет группировать таймеры. Если у вас имеется множество таймеров, срабатывающих примерно в одно время, имеет смысл сгруппировать их во избежание излишних переключений контекста. Предположим, что таймер А срабатывает через 5 мс, а таймер В — через 6 мс. Через 5 мс поток исполняет функцию обратного вызова таймера А, снова засыпает и тут же просыпается, чтобы исполнить функцию обратного вызова таймера В, и т.д. Чтобы избежать переключения контекста и добавления-удаления потоков из пула, можно установить msWindowLength = 2 для таймеров А и В. Тогда пул потоков будет знать, что функция обратного вызова таймера будет выполнена в течение 5-7 мс, начиная с текущего момента, а функция таймера В - в интервале 6-8 мс. В этом случае пул потоков предпочтет сгруппировать эти таймеры и установить для них время срабатывания 6 мс. Таким образом, при срабатывании этих таймеров достаточно будет пробудить один поток, который выполнит функцию обратного вызова сначала таймера А, затем таймера В, и только после этого вернется к ожиданию в пуле. Такая оптимизация особенно важна в случае таймеров с очень близкими временами срабатывания. Учтите также, что чем чаще срабатывают таймеры, тем выше издержки на пробуждение и «усыпление» потоков.

Я хочу подчеркнуть, что после установки таймеры можно изменять, вызывая функцию SetThreadpoolTimer. При этом в параметре pTimer передается указатель на существующий таймер, а в параметрах pftDueTime, msPeriod и

Глава 11. Пулы потоков.docx 249

msWindowLength — новые значения. На самом деле, в pftDueTime можно передать и NULL тогда поток перестанет вызывать вашу функцию TimerCallback. Это хороший способ приостановить таймер, не уничтожая его, особенно удобно это делать из функции обратного вызова.

Функция IsThreadpoolTimerSet позволяет узнать, установлен ли таймер (т.е. не содержится ли в pftDueTime NULL-значение):

BOOL IsThreadpoolTimerSet(PTP_TIMER pti);

Наконец, можно заставить поток ждать срабатывания таймера, вызвав функцию WaitForThreadpoolTimerCallbacks, а уничтожить объект «таймер» — вызовом функции CloseThreadpoolTimer. Эти функции работают аналогично функциям

WaitForThreadpoolWork и CloselhreadpoolWork, описанным выше в этой главе.

Программа-пример TimedMsgBox

Эта программа, 11-TimedMsgBox.exe, показывает, как пользоваться таймерными функциями пула потоков для создания окна, автоматически закрываемого через заданное время в отсутствие реакции пользователя. Файлы исходного кода и ресурсов этой программы находятся в каталоге 11-TimedMsgBox внутри архива, доступного на веб-сайте поддержки этой книги.

При запуске программа присваивает глобальной переменной g_nSecLeft значение 10. Эта переменная определяет, сколько времени (в секундах) программа ждет реакции пользователя на сообщение, показанное в окне. Далее вызывается функция CreateThreadpoolTimer, создающая таймер пула потоков. Затем этот таймер передается функции SetThreadpoolTimer, настраивающей пул на ежесекундный вызов MsgBoxTimeout Инициализировав все необходимые переменные, программа обращается к MessageBox и выводит окно, показанное ниже.

Пока ожидается ответ от пользователя, один из потоков пула каждую секунду вызывает функцию MsgBoxTimeout, которая находит описатель этого окна, уменьшает значение глобальной переменной g_nSecLeft на 1 и обновляет строку в окне. При первом вызове MsgBoxTimeout окно выглядит так.

250 Часть II. Приступаем к работе

При девятом вызове MsgBoxTimeout переменная g_nSecLeft получает значение 1, и тогда MsgBoxTimeout вызывает EndDialog, чтобы закрыть окно. После этого функция MessageBox, вызванная первичным потоком, возвращает управление, и вызывается CloseThreadpoolTimer, заставляющая пул прекратить вызовы MsgBoxTimeout. В результате открывается другое окно, где сообщается о том, что никаких действий в отведенное время не предпринято.

Если же пользователь успел отреагировать на первое сообщение, на экране появляется то же окно, но с другим текстом.

/******************************************************************************

Module: TimedMsgBox.cpp

Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre

******************************************************************************/

#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include <tchar.h>

#include <StrSafe.h>

//////////////////////////////////////////////////////////////////////////////

//The caption of our message box TCHAR g_szCaption[100];

//How many seconds we'll display the message box int g_nSecLeft = 0;

//This is STATIC window control ID for a message box

#define ID_MSGBOX_STATIC_TEXT

0x0000ffff

 

 

//////////////////////////////////////////////////////////////////////////////

VOID CALLBACK MsgBoxTimeoutCallback(

PTP_CALLBACK_INSTANCE pInstance,

Глава 11. Пулы потоков.docx 251

PVOID

pvContext,

PTP_TIMER

pTimer

)

 

{

//NOTE: Due to a thread race condition, it is possible (but very unlikely)

//that the message box will not be created when we get here.

HWND hwnd = FindWindow(NULL, g_szCaption);

if (hwnd != NULL) {

if (g_nSecLeft == 1) {

// The time is up; force the message box to exit. EndDialog(hwnd, IDOK);

return;

}

//The window does exist; update the time remaining. TCHAR szMsg[100];

StringCchPrintf(szMsg, _countof(szMsg),

TEXT("You have %d seconds to respond"), --g_nSecLeft); SetDlgItemText(hwnd, ID_MSGBOX_STATIC_TEXT, szMsg);

}else {

//The window does not exist yet; do nothing this time.

//We'll try again in another second.

}

}

int WINAPI _tWinMain(HINSTANCE, HINSTANCE, PTSTR, int) {

_tcscpy_s(g_szCaption, _countof(g_szCaption), TEXT("Timed Message Box"));

//How many seconds we'll give the user to respond g_nSecLeft = 10;

//Create the threadpool timer object

PTP_TIMER lpTimer =

CreateThreadpoolTimer(MsgBoxTimeoutCallback, NULL, NULL);

if (lpTimer == NULL) { TCHAR szMsg[MAX_PATH];

StringCchPrintf(szMsg, _countof(szMsg),

TEXT("Impossible to create the timer: %u"), GetLastError()); MessageBox(NULL, szMsg, TEXT("Error"), MB_OK | MB_ICONERROR);

return(-1);

}

// Start the timer in one second to trigger every 1 second

252 Часть II. Приступаем к работе

ULARGE_INTEGER ulRelativeStartTime;

ulRelativeStartTime.QuadPart = (LONGLONG) -(10000000); // start in 1 second FILETIME ftRelativeStartTime;

ftRelativeStartTime.dwHighDateTime = ulRelativeStartTime.HighPart; ftRelativeStartTime.dwLowDateTime = ulRelativeStartTime.LowPart; SetThreadpoolTimer(

lpTimer,

&ftRelativeStartTime,

1000, // Triggers every 1000 milliseconds 0 );

// Display the message box

MessageBox(NULL, TEXT("You have 10 seconds to respond"), g_szCaption, MB_OK);

//Clean up the timer CloseThreadpoolTimer(lpTimer);

//Let us know if the user responded or if we timed out MessageBox(

NULL, (g_nSecLeft == 1) ? TEXT("Timeout") : TEXT("User responded"), TEXT("Result"), MB_OK);

return(0);

}

//////////////////////////////// End of File /////////////////////////////////

Прежде чем перейти к разбору следующего сценария, позвольте мне обратить ваше внимание на пару моментов. Установка периодически срабатывающего таймера гарантирует добавление рабочих элементов в очередь пула потоков с заданным интервалом. Если таймер срабатывает каждые 10 секунд, то и ваша функция обратного вызова будет исполняться каждые 10 секунд. Учтите, что в этом может быть задействовано сразу несколько потоков пула, поэтому исполнение отдельных частей функции обратного вызова может требовать синхронизации. Имейте также в виду, что при перегрузке пула, например из-за нехватки потоков, обработка элементов очереди может задерживаться.

Если такая ситуация вас не устраивает, и вы хотите, чтобы следующий элемент ставился в очередь не раньше чем через 10 секунд после обработки предыдущего, можно пойти другим путем и создать интеллектуальный таймер однократного срабатывания следующим образом.

■ Создайте таймер как обычно вызовом функции CreateThreadpoolTimer.

Глава 11. Пулы потоков.docx 253

Вызовите SetThreadpoolTimer, при этом в параметре msPeriod передайте 0, чтобы настроить таймер на однократное срабатывание.

После завершения обработки можно перезапустить таймер, повторив шаг 2

И последнее: чтобы корректно остановить таймер, перед вызовом CloseThreadpoolTimer вызовите функцию WaitForThreadpoolTimerCallbacks, передав TRUE

в последнем параметре - так вы запретите пулу потоков дальнейшую обработку элементов для этого таймера. Если вы забудете сделать это, будет исполнена функция обратного вызова, и при вызове SetThreadpoolTimer вы получите исключение.

Заметьте, что для создания таймера однократного срабатывания можно вызвать SetThreadpoolTimer с параметром msPeriod, равным 0. Функция обратного вызова должна уничтожать таймер вызовом CloseThreadpoolTimer до своего завершения, чтобы наверняка освободить ресурсы, занятые пулом потоков.

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

Майкрософт обнаружила, что во многих приложениях потоки порождаются только для того, чтобы ждать на тех или иных объектах ядра. Как только объект освобождается, поток посылает уведомление и снова переходит к ожиданию того же объекта. Некоторые разработчики умудряются писать программы так, что в них создается несколько потоков, ждущих один объект. Это невероятное расточительство системных ресурсов. Конечно, издержки от создания потоков существенно меньше, чем от создания процессов, но и потоки не воздухом питаются. У каждого из них свой стек, не говоря уж об огромном количестве команд, выполняемых процессором при создании и уничтожении потока. Поэтому надо стараться сводить любые издержки к минимуму.

Если вы хотите зарегистрировать рабочий элемент так, чтобы он обрабатывался при освобождении какого-либо объекта ядра, используйте еще одну новую функцию пула потоков:

VOID CALLBACK WaitCallback(

 

PTP_CALLBACK_INSTANCE pInstance,

// см. раздел "Обработка завершения

 

// обратного вызова"

PVOID Context,

 

PTP_WAIT Wait,

 

ТР WAIT_RESULT WaitResult);

 

Далее создайте объект, на котором пул будет ждать, вызвав CreateThreadpoolWait.

PTP_WAIT CreateThreadpoolWait(

254 Часть II. Приступаем к работе

PTP_WAIT_CALLBACK

pfnWaitCallback,

 

PVOID

pvContext,

 

PTP_CALLBACK_ENVIRON

pcbe);

// см. раздел "Настройка пула потоков"

Подготовив все необходимое, свяжите объект ядра с только что созданным объектом, вызвав функцию:

VOID SetThreadpoolWait(

PTP_WAIT pWaitltem,

HANDLE hObject,

PFILETIME pftTimeout);

Ясно, что параметр идентифицирует объект, созданный функцией CreateThreadpoolWait, а параметр hObject — некоторый объект ядра, при освобождении которого пул потоков вызовет вашу функцию WaitCallback. Последний параметр, pftTimeout, определяет длительность ожидания пулом освобождения объекта ядра. Если установить его в 0, пул вовсе не будет ждать, отрицательные значения задают относительное время, положительные — абсолютное время, NULL заставит пул ждать бесконечно.

«За кулисами» пул потоков вызывает функцию WaitForMultipleObjects (см. главу 9), передавая ей набор описателей, зарегистрированных при помощи SetThreadpoolWa.it, и FALSE в параметре bWait All. Таким образом, поток в пуле будет пробуждаться при освобождении любого из объектов, описатели которых были переданы WaitForMultipleObjects. Поскольку WaitForMultipleObjects принимает до 64 описателей, т.е. может ждать максимально на 64 объектах (как задано значением MAXIMUM_WAIT_OBJECTS, см. главу 9), получается, что пул может использовать один поток в расчете на 64 объекта ядра, т.е. работает довольно эффективно.

Кроме того, WaitForMultipleObjects запрещает многократную передачу одного и того же описателя, поэтому каждый описатель следует регистрировать с помощью SetThreadpoolWait не более одного раза. Однако можно создать с помощью DuplicateHandle копию описателя и зарегистрировать ее независимо от оригинала.

При освобождении объекта ядра или истечении срока ожидания один из потоков пула пробуждается и вызывает вашу функцию WaitCallback (см. выше). Большинство параметров этой функции понятно без комментариев, за исключением последнего, WaitResult. Его тип — TP_WAIT_RESULT (он определен как DWORD-значение). Этот параметр указывает причину вызова функции WaitCallback и принимает одно из значений, перечисленных в табл. 11-1.

Соседние файлы в предмете Программирование на C++