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

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

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

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

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

И последнее (но от этого не менее важное) замечание: строки добавляются к списку вызовом вспомогательной функции AddText, в которой используется _vstprintf_s, новая безопасная функция для работы со строками:

void AddText(HWND hWndLB, PCTSTR pszFormat,,..) {

va_list argList; va_start(argList, pszFormat);

TCHAR sz[20 * 1024];

_vstprintf_s(sz, _countof(sz), pszFormat, argList); ListBox_SetCurSel(hWndLB, ListBox_AddString(hWndLB, sz));

va_end(argList);

}

Несколько полезных приемов

Используя критические секции, желательно привыкнуть делать одни вещи и избегать других. Вот несколько полезных приемов, которые пригодятся вам в работе с критическими секциями. (Они применимы и к синхронизации потоков с помощью объектов ядра, о которой я расскажу в следующей главе.)

На каждый разделяемый ресурс используйте отдельную структуру

CRITICAL_SECTION

Довольно часто встречаются единые «логические» ресурсы, составленные из нескольких объектов. Примером может быть ситуация, когда при добавлении элемента в набор необходимо также увеличить значение счетчика. Для этого достаточно одной блокировки, управляющей доступом для чтения и записи к логическому ресурсу.

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

Одновременный доступ к нескольким ресурсам

Иногда нужен одновременный доступ сразу к двум и более логическим ресурсам. Например, приложению необходимо заблокировать один ресурс для

Глава 8. Синхронизация потоков в пользовательском режиме.docx 279

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

DWORD WINAPI ThreadFunc(PVOID pvParam) {

EnterCriticalSectlon(&g_csResource1);

EnterCriticalSection(&g_csResource2);

//извлекаем элемент из Resource1

//добавляем элемент в Resource2 LeaveCriticalSection(&g_csResource2); LeaveCriticalSection(&g_csResou rce1); return(0);

}

Предположим, доступ к обоим массивам требуется и другому потоку в данном процессе; при этом его функция написана следующим образом:

DWORD WINAPI OtherThreadFunc(PVOTD pvParam) {

EnterCriticalSection(&g_csResource2);

EnterCriticalSection(&g_csResource1);

// извлекаем элемент из Resource1 // добавляем элемент в Resource2

LeaveCriticalSection(&g_csResou rce2); LeaveCriticalSection(&g_csResou rce1); return(0);

}

Я лишь поменял порядок вызовов EnterCriticalSection и LeaveCriticalSection.

Но из-за того, что функции ThreadFunc и OtherThreadFunc написаны именно так, существует вероятность взаимной блокировки. Допустим, ThreadFunc начинает исполнение и занимает критическую секцию g_csResource1. Получив от системы процессорное время, поток с функцией OtherThreadFunc захватывает критическую секцию g_csResource2. Тут-то и происходит взаимная блокировка потоков. Какая бы из функций — ThreadFunc или OtherThreadFunc — ни пыталась продолжить исполнение, она не сумеет занять другую, необходимую ей критическую секцию.

Эту ситуацию легко исправить, написав код обеих функций так, чтобы они вызывали EnterCriticalSection в одинаковом порядке. Заметьте, что порядок вызовов LeaveCriticalSection несуществен, поскольку эта функция никогда не приостанавливает поток.

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

Не занимайте критические секции надолго

Надолго занимая критическую секцию, ваше приложение может блокировать другие потоки, что отрицательно скажется на его общей производительности. Вот прием, позволяющий свести к минимуму время пребывания в критической секции. Следующий код не дает другому потоку изменять значение в g_s до тех пор, пока в окно не будет отправлено сообщение WM_SOMEMSG:

SOMESTRUCT g_s;

CRITICAL_SECTION g_cs;

DWORD WINAPI SomeThread(PVOID pvParam) {

EnterCriticalSection(&g_cs);

// посылаеи в окно сообщение

SendNessage(hNndSomeWnd, WM_SOMEMSG, &g_s, 0);

LeaveCrlticalSection(&g_cs); return(0);

}

Трудно сказать, сколько времени уйдет на обработку WM_SOMEMSG оконной процедурой — может, несколько миллисекунд, а может, и несколько лет. В течение этого времени никакой другой поток не получит доступ к структуре g_s. Поэтому лучше составить код иначе:

SONESTRUCT g_s;

CRITICAL_SECTION g_cs;

DWORD WINAPI SomeThread(PVOID pvParam) {

EnterCriticalSection(&g_cs);

SOMESTRUCT sTemp = g_s;

LeaveCriticalSection(&g_cs);

// посылаем в окно сообщение

SendMessage(hWndSomeWnd, WM_SOMEMSG, &sTemp, 0); return(0);

}

Этот код сохраняет значение элемента g_s во временной переменной sTemp. Нетрудно догадаться, что на исполнение этой строки уходит всего несколько тактов процессора. Далее программа сразу вызывает LeaveCriticalSection — защищать глобальную структуру больше не нужно. Так что вторая версия программы намного лучше первой, поскольку другие потоки «отлучаются» от структуры g_s лишь на несколько тактов процессора, а не на неопределенно долгое время. Такой подход предполагает, что «моментальный снимок» структуры вполне пригоден для чтения оконной процедурой, а также что оконная процедура не будет изменять элементы этой структуры.

Оглавление

 

Г Л А В А 9 Синхронизация потоков с использованием объектов ядра ......................

280

Wait-функции ............................................................................................................................

282

Побочные эффекты успешного ожидания..........................................................................

286

События ....................................................................................................................................

288

Программа-пример Handshake ..........................................................................................

293

Ожидаемые таймеры ..............................................................................................................

298

Ожидаемые таймеры и АРС-очередь...............................................................................

302

И еще кое-что о таймерах ...................................................................................................

305

Семафоры.................................................................................................................................

306

Мьютексы..................................................................................................................................

308

Мьютексы и критические секции ......................................................................................

311

Программа-пример Queue ..................................................................................................

312

Сводная таблица объектов, используемых для синхронизации потоков ....................

321

Другие функции, применяемые в синхронизации потоков..............................................

323

Асинхронный ввод-вывод на устройствах.....................................................................

323

Функция WaitForlnputldle ....................................................................................................

323

Функция MsgWaitForMultipleObjects(Ex)...........................................................................

325

Функция WaitForDebugEvent ..............................................................................................

325

Функция SignalObjectAndWait ............................................................................................

326

Обнаружение взаимных блокировок с помощью Wait Chain Traversal API ...............

327

Г Л А В А 9

Синхронизация потоков с использованием объектов ядра

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

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

В этой главе мы рассмотрим, как синхронизировать потоки с помощью объектов ядра. Вы увидите, что такие объекты предоставляют куда больше возможностей, чем механизмы синхронизации в пользовательском режиме. В сущности, единственный их недостаток — меньшее быстродействие. Дело в том, что при вызове любой из функций, упоминаемых в этой главе, поток должен перейти из пользовательского режима в режим ядра. А такой переход обходится очень дорого — в 200 процессорных тактов на платформе x86. Прибавьте сюда еще и время, которое необходимо на выполнение кода этих функций в режиме ядра. Но все это мелочь по сравнению с издержками, вызванными необходимостью планирования потока, сбросом и промахами кэша, измеряемыми десятками тысяч тактов.

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 281

Кэтому моменту я уже рассказал вам о нескольких объектах ядра, в том числе

опроцессах, потоках и заданиях. Почти все они годятся и для решения задач синхронизации. В случае синхронизации потоков о каждом из этих объектов говорят, что он находится либо в свободном (signaled state), либо в занятом состоянии (nonsignaled state). Переход из одного состояния в другое осуществляется по правилам, определенным Майкрософт для каждого из объектов ядра. Так, объекты ядра «процесс» сразу после создания всегда находятся в занятом состоянии. В момент завершения процесса операционная система автоматически освобождает его объект ядра «процесс», и он навсегда остается в этом состоянии.

Объект ядра «процесс» пребывает в занятом состоянии, пока выполняется сопоставленный с ним процесс, и переходит в свободное состояние, когда процесс завершается. Внутри этого объекта поддерживается булева переменная, которая при создании объекта инициализируется как FALSE («занято»). По окончании работы процесса операционная система меняет значение этой переменной на TRUE, сообщая тем самым, что объект свободен.

Если вы пишете код, проверяющий, выполняется ли процесс в данный момент, вам нужно лишь вызвать функцию, которая просит операционную систему проверить значение булевой переменной, принадлежащей объекту ядра «процесс». Тут нет ничего сложного. Вы можете также сообщить системе, чтобы та перевела ваш поток в состояние ожидания и автоматически пробудила его при изменении значения булевой переменной с FALSE на TRUE. Тогда появляется возможность заставить поток в родительском процессе, ожидающий завершения дочернего процесса, просто заснуть до освобождения объекта ядра, идентифицирующего дочерний процесс. В дальнейшем вы увидите, что в Windows есть ряд функций, позволяющих легко решать эту задачу.

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

Следующие объекты ядра бывают в свободном или занятом состоянии: ■ процессы; ■ потоки; ■ задания; ■ файлы;

■ консольный ввод; ■ мьютексы; ■ семафоры;

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

ожидаемые таймеры;

события.

Потоки могут засыпать и в таком состоянии ждать освобождения какого-либо объекта. Правила, по которым объект переходит в свободное или занятое состояние, зависят от типа этого объекта. О правилах для объектов процессов и потоков

яупоминал совсем недавно, а правила для заданий были описаны в главе 5.

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

Когда я только начинал осваивать всю эту тематику, я предпочитал рассматривать понятия «свободен-занят» по аналогии с обыкновенным флажком. Когда объект свободен, флажок поднят, а когда он занят, флажок опущен.

Потоки спят, пока ожидаемые ими объекты заняты (флажок опущен). Как только объект освободился (флажок поднят), спящий поток замечает это, просыпается и возобновляет выполнение.

Wait-функции

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

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 283

DWORD WaitForSingleObject(

HANDLE hObject,

DWORD dwMilliseconds);

Когда поток вызывает эту функцию, первый параметр, hObject, идентифицирует объект ядра, поддерживающий состояния «свободен-занят». (То есть любой объект, упомянутый в списке из предыдущего раздела.) Второй параметр, dwMilliseconds, указывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта.

Следующий вызов сообщает системе, что поток будет ждать до тех пор, пока не завершится процесс, идентифицируемый описателем hProcess:

WaitForSingleObject(hProcess, INFINITE);

Второй параметр сообщает системе, что вызывающий поток намерен ждать вечно (либо до завершения процесса).

В данном случае константа INFINITE, передаваемая во втором параметре, подсказывает системе, что вызывающий поток готов ждать этого события хоть целую вечность. Именно эта константа обычно и передается функции WaitForSingleObject, но вы можете указать любое значение в миллисекундах. Кстати, константа INFINITE определена как 0xFFFFFFFF (или -1). Разумеется, передача INFINITE не всегда безопасна. Если объект так и не перейдет в свободное состояние, вызывающий поток никогда не проснется; одно утешение: тратить драгоценное процессорное время он при этом не будет.

Вот пример, иллюстрирующий, как вызывать WaitForSingleObject со значением таймаута, отличным от INFINITE:

DWORD dw = WaitForSingleObject(hProcess, 5000); switch (dw) {

case WAIT_0BJECT_0:

// процесс завершается break;

case WAIT_TIMEOUT:

// процесс не завершился в течение 5000 мс break;

case WAIT_FAILED:

// неправильный вызов функции (неверный описатель?) break;

}

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

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

чит свою работу. Заметьте, что в параметре dwMilliseconds можно передать 0, и тогда WaitForSingleObject немедленно вернет управление.

Возвращаемое значение функции WaitForSingleObject указывает, почему вызывающий поток снова стал планируемым. Если функция возвращает WAIT_OBJECT_0, объект свободен, а если WAIT_TIMEOUT — заданное время ожидания (таймаут) истекло. При передаче неверного параметра (например, недопустимого описателя) WaitForSingleObject возвращает WAIT_FAILED. Чтобы выяснить конкретную причину ошибки, вызовите функцию GetLastError.

Функция WaitForMultipleObjects аналогична WaitForSingleObject с тем исклю-

чением, что позволяет ждать освобождения сразу нескольких объектов или како- го-то одного из списка объектов:

DWORD WaitForMultipleObjects(

DWORD dwCount,

CONST HANDLE* phObjects,

B00L bWaitAll,

DWORD dwMilliseconds);

Параметр dwCount определяет количество интересующих вас объектов ядра. Его значение должно быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64). Параметр phObjects — это указатель на массив описателей объектов ядра.

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

Параметр dwMilliseconds идентичен одноименному параметру функции WaitForSingleObject. Если вы указываете конкретное время ожидания, то по его истечении функция в любом случае возвращает управление. И опять же, в этом параметре обычно передают INFINITE (будьте внимательны при написании кода, чтобы не создать ситуацию взаимной блокировки).

Возвращаемое значение функции WaitForMultipleObjects сообщает, почему возобновилось выполнение вызвавшего ее потока. Значения WAIT_FAILED и WAIT_TIMEOUT никаких пояснений не требуют. Если вы передали TRUE в параметре fWaitAll и все объекты перешли в свободное состояние, функция возвращает значение WAIT_OBJECT_0. Если же fWaitAll приравнен FALSE, она возвращает управление, как только освобождается любой из объектов. Вы, по-видимому, захотите выяснить, какой именно объект освободился. В этом случае возвращается значение от WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1. Иначе говоря, если возвращаемое значение не равно WAIT_TIMEOUT или WAIT_FAILED, вычтите из него значение WAIT_OBJECT_0, и вы получите индекс в массиве описателей, на который указывает второй параметр функции WaitForMultipleObjects. Индекс

Глава 9. Синхронизация потоков с использованием объектов ядра.docx 285

подскажет вам, какой объект перешел в незанятое состояние. Поясню сказанное на примере.

HANDLE h [3]; h[0] = hProcess1; h[1] = hProcess2; h[2] = hProcess3;

DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); switch (dw) {

case WAIT_FAILED:

// неправильный вызов функции (неверный описатель?) break;

case WAIT_TIMEOUT:

// ни один из объектов не освободился в течение 5000 мс break;

case WAIT_0BJECT_0 + 0:

// завершился процесс, идентифицируемый h[0], т. е. описателем (hProcess1) break;

case WAIT_0BJECT_0 + 1:

// завершился процесс, идентифицируемый h[1], т. е. описателем (hProcess2) break;

case WAIT_0BJECT.0 + 2:

// завершился процесс, идентифицируемый h[2], т. е. описателем (hProcess3) break;

}

Если вы передаете FALSE в параметре fWaitAll, функция WaitForMultipleObjects сканирует массив описателей (начиная с нулевого элемента), и первый же освободившийся объект прерывает ожидание. Это может привести к нежелательным последствиям. Например, ваш поток ждет завершения трех дочерних процессов; при этом вы передали функции массив с их описателями. Если завершается процесс, описатель которого находится в нулевом элементе массива, WaitForMultipleObjects возвращает управление. Теперь поток может сделать то, что ему нужно, и вновь вызвать эту функцию, ожидая завершения другого процесса. Если поток передаст те же три описателя, функция немедленно вернет управление, и вы снова получите значение WAIT_OBJECT_0. Таким образом, пока вы не удалите описатели тех объектов, об освобождении которых функция уже сообщила вам, код будет работать некорректно.

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