Издание четвертое windows ® для профессионалов создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows



Pdf просмотр
страница27/68
Дата28.11.2016
Размер3.57 Mb.
Просмотров12678
Скачиваний0
1   ...   23   24   25   26   27   28   29   30   ...   68
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
мент данных. Скопировав элемент в массив очереди, функция возвращает управление. По окончании этой операции
Append вызывает ReleaseMutex, чтобы и другие потоки могли получить доступ к очереди. Остальной код в методе
Append отвечает за обработку ошибок и неудачных вызовов.
Теперь посмотрим, как серверный поток вызывает метод
Remove для выборки элемента из очереди. Сначала этот метод должен убедиться, что вызывающий поток получил монопольный доступ к очереди и что в ней есть хотя бы один элемент. Разумеется, серверному потоку нет смысла пробуждаться, если очередь пуста. Поэтому метод предварительно обращается к WaitForMultipleObjects, передавая ей описатели мьютекса и семафора. И только после освобождения обоих объектов серверный поток может пробудиться.
Если возвращается WAIT_OBJECT_0, значит, поток получил монопольный доступ к очереди ив ней есть хотя бы один элемент. В этот момент программа извлекает из массива элемент с индексом 0, а остальные элементы сдвигает вниз на одну позицию.
Это, конечно, не самый эффективный способ реализации очереди, так как требует слишком большого количества операций копирования в памяти, но наша цель заключается лишь в том, чтобы продемонстрировать синхронизацию потоков. По окончании этих операций вызывается
ReleaseMutex, и очередь становится доступной другим потокам.
Заметьте, что объект семафор отслеживает, сколько элементов находится в очереди. Вы, наверное, сразу же поняли, что это значение увеличивается, когда метод
Append вызывает ReleaseSemaphore после добавления нового элемента к очереди. Но как оно уменьшается после удаления элемента из очереди, уже не столь очевидно. Эта операция выполняется вызовом
WaitForMultipleObjects из метода Remove. Тут надо вспомнить, что побочный эффект успешного ожидания семафора заключается в уменьшении его счетчика на 1. Очень удобно для нас.
Теперь, когда Вы понимаете, как работает класс CQueue, Вы легко разберетесь в остальном коде этой программы.
Queue.cpp
/******************************************************************************
Модуль: Автор Copyright (c) 2000, Джеффри Рихтер (Jeffrey Richter)
******************************************************************************/
#include "..\CmnHdr.h"
/* см. приложение A */
#include
#include
#include
// для доступа к _beginthreadex
#include "Resource.h"
///////////////////////////////////////////////////////////////////////////////
class CQueue {
public:
struct ELEMENT {
int m_nThreadNum, m_nRequestNum;
// другие элементы данных должны быть определены здесь
};
Рис. 9-2.
Программа-пример Queue

235
Г ЛАВА 9
Синхронизация потоков с использованием объектов ядра
Рис. 9-2.
продолжение
typedef ELEMENT* PELEMENT;
private:
PELEMENT m_pElements;
// массив элементов, подлежащих обработке int m_nMaxElements;
// количество элементов в массиве m_h[2];
// описатели мьютекса и семафора &m_hmtxQ;
// ссылка на m_h[0]
HANDLE &m_hsemNumElements; // ссылка на m_h[1]
public:
CQueue(int nMaxElements);
CQueue();
BOOL Append(PELEMENT pElement, DWORD dwMilliseconds);
BOOL Remove(PELEMENT pElement, DWORD dwMilliseconds);
};
///////////////////////////////////////////////////////////////////////////////
CQueue::CQueue(int nMaxElements)
: m_hmtxQ(m_h[0]), m_hsemNumElements(m_h[1]) {
m_pElements = (PELEMENT)
HeapAlloc(GetProcessHeap(), 0, sizeof(ELEMENT) * nMaxElements);
m_nMaxElements = nMaxElements;
m_hmtxQ = CreateMutex(NULL, FALSE, NULL);
m_hsemNumElements = CreateSemaphore(NULL, 0, nMaxElements, NULL);
}
///////////////////////////////////////////////////////////////////////////////
CQueue::
CQueue() {
CloseHandle(m_hsemNumElements);
CloseHandle(m_hmtxQ);
HeapFree(GetProcessHeap(), 0, m_pElements);
}
///////////////////////////////////////////////////////////////////////////////
BOOL CQueue::Append(PELEMENT pElement, DWORD dwTimeout) {
BOOL fOk = FALSE;
DWORD dw = WaitForSingleObject(m_hmtxQ, dwTimeout);
if (dw == WAIT_OBJECT_0) {
// этот поток получил монопольный доступ к очереди увеличиваем число элементов в очереди lPrevCount;
fOk = ReleaseSemaphore(m_hsemNumElements, 1, &lPrevCount);
if (fOk) {
см. след. стр.

236
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Рис. 9-2.
продолжение
// в очереди еще есть место добавляем новый элемент m_pElements[lPrevCount] = *pElement;
} else {
// очередь полностью заполнена устанавливаем код ошибки и сообщаем о неудачном завершении вызова разрешаем другим потокам обращаться к очереди else {
// время ожидания истекло устанавливаем код ошибки и сообщаем о неудачном завершении вызова // GetLastError сообщит дополнительную информацию CQueue::Remove(PELEMENT pElement, DWORD dwTimeout) {
// ждем монопольного доступа к очереди и появления в ней хотя бы одного элемента fOk = (WaitForMultipleObjects(chDIMOF(m_h), m_h, TRUE, dwTimeout)
== WAIT_OBJECT_0);
if (fOk) {
// в очереди есть элемент извлекаем его = m_pElements[0];
// сдвигаем остальные элементы вниз на одну позицию, &m_pElements[1],
sizeof(ELEMENT) * (m_nMaxElements
1));
// разрешаем другим потокам обращаться к очереди else {
// время ожидания истекло устанавливаем код ошибки и сообщаем о неудачном завершении вызова // GetLastError сообщит дополнительную информацию
}
///////////////////////////////////////////////////////////////////////////////

237
Г ЛАВА 9
Синхронизация потоков с использованием объектов ядра
Рис. 9-2.
продолжение
CQueue g_q(10);
// совместно используемая очередь BOOL g_fShutdown = FALSE; // сигнализирует клиентскими серверным потокам когда им нужно завершаться g_hwnd;
// позволяет выяснять состояние клиентских и серверных потоков описатели и количество всех потоков (клиентских и серверных)
HANDLE g_hThreads[MAXIMUM_WAIT_OBJECTS];
int g_nNumThreads = 0;
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI ClientThread(PVOID pvParam) {
int nThreadNum = PtrToUlong(pvParam);
HWND hwndLB = GetDlgItem(g_hwnd, IDC_CLIENTS);
for (int nRequestNum = 1; !g_fShutdown; nRequestNum++) {
TCHAR sz[1024];
CQueue::ELEMENT e = { nThreadNum, nRequestNum };
// пытаемся поместить элемент в очередь if (g_q.Append(&e, 200)) {
// указываем номера потока и запроса wsprintf(sz, TEXT("Sending %d:%d"), nThreadNum, nRequestNum);
} else {
// поставить элемент в очередь не удалось wsprintf(sz, TEXT("Sending %d:%d (%s)"), nThreadNum, nRequestNum,
(GetLastError() == ERROR_TIMEOUT)
? TEXT("timeout") : TEXT("full"));
}
// показываем результат добавления элемента, ListBox_AddString(hwndLB, sz));
Sleep(2500); // интервал ожидания до добавления следующего элемента WINAPI ServerThread(PVOID pvParam) {
int nThreadNum = PtrToUlong(pvParam);
HWND hwndLB = GetDlgItem(g_hwnd, IDC_SERVERS);
while (!g_fShutdown) {
см. след. стр.

238
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Рис. 9-2.
продолжение
TCHAR sz[1024];
CQueue::ELEMENT e;
// пытаемся получить элемент из очереди if (g_q.Remove(&e, 5000)) {
// сообщаем, какой поток обрабатывает этот элемент какой поток поместил его в очередь и какой он по счету wsprintf(sz, TEXT("%d: Processing %d:%d"),
nThreadNum, e.m_nThreadNum, e.m_nRequestNum);
// на обработку запроса серверу нужно какое то время * e.m_nThreadNum);
} else {
// получить элемент из очереди не удалось wsprintf(sz, TEXT("%d: (timeout)"), nThreadNum);
}
// показываем результат обработки элемента, ListBox_AddString(hwndLB, sz));
}
return(0);
}
///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {
chSETDLGICONS(hwnd, IDI_QUEUE);
g_hwnd = hwnd; // используется клиентскими и серверными потоками для уведомления о своем состоянии dwThreadID;
// создаем клиентские потоки for (int x = 0; x < 4; x++)
g_hThreads[g_nNumThreads++] =
chBEGINTHREADEX(NULL, 0, ClientThread, (PVOID) (INT_PTR) x,
0, &dwThreadID);
// создаем серверные потоки for (x = 0; x < 2; x++)
g_hThreads[g_nNumThreads++] =
chBEGINTHREADEX(NULL, 0, ServerThread, (PVOID) (INT_PTR) x,
0, &dwThreadID);
return(TRUE);
}

239
Г ЛАВА 9
Синхронизация потоков с использованием объектов ядра
Рис. 9-2.
продолжение
///////////////////////////////////////////////////////////////////////////////
void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
switch (id) {
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
///////////////////////////////////////////////////////////////////////////////
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);
}
return(FALSE);
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {
DialogBox(hinstExe, MAKEINTRESOURCE(IDD_QUEUE), NULL, Dlg_Proc);
InterlockedExchangePointer((PVOID*) &g_fShutdown, (PVOID) TRUE);
// ждем завершения всех потоков, а затем проводим очистку, g_hThreads, TRUE, INFINITE);
while (g_nNumThreads
)
CloseHandle(g_hThreads[g_nNumThreads]);
return(0);
}
//////////////////////////////// Конец файла //////////////////////////////////
Сводная таблица объектов, используемых
для синхронизации потоков
В следующей таблице суммируются сведения о различных объектах ядра применительно к синхронизации потоков.
Находится в занятом
Переходит в свободное
Побочный эффект
Объект состоянии, когда:
состояние, когда:
успешного ожидания
Процесс процесс еще активен процесс завершается
Нет
(
ExitProcess, TerminateProcess)
Поток поток еще активен поток завершается
Нет
(
ExitThread, TerminateThread)
см. след. стр.

240
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
продолжение
Находится в занятом
Переходит в свободное
Побочный эффект
Объект состоянии, когда:
состояние, когда:
успешного ожидания
Задание время, выделенное время, выделенное заданию,
Нет заданию, еще не истекло истекло
Файл выдан запрос завершено выполнение
Нет на ввод вывод запроса на ввод вывод
Консольный ввода нет ввод есть
Нет ввод
Уведомление в файловой системе файловая система
Сбрасывается об изменении нет изменений обнаруживает изменения в исходное файла состояние
Событие вызывается
ResetEvent,
вызывается
SetEvent
Сбрасывается с автосбросом
PulseEvent или ожидание или
PulseEvent
в исходное успешно завершилось состояние
Событие вызывается
ResetEvent
вызывается
SetEvent
Нет со сбросом или
PulseEvent
или
PulseEvent
вручную
Ожидаемый вызывается
CancelWaitable
наступает время
Сбрасывается таймер
Timer или ожидание срабатывания в исходное с автосбросом успешно завершилось
(
SetWaitableTimer)
состояние
Ожидаемый вызывается
CancelWaitable
наступает время
Нет таймер
Timer
срабатывания со сбросом
(
SetWaitableTimer)
вручную
Семафор ожидание успешно счетчик > Счетчик завершилось
(
ReleaseSemaphore)
уменьшается на 1
Мьютекс ожидание успешно поток освобождает мьютекс
Передается пото завершилось
(
ReleaseMutex)
ку во владение
Критическая ожидание успешно поток освобождает
Передается потоку секция (поль завершилось критическую секцию во владение зовательского
(
(Try)EnterCriticalSection)
(
LeaveCriticalSection)
режима)
Interlocked функции (пользовательского режима) никогда не приводят к исключению потока из числа планируемых они лишь изменяют какое то значение и тут же возвращают управление.
Другие функции, применяемые в синхронизации потоков
При синхронизации потоков чаще всего используются функции
WaitForSingleObject и
WaitForMultipleObjects. Однако весть и другие, несколько отличающиеся функции, которые можно применять стой же целью. Если Вы понимаете, как работают
Wait–
ForSingleObject и WaitForMultipleObjects, Вы без труда разберетесь ив этих функциях.
Асинхронный ввод-вывод на устройствах
При асинхронном вводе выводе поток начинает операцию чтения или записи и не ждет ее окончания. Например, если потоку нужно загрузить в память большой файл,
он может сообщить системе сделать это за него. И пока система грузит файл в память, поток спокойно занимается другими задачами — создает окна, инициализирует внутренние структуры данных и т. д. Закончив, поток приостанавливает себя и ждет уведомления от системы о том, что загрузка файла завершена.

241
Г ЛАВА 9
Синхронизация потоков с использованием объектов ядра
Объекты устройств являются синхронизируемыми объектами ядра, а это означает, что Вы можете вызывать
WaitForSingleObject и передавать ей описатель какого либо файла, сокета, коммуникационного порта и т. д. Пока система выполняет асинхронный ввод вывод, объект устройства пребывает в занятом состоянии. Как только операция заканчивается, система переводит объект в свободное состояние, и поток узнает о завершении операции. С этого момента поток возобновляет выполнение.
Функция WaitForInputIdle
Поток может приостановить себя и вызовом
WaitForInputIdle:
DWORD WaitForInputIdle(
HANDLE hProcess,
DWORD Эта функция ждет, пока у процесса, идентифицируемого описателем
hProcess, не опустеет очередь ввода в потоке, создавшем первое окно приложения.
WaitForInputIdle
полезна для применения, например, в родительском процессе, который порождает дочерний для выполнения какой либо нужной ему работы. Когда один из потоков родительского процесса вызывает
CreateProcess, он продолжает выполнение ив то время, пока дочерний процесс инициализируется. Этому потоку может понадобиться описатель окна, создаваемого дочерним процессом. Единственная возможность узнать о моменте окончания инициализации дочернего процесса — дождаться, когда тот прекратит обработку любого ввода. Поэтому после вызова
CreateProcess поток родительского процесса должен вызвать
WaitForInputIdle.
Эту функцию можно применить ив том случае, когда Вы хотите имитировать в программе нажатие каких либо клавиш. Допустим, Вы асинхронно отправили в главное окно приложения следующие сообщения:
WM_KEYDOWN
с виртуальной клавишей с виртуальной клавишей с виртуальной клавишей с виртуальной клавишей с виртуальной клавишей с виртуальной клавишей Эта последовательность дает тот же эффект, что и нажатие клавиш Alt+F, O, — в большинстве англоязычных приложений это вызывает команду Open изменю Выбор данной команды открывает диалоговое окно но, прежде чем оно появится на экране, Windows должна загрузить шаблон диалогового окна из файла и «пройтись»
по всем элементам управления в шаблоне, вызывая для каждого из них функцию
CreateWindow. Разумеется, на это уходит какое то время. Поэтому приложение, асинхронно отправившее сообщения типа WM_KEY
*, теперь может вызвать WaitForInput
Idle и таким образом перейти в режим ожидания до того момента, как Windows закончит создание диалогового окна и оно будет готово к приему данных от пользователя. Далее программа может передать диалоговому окну и его элементам управления сообщения о еще каких то клавишах, что заставит диалоговое окно проделать те или иные операции.
С этой проблемой, кстати, сталкивались многие разработчики приложений для разрядной Windows. Программам нужно было асинхронно передавать сообщения в окно, но получить точной информации о том, создано ли это окно и готово лик работе, они не могли. Функция
WaitForInputIdle решает эту проблему.

242
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
Функция MsgWaitForMultipleObjects(Ex)
При вызове
MsgWaitForMultipleObjects или MsgWaitForMultipleObjectsEx поток переходит в ожидание своих (предназначенных этому потоку) сообщений MsgWaitForMultipleObjects(
DWORD dwCount,
PHANDLE phObjects,
BOOL fWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask);
DWORD MsgWaitForMultipleObjectsEx(
DWORD dwCount,
PHANDLE phObjects,
DWORD dwMilliseconds,
DWORD dwWakeMask
DWORD Эти функции аналогичны
WaitForMultipleObjects. Единственное различие заключается в том, что они пробуждают поток, когда освобождается некий объект ядра или когда определенное оконное сообщение требует перенаправления в окно, созданное вызывающим потоком.
Поток, который создает окна и выполняет другие операции, относящиеся к пользовательскому интерфейсу, должен работать с функцией
MsgWaitForMultipleObjectsEx, а нес, так как последняя не дает возможности реагировать на действия пользователя. Подробнее эти функции рассматриваются в главе 26.
Функция WaitForDebugEvent
В Windows встроены богатейшие отладочные средства. Начиная исполнение, отладчик подключает себя к отлаживаемой программе, а потом просто ждет, когда операционная система уведомит его о каком нибудь событии отладки, связанном с этой программой. Ожидание таких событий осуществляется через вызов WaitForDebugEvent(
PDEBUG_EVENT pde,
DWORD Когда отладчик вызывает
WaitForDebugEvent, его поток приостанавливается. Система уведомит поток о событии отладки, разрешив функции
WaitForDebugEvent вернуть управление. Структура, на которую указывает параметр
pde, заполняется системой перед пробуждением потока отладчика. В ней содержится информация, касающаяся только что произошедшего события отладки.
Функция SignalObjectAndWait
SignalObjectAndWait переводит в свободное состояние один объект ядра и ждет другой объект ядра, выполняя все это как одну операцию на уровне атомарного доступа SignalObjectAndWait(
HANDLE hObjectToSignal,
HANDLE hObjectToWaitOn,
DWORD dwMilliseconds,
BOOL fAlertable);

243
Г ЛАВА 9
Синхронизация потоков с использованием объектов ядра
Параметр
hObjectToSignal должен идентифицировать мьютекс, семафор или событие объекты любого другого типа заставят
SignalObjectAndWait вернуть а функцию
GetLastError — ERROR_INVALID_HANDLE. Функция SignalObjectAndWait про веряет тип объекта и выполняет действия, аналогичные тем, которые предпринимают функции
ReleaseMutex, ReleaseSemaphore (со счетчиком, равным 1) или ResetEvent.
Параметр
hObjectToWaitOn идентифицирует любой из следующих объектов ядра:
мьютекс, семафор, событие, таймер, процесс, поток, задание, уведомление об изменении файла или консольный ввод. Параметр
dwMilliseconds, как обычно, определяет,
сколько времени функция будет ждать освобождения объекта, а флаг
fAlertable указывает, сможет ли поток в процессе ожидания обрабатывать посылаемые ему APC вы зовы.
Функция возвращает одно из следующих значений WAIT_OBJECT_0, WAIT_TIME
OUT, WAIT_FAILED, WAIT_ABANDONED (см. раздело мьютексах) или WAIT_IO_COMP
LETION.
SignalObjectAndWait — удачное добавление к Windows API по двум причинам. Во первых, освобождение одного объекта и ожидание другого — задача весьма распространенная, а значит, объединение двух операций водной функции экономит процессорное время. Каждый вызов функции, заставляющей поток переходить из кода,
который работает в пользовательском режиме, в код, работающий в режиме ядра,
требует примерно 1000 процессорных тактов (на платформах
x86), и поэтому для выполнения, например, такого кода, понадобится около 2000 тактов. В высокопроизводительных серверных приложениях дает заметную экономию процессорного времени.
Во вторых, без функции
SignalObjectAndWait ни у одного потока не было бы возможности узнать, что другой поток перешел в состояние ожидания. Знание таких вещей очень полезно для функций типа
PulseEvent. Как я уже говорил в этой главе,
PulseEvent переводит событие в свободное состояние и тут же сбрасывает его. Если ни один из потоков не ждет данный объект, событие не зафиксирует этот импульс. Я встречал программистов, которые пишут вот такой код выполняем какие то операции, INFINITE);
// выполняем еще какие то операции
M
Этот фрагмент кода выполняется рабочим потоком, который проделывает какие то операции, а затем вызывает
SetEvent, чтобы сообщить (другому потоку) об окончании своих операций. В тоже время в другом потоке имеется код:
WaitForSingleObject(hEventWorkerThreadDone);
PulseEvent(hEventMoreWorkToBeDone);
Приведенный ранее фрагмент кода рабочего потока порочен по самой своей сути,
так как будет работать ненадежно. Ведь вполне вероятно, что после того, как рабочий поток обратится к
SetEvent, немедленно пробудится другой потоки вызовет Pulse
Event. Проблема здесь в том, что рабочий поток уже вытеснен и пока еще не получил шанса на возврат из вызова
SetEvent, не говоря ужо вызове WaitForSingleObject. В ито

244
Ч АС Т Ь I I
НАЧИНАЕМ РАБОТАТЬ
ге рабочий поток не сможет своевременно освободить событие
hEventMoreWork
ToBeDone.
Но если Вы перепишете код рабочего потока с использованием функции
Signal
ObjectAndWait:
// выполняем какие то операции, INFINITE, FALSE);
// выполняем еще какие то операции
M
то код будет работать надежно, поскольку освобождение и ожидание реализуются на уровне атомарного доступа. И когда пробудится другой поток, Вы сможете быть абсолютно уверены, что рабочий поток ждет события
hEventMoreWorkToBeDone, а значит, он обязательно заметит импульс, приложенный к событию.
В Windows 98 функция
SignalObjectAndWait определена, ноне реализована.

245
Г ЛАВА 0
Полезные средства для синхронизации потоков
З
а годы своей практики я часто сталкивался с проблемами синхронизации потоков и поэтому написал ряд C++ классов и компонентов, которыми я поделюсь с Вами в этой главе. Надеюсь, этот код Вам пригодится и сэкономит массу времени при разработке приложений — или по крайней мере чему нибудь научит.
Я начну главу с того, что расскажу о реализации критической секции и расширении ее функциональности. В частности, Вы узнаете, как пользоваться одной критической секцией в нескольких процессах. Далее Вы увидите, как сделать объекты безопасными для применения в многопоточной среде, создав для собственных типов данных оболочку из C++ класса. Используя такие классы, я попутно представлю объект, ведущий себя прямо противоположно семафору.
Потом мы рассмотрим одну из типичных задач программирования что делать,
когда считывает какой то ресурс несколько потоков, а записывает в него — только один. В Windows нет подходящего на этот случай синхронизирующего объекта, и я написал специальный C++ класс.
Наконец, я продемонстрирую свою функцию
WaitForMultipleExpressions. Работая по аналогии с
WaitForMultipleObjects, заставляющей ждать освобождения одного или всех объектов, она позволяет указывать более сложные условия пробуждения потока.



Поделитесь с Вашими друзьями:
1   ...   23   24   25   26   27   28   29   30   ...   68


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал