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

u_course

.pdf
Скачиваний:
39
Добавлен:
04.06.2015
Размер:
1.87 Mб
Скачать

Средства разработки параллельных программм

121

WaitForMultipleObjects(m,hThread,TRUE,INFINITE);

// ожидание завершения

return 0;

// всех потоков

 

}

 

Критические секции

Критическая секции (подробнее см. главу 2) – это небольшой участок кода, требующий монопольного доступа к каким-либо общим для потоков данным. Протокол входа и выхода из критической секции реализован в WIN API на уровне конкретных функций. Для работы с критическими секциями в Windows существует структура CRITICAL_SECTION. Все управление секциями происходит через изменение переменных этой структуры. Системой не предполагается обращение к полям этой структуры напрямую – все действия должны производиться с помощью специальных функций.

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

Рассмотрим функции работы со структурой CRITICAL_SECTION.

VOID InitializeCriticalSection(CRITICAL_SECTION *pcs);

Функция инициализирует переменные структуры CRITICAL_SECTION, на которую указывает параметр pcs.

VOID DeleteCriticalSection(CRITICAL_SECTION *pcs);

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

VOID EnterCriticalSection(CRITICAL_SECTION *pcs);

Функция входа в критическую секцию. Участок кода, работающий с разделяемым ресурсом, предваряется вызовом этой функции. Функция проверяет значения элементов структуры CRITICAL_SECTION. Если ресурс занят, в них содержатся сведения о том, какой поток пользуется ресурсом. Затем EnterCriticalSection() выполняет следующие действия:

Если ресурс свободен, EnterCriticalSection() изменяет элементы структуры, указывая, что вызывающий поток занимает ресурс, после чего возвра-

Средства разработки параллельных программм

122

щает управление, и поток продолжает свою работу, получив доступ к ресурсу.

Если ресурс занят этим же самым потоком, EnterCriticalSection() обновляет элементы структуры, отмечая тем самым, сколько раз подряд этот поток захватил ресурс, и немедленно возвращает управление.

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

Все эти действия функция выполняет на уровне атомарного (неделимого) доступа.

VOID TryEnterCriticalSection(CRITICAL_SECTION *pcs);

Функция пытается войти в критическую секцию. Если ей это не удается, поток продолжает выполнение без задержки.

VOID LeaveCriticalSection(CRITICAL_SECTION *pcs);

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

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

Как и EnterCriticalSection(), функция LeaveCriticalSection() выполняет свои

действия на уровне атомарного доступа. Однако LeaveCriticalSection() никогда не приостанавливает поток, а управление возвращает немедленно.

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

// определяем глобальные переменные

LONG x;

CRITICAL_SECTION cs_print, cs_mod;

Средства разработки параллельных программм

123

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{

int i;

// Защищаем критической секцией вывод сообщения на экран

EnterCriticalSection(&cs_print); printf(“Work of thread one begin!!!”);

//Вывод закончен – секцию можно открыть

LeaveCriticalSection(&cs_print);

for(i = 0; i<5; i++)

{

// Защищаем критической секцией обращение к разделяемой переменной

EnterCriticalSection(&cs_mod); x++;

//Изменение закончено – секцию можно покинуть

LeaveCriticalSection(&cs_mod);

}

return 0;

}

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{

//Так как в операторе вывода задействованы сразу оба ресурса, // часть кода находится сразу в двух критических секциях

EnterCriticalSection(&cs_print);

EnterCriticalSection(&cs_mod);

printf(“Work of thread two begin!!! x is %d”, x);

LeaveCriticalSection(&cs_mod);

LeaveCriticalSection(&cs_print);

return 0;

}

int main(int argc, char** argv)

{

int thr_array[2];

//инициализация критической секции, защищающей вывод на экран

InitializeCriticalSection(&cs_print);

//инициализация критической секции, защищающей изменение переменной

InitializeCriticalSection(&cs_mod);

thr_array[0] = 0;

hThread[0]=CreateThread(NULL,0,ThreadFunc1,(PVOID)&thr_array[0], 0, &dwThreadId[0]); if (!hThread) printf("main process: thread %d not execute!",0);

thr_array[1] = 1;

hThread[1]=CreateThread(NULL,0,ThreadFunc2,(PVOID)&thr_array[1], 0, &dwThreadId[1]); if (!hThread) printf("main process: thread %d not execute!",1);

Средства разработки параллельных программм

124

//после завершения потоков – удаление критических секций

Delete CriticalSection(&cs_print);

Delete CriticalSection(&cs_mod); return 0;

}

СИНХРОНИЗАЦИЯПОТОКОВСПОМОЩЬЮОБЪЕКТОВЯДРА

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

WinAPI предоставляет возможность синхронизации потоков с помощью объектов ядра. Как правило, для этого используются мьютексы (mutexes), семафоры (semaphores) и события (events). Коренным отличием от синхронизации в пользовательском режиме является то, что с помощью объектов ядра можно синхронизировать потоки, находящиеся внутри разных процессов. Далее рассматриваются общие свойства объектов ядра, используемые для синхронизации потоков, и конкретные примеры использования объектов для этой цели.

Wait-функции

Почти каждый объект ядра Windows может пребывать в одном из двух состояний – свободном (signalted state) или занятом (unsignalted state). Пере-

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

Потоки могут «засыпать» и в таком состоянии ждать освобождения ка- кого-либо объекта. Для этого используется семейство Wait-функций.

Wait-функции позволяют потоку в любой момент приостановиться и ожидать освобождения какого-либо объект ядра.

DWORD WaitForSingleObject(HANDLE hObject,DWORD dwMilliseconds);

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

Средства разработки параллельных программм

125

второго параметра можно передать слово INFINITE, что будет означать, что поток готов ждать «вечно»:

WaitForSingleObject(hProc, INFINITE);

Исполняя эту команду, поток будет «вечно» ждать, пока не завершится процесс, на который указывает описатель hProc.

Кроме того, Wait-функция возвращает значение типа DWORD, которое можно проанализировать, определив, чем конкретно закончилось ожидание:

DWORD dw = WaitForSingleObject(hProc, 5000);

switch(dw) {

case WAIT_OBJECT_0: // процесс успешно завершился

... // код обработки break;

case WAIT_TIMEOUT: // процесс не завершился за 5000 мс, ожидание прервано

... // код обработки break;

case WAIT_FAILED: // неправильный вызов функции

... // код обработки break;

}

Данный код сообщает системе, что вызывающий поток не должен получать процессорное время до тех пор, пока не завершится указанный процесс или не пройдет 5000 мс. Возвращаемое значение функции указывает, почему поток снова стал планируемым. Если функция возвращает WAIT_OBJECT_0, ожидаемый объект свободен, если WAIT_TIMEOUT – заданное время ожидания истекло. Относительно другого объекта ядра Wait-функция может возвращать и другие значения.

DWORD WaitForMultipleObjects( DWORD dwCount, HANDLE* phObjects, BOOL fWaitAll,

DWORD dwMilliseconds);

Функция аналогична предыдущей, но позволяет ждать освобождения сразу нескольких или одного из списка объектов. Первый параметр dwCount определяет количество интересующих объектов ядра. Параметр phObjects – это указатель на массив описателей ядра. Параметр fWaitAll указывает, следует ли ждать освобождения всех объектов (TRUE) или хотя бы одного (FALSE).

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

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

Средства разработки параллельных программм

126

События

События – самая примитивная разновидность объектов ядра. Они содержат счетчик числа пользователей (как, впрочем, и все объекты ядра) и две булевы переменные: тип события (с автосбросом – без автосброса) и его состояние (свободно – занято).

Как правило, события используются для уведомления об окончании ка- кой-либо операции. Объекты «события» бывают двух видов: со сбросом вручную (manual-reset event) и с автосбросом (auto-reset event). Первые по-

зволяют возобновить выполнение сразу нескольких ожидающих потоков, вторые – только одного.

Рассмотрим функции WinAPI, работающие с объектом ядра «событие».

HANDLE CreateEvent(

PSECURITY_ATTRIBUTES psa, BOOL fManualReset,

BOOL fInitialState,

PCTSTR pszName);

Функция создает объект ядра «событие».

Параметр psa указывает на настройки безопасности (как правило, здесь передается NULL, выше эти настройки обсуждались более подробно).

Параметр fManualReset определяет, будет ли событие со сбросом вручную (TRUE) или автосбросом (FALSE).

Параметр fInitialState определяет начальное состояние события – свободное (TRUE) или занятое (FALSE).

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

функцией OpenEvent().

HANDLE OpenEvent(

DWORD fdwAccess, BOOL fInherit,

PCTSTR pszName);

Функция открывает объект ядра «событие». Последний параметр, pszName, определяет имя объекта ядра. Он всегда должен содержать адрес строки с нулевым символом в конце (передавать NULL нельзя). Функции OpenHandle() просматривают единое пространство имен объектов ядра, пытаясь найти совпадение. Если объекта ядра «событие» с указанным именем нет или он другого типа, Open-функции возвращают NULL. Но если объект ядра «событие» с заданным именем существует, система проверяет, разрешен ли к данному объекту доступ запрошенного (через параметр fdwAccess) вида. Если

Средства разработки параллельных программм

127

доступ разрешен, таблица описателей в вызывающем процессе обновляется, и счетчик числа пользователей объекта возрастает на единицу. Если присвоить параметру fInherit значение TRUE, то будет получен наследуемый описатель.

BOOL CloseHandle(HANDLE hObj)

Функция закрывает ненужный описатель hObj объекта ядра «событие».

BOOL SetEvent(HANDLE hEvent);

Функция переводит «событие» hEvent в свободное состояние.

BOOL ResetEvent(HANDLE hEvent);

Функция переводит «событие» hEvent в занятое состояние.

Важным отличием событий с автосбросом и ручным сбросом является их реакция на вызванную по отношению к ним Wait-функцию.

Для событий с автосбросом действует следующее правило. Когда его ожидание потоком успешно завершается, этот объект автоматически сбрасывается в занятое состояние. Для этого объекта обычно не требуется вызывать ResetEvent(), так как система сама восстанавливает его состояние. Таким образом, событие с автосбросом можно представить в виде двоичного семафора, где Wait-функции или ResetEvent() реализуют функцию P(), а SetEvent() – функцию V().

Для событий с ручным сбросом никаких побочных эффектов не предусмотрено. Поэтому только ResetEvent() реализуют функцию P(). Крупномодульное представление Wait-функций можно представить в нащей нотации следующим оператором ожидания:

< await Event>0 >;

Поскольку Wait-функция не занимает событие, освобождение события с ручным сбросом могут дождаться сразу несколько потооков.

Рисунок 4.2. демонстрирует аналогию между нотацией семафоров, введенной в главе 2 и объектом ядра «событие».

Ниже приведены два примера использования события – с ручным сбросом и автосбросом соответственно.

В первом примере поток ThreadOne() готовит набор данных. Пока происходит подготовка, потоки ThirdSecond() и ThreadThird() ожидают ее завершения. Как только первый поток завершает формирование данных, он освобождает событие hEvent. Поскольку событие инициализировано с ручным автосбросом, то после освобождения события оба ожидавших его потока могут начать обработку данных.

Средства разработки параллельных программм

128

СОБЫТИЕ

с автосбросом

с ручным сбросом

нотация

HANDLE hEvent;

HANDLE hEvent;

 

hEvent = CreateEvent(NULL,

hEvent = CreateEvent(NULL,

sem Event=0;

FALSE, FALSE, “Event”);

TRUE, FALSE, “Event”);

 

установить СОБЫТИЕ в значение «свободно»

SetEvent(Event);

V(Event): <if (Event=0)

Event++;>

 

 

установить СОБЫТИЕ в значение «занято»

 

 

 

ResetEvent(Event);

ResetEvent(Event);

P(Event): <await

WaitForSingleObject(Event,INFINITE);

(Event>0) Event--;>

 

дождаться освобождения СОБЫТИЯ, не занимая его

нет

WaitForSingleObject(Event,INFINITE);

<await (Event>0);>

Рис. 4.2. Объект ядра «событие» и двоичный семафор

HANDLE hEvent;

DWORD WINAPI ThreadOne(PVOID pvParam)

{

hEvent = CreateEvent(NULL, TRUE, FALSE, “EventBeginExecute”);

... // блок подготовки данных

SetEvent(hEvent);

return 0;

}

DWORD WINAPI ThreadSecond(PVOID pvParam)

{

WaitForSingleObject(hEvent, INFINITE);

... // блок обработки данных, подготовленных потоком ThreadSecond

return 0;

}

DWORD WINAPI ThreadThird(PVOID pvParam)

{

WaitForSingleObject(hEvent, INFINITE);

... // блок обработки данных, подготовленных потоком ThreadThird

return 0;

}

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

Средства разработки параллельных программм

129

HANDLE hEvent;

DWORD WINAPI ThreadOne(PVOID pvParam)

{

hEvent = CreateEvent(NULL, FALSE, FALSE, “EventBeginExecute2”);

... // блок подготовки данных

SetEvent(hEvent);

return 0;

}

DWORD WINAPI ThreadSecond(PVOID pvParam)

{

WaitForSingleObject(hEvent, INFINITE);

... // блок обработки данных, подготовленных потоком ThreadSecond

SetEvent(hEvent);

return 0;

}

DWORD WINAPI ThreadThird(PVOID pvParam)

{

WaitForSingleObject(hEvent, INFINITE);

... // блок обработки данных, подготовленных потоком ThreadThird

SetEvent(hEvent);

return 0;

}

Семафоры

Объекты ядра «семафор» используется для учета ресурсов. Как и все объекты ядра, они содержат счетчик числа пользователей, но, кроме того, поддерживают еще два значения: одно определяет максимальное число ресурсов, другое показывает текущее число свободных ресурсов. Семафоры Windows практически реализуют поведение семафоров, описанное в главе 2.

Для семафоров определены следующие правила.

Когда счетчик текущего числа свободных ресурсов становится больше 0, семафор переходит в свободное состояние.

Если счетчик текущего числа свободных ресурсов равен 0, семафор

занят.

Система не допускает присвоения отрицательных значений счетчику текущего числа ресурсов.

Счетчик текущего числа ресурсов не может быть больше максимального числа ресурсов.

Рассмотрим функции, определенные для семафоров.

Средства разработки параллельных программм

130

HANDLE CreateSemaphore( PSECURITY_ATTRIBUTES psa, LONG lInitialCount,

BOOL lMaximumCount,

PCTSTR pszName);

Функция создает объект ядра «семафор». Параметры psa и pszName аналогичны, описанным ранеее для событий. Параметр lMaximumCount показывает максимально возможное число ресурсов, обрабатываемых семафором. Параметр lInitialCount устанавливает начальное значение счетчика текущего числа свободных ресурсов.

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

hSem = CreateSemaphore(NULL, 2, 5, “MySemaphore”);

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

HANDLE OpenSemaphore(

DWORD fdwAccess, BOOL fInherit,

PCTSTR pszName);

Функция открывает существующий объект ядра «семафор». Параметры функции аналогичны соответствующей функции для события.

Поток получает доступ к ресурсу, вызвав одну из Wait-функций с описателем семафора, который соответствует этому ресурсу. Wait-функция выполняет проверку значения счетчика текущего числа свободных ресурсов. Если оно больше 0 (семафор свободен), то система атомарно уменьшает значение счетчика на 1, и поток продолжает выполнение. Если же счетчик текущего число свободных ресурсов равен 0, то система переводит вызывающий поток в режим ожидания. Когда другой поток освободит ресурс, систем вспомнит об ожидающих потоках и сделает их исполняемыми. Таким образом, Wait-функции используются в качестве P()-функции семафора.

BOOL ReleaseSemaphore( HANDLE hSem,

LONG lReleaseCount,

LONG* plPreviousCount);

Функция освобождает определенное число ресурсрв. Первый параметр hSem указывает на освобождаемый семафор, второй параметр lReleaseCount содержит число освобождаемых ресурсов, а в третий параметр * plPreviousCount функция возвращает предыдущее (до вызова функции) значение счетчика текущего числа свободных ресурсов. Таким образом, функция ReleaseSemaphore() реализует несколько расширенную V()-функцию семафора.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]