Вы находитесь на странице: 1из 32

Тема: Синхронизация потоков

Необходимость синхронизации потоков


Все потоки в ОС Windows должны иметь доступ к
различным системным ресурсам — памяти, окнам,
файлам, портам ввода-вывода и т. д. Если одному из
потоков будет предоставлен монопольный доступ к
какому-либо ресурсу, другие потоки, которым тоже
нужен этот ресурс, не смогут выполнить свои
задачи. С другой стороны, бесконтрольное
использование ресурсов потоками может привести к
конфликтам или неправильным результатам.
Например, один поток пишет в блок памяти, из
которого другой поток в это время производит
считывание и вывод данных.
DWORD a[5]; // Объявление глобального массива
// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{ int i; DWORD dwResult = 0, num=0;
struct PARAM *p= (struct PARAM *) pvParam;
while (!p->stop)
{
for ( i = 0; i < 5; i++ ) a[ i ] = num;
num++;
}
return(dwResult);
}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{ int i; DWORD dwResult = 0;
struct PARAM *p= (struct PARAM *) pvParam;
while (!p->stop)
{
for ( i = 0; i < 5; i++ ) printf("%ld ", a[ i ]);
printf("\n");
}
return(dwResult);
}
В результате, 1-й поток последовательно заполняет
массив a[5] возрастающими сериями по 5 значений,
а второй поток, работая параллельно, считывает и
выводит на экран фрагменты этих серий чисел.
Работа двух и более потоков должна быть
синхронизирована в следующих случаях:
- потоки совместно используют разделяемый ресурс;
- когда нужно уведомлять другие потоки о
завершении каких-либо операций.

Средства синхронизации
Это набор объектов операционной системы, которые
создаются и управляются программно, являются
общими для всех потоков в системе (некоторые –
только для потоков, принадлежащих одному
процессу) и используются для координирования
доступа к ресурсам.
В качестве ресурсов может выступать все, что
может быть общим для двух и более потоков - файл
на диске, порт, запись в базе данных, объект GDI,
глобальная переменная программы.
Для синхронизации потоков в Windows предусмотре-
ны следующие средства:
- критический раздел (секция);
- семафор;
- мьютекс;
- событие;
- ожидаемый таймер.

Критические разделы (секции)


Критический раздел - это часть кода, доступ к
которому в данное время имеет только один поток.
Другой поток может обратиться к критической
секции, только когда первый выйдет из него.
Работа с критическими разделами производится
следующим образом.
Вначале необходимо определить объект
критический раздел, который объявляется как
глобальная переменная типа CRITICAL_SECTION.
Например:
CRITICAL_SECTION cs;
Тип данных CRITICAL_SECTION является структурой,
но ее поля используются только самой Windows.
Имеется четыре функции для работы с
критическими разделами.
1. Объект критический раздел сначала должен быть
инициализирован одним из потоков программы с
помощью функции:
VOID InitializeCriticalSection (LPCRITICAL_SECTION
lpCriticalSection);
где lpCriticalSection - указатель на переменную типа
CRITICAL_SECTION.
Например: InitializeCriticalSection(&cs);
Эта функция создает критический раздел с именем
cs. Чаще всего этот объект создается при
инициализации процесса, т.е. первичным потоком в
функции WinMain.
2. После инициализации объекта критического
раздела любой поток может войти в критический
раздел, осуществляя вызов:
VOID EnterCriticalSection (LPCRITICAL_SECTION
lpCriticalSection);
Например: EnterCriticalSection(&cs);
В этот момент именно этот поток становится
владельцем объекта.
3. Два различных потока не могут быть владельцами
одного объекта критический раздел одновременно.
Следовательно, если один поток вошел в
критический раздел, то следующий поток, вызвав
функцию EnterCriticalSection с тем же самым
объектом, будет ожидать, когда первый поток
покинет критический раздел, вызвав функцию
VOID LeaveCriticalSection (LPCRITICAL_SECTION
lpCriticalSection);
Например: LeaveCriticalSection(&cs);
В этот момент второй поток, задержанный в
функции EnterCriticalSection, станет владельцем
критического раздела, и его выполнение будет
возобновлено.
4. Когда объект критический раздел больше не
нужен процессу, его можно удалить (обычно в
функции WinMain) с помощью функции:
VOID DeleteCriticalSection (LPCRITICAL_SECTION
lpCriticalSection);
Например: DeleteCriticalSection(&cs);
Примечания.
1. Обычно вызовы функций EnterCriticalSection и
LeaveCriticalSection осуществляют до и после
фрагмента кода потока, при выполнении которого
могут возникнуть проблемы с разделением данных:
EnterCriticalSection(&cs);
// критичекий фрагмент кода потока
..............
LeaveCriticalSection(&cs);
2. Критические разделы используются для
синхронизации двух и более потоков только внутри
одного приложения.

Пример использования критического раздела.


Синхронизируется работа двух потоков при
заполнении элементов массива значениями и
выводе их на экран.
CRITICAL_SECTION cs;
DWORD a[5]; // Объявление глобального массива
// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{ int i; DWORD dwResult = 0, num=0;
struct PARAM *p= (struct PARAM *) pvParam;
while (!p->stop)
{
EnterCriticalSection(&cs);
for ( i = 0; i < 5; i++ ) a[ i ] = num;
LeaveCriticalSection(&cs);
num++;
}
return(dwResult);
}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{ int i; DWORD dwResult = 0;
struct PARAM *p= (struct PARAM *) pvParam;
while (!p->stop)
{
EnterCriticalSection(&cs);
for ( i = 0; i < 5; i++ ) printf("%ld ", a[ i ]);
printf("\n");
LeaveCriticalSection(&cs);
}
return(dwResult);
}
// --- Функция WinMain
int APIENTRY WinMain(…)
{
// Создание объекта критический раздел
InitializeCriticalSection(&cs);

// Удаление объекта критический раздел
// (при необходимости)
DeleteCriticalSection(&cs);

}
Потоки по очереди заполняют и правильно выводят
элементы массива.
Средства синхронизации потоков с использованием
объектов ядра.
Рассмотренные ранее критические разделы
работают в пользовательском режиме, т.е. их
использование не требует обращения к ядру ОС.
Остальные средства синхронизации (мьютекс, сема-
фор, событие, ожидаемый таймер) представляют
собой объекты ядра. При их использовании потокам
необходимо ожидать освобождения необходимых им
синхронизирующих объектов.

Функции ожидания потоков (Wait-функции)


Эти функции позволяют потоку в любой момент
приостановиться и ждать освобождения какого-либо
объекта ядра. Имеется 2 таких функции:
1. Функция WaitForSingleObject() обеспечивает ожи-
дание освобождения синхронизирующего объекта :
DWORD WaitForSingleObject (HANDLE hOblect,
DWORD dwMilliseconds);
где hOblect - дескриптор синхронизирующего
объекта ядра;
dwMilliseconds - время (в миллисекундах), в течение
которого происходит ожидание освобождения
объекта ядра. Если передать значение INFINITE
(бесконечность), то функция будет ждать
бесконечно долго.
Данная функция может возвращать следующие
значения:
WAIT_OBJECT_0 - объект освободился;
WAIT_TIMEOUT - время ожидания освобождения
прошло, а объект не освободился;
WAIT_FAILED - произошла ошибка.
В качестве объектов ядра, освобождения которых
может ожидать функция WaitForSingleObject()
являются не только потоки, но и другие процессы.
Например:
DWORD res = WaitForSingleObject(hProcess, 5000);
switch (res) {
case WAIT_OBJECT_0:
// процесс завершился
break;
case WAIT_TIMEOUT:
// процесс не завершился в течение 5000 мс
break;
case WAIT_FAILED:
// произошла ошибка
break;
}
2. Функция WaitForMultipleObjects() позволяет ждать
освобождения сразу нескольких объектов или
какого-то одного из списка объектов:
DWORD WaitForMultipleObjects(DWORD dwCount,
CONST HANDLE* phObjects, BOOL fWaitAll,
DWORD dwMilliseconds);
Параметр dwCount определяет количество
синхронизирующих объектов ядра. Его значение
должно быть в пределах от 1 до
MAXIMUM_WAIT_OBJECTS (в заголовочных файлах
Windows оно определено как 64).
Параметр phObjects — это указатель на массив
описателей объектов ядра.
Параметр fWaitAll определяет, будет ли функция
ждать освобождения всех заданных объектов ядра,
либо одного из них. Если он равен TRUE, функция
не даст потоку возобновить свою работу, пока не
освободятся все объекты. В противном случае поток
продолжит работу, если освободился один из
объектов.
Параметр dwMilliseconds идентичен одноименному
параметру функции WaitForSingleObject.
Возвращаемые функцией WaitForMultipleObjects
значения следующие:
- WAIT_TIMEOUT и WAIT_FAILED аналогичны
предыдущей функции.
- если параметр fWaitAll равен TRUE и все объекты
перешли в свободное состояние, функция
возвращает значение WAIT_OBJECT_0.
- если fWaitAll равен FALSE, функция возвращает
управление, как только освобождается любой из
объектов. При этом возвращается значение от
WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1.
Таким образом, если возвращаемое значение не
равно WAIT_TIMEOUT или WAIT_FAILED, то вычтя
из него значение WAIT OBJECT_0, получим индекс в
массиве описателей, который указывает какой
объект перешел в незанятое состояние.
Например:
HANDLE h[3];
h[0] = hObject1;
h[1] = hObject2;
h[2] = hObject3;
DWORD res=WaitForMultipleObjects(3,h,FALSE,3000);
switch (res) {
case WAIT_FAILED:
// произошла ошибка
break;
case WAIT_TIMEOUT:
// ни один из объектов не освободился
// в течение 3000 мс
break;
case WAIT_OBJECT_0 + 0:
// освободился объект с описателем hObject1
break;
case WAIT_OBJECT_0 + 1:
// освободился объект с описателем hObject2
break;
case WAIT_OBJECT_0 + 2:
// освободился объект с описателем hObject3
break;
}
В качестве объектов, освобождения которых
ожидает поток, могут также выступать другие
процессы, например дочерние.
Мьютексы
Объекты ядра «мьютексы» (mutual exclusion - mutex)
обеспечивают потокам взаимоисключающий доступ
к общему ресурсу. Они содержат счетчик числа
пользователей, счетчик рекурсии и переменную, в
которой запоминается идентификатор потока.
Мьютексы ведут себя точно так же, как и
критические секции. В отличие от них мьютексы
являются объектами ядра и они позволяют
синхронизировать доступ к ресурсу нескольких
потоков из разных процессов.
В любой момент времени мьютексом может владеть
только один поток. Доступ к объекту разрешается,
когда поток, владеющий объектом, освободит его.
Наиболее часто, с помощью мьютексов защищают
блоки памяти, к которым обращается множество
потоков.
Функции для работы с мьютексами следующие:
1. Создание объекта mutex
HANDLE CreateMutex (LPSECURITY_ATTRIBUTES
lpMutexAttributes, BOOL bInitialOwner, LPCTSTR
lpName )
где
- lpMutexAttributes - указатель на структуру
SECURITY_ATTRIBUTES;
- bInitialOwner - указывает первоначальное
состояние созданного объекта (TRUE - объект сразу
становится занятым, FALSE - объект свободен);
- lpName - указывает на строку, содержащую имя
объекта. Имя необходимо для доступа к объекту
других процессов, в этом случае объект становится
глобальным и им могут оперировать разные
программы. Если не нужен именованный объект, то
указывается NULL.
Функция возвращает дескриптор объекта mutex. В
дальнейшем этот дескриптор используется для
управления мьютексом.
2. Освобождение объекта mutex
BOOL ReleaseMutex( HANDLE hMutex ) - освобождает
объект mutex с описателем hMutex, переводя его из
занятого в свободное состояние.
3. Закрытие (уничтожение) объекта mutex и
освобождение его дескриптора
BOOL CloseHandle( HANDLE hMutex )
Взаимодействие потока и мьютекса происходит
следующим образом.
Поток пытается получить доступ к разделяемому
ресурсу, вызывая одну из Wait-функций и передавая
ей описатель мьютекса, который охраняет этот
ресурс.
Wait-функция проверяет у мьютекса идентификатор
потока: если его значение равно 0, мьютекс
свободен; в этом случае он принимает значение
идентификатора вызывающего потока, и этот поток
сразу получает доступ к ресурсу.
Если Wait-функция определяет, что у мьютекса
идентификатор потока не равен 0 (мьютекс занят),
вызывающий поток переходит в состояние
ожидания.
Когда ожидание мьютекса потоком успешно
завершается, последний получает монопольный
доступ к защищенному ресурсу. Все остальные
потоки, пытающиеся обратиться к этому ресурсу,
переходят в состояние ожидания.
Когда поток, занимающий ресурс, заканчивает с ним
работать, он должен освободить мьютекс вызовом
функции ReleaseMutex.

Пример использования мьютекса для синхронизации


работы двух потоков при заполнении массива
значениями и его обработке.
// Объявление глобальных данных
FLOAT y[20], s;
HANDLE hMutex;
// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{ int i; DWORD dwResult = 0, n=1;
struct PARAM *p= (struct PARAM *) pvParam;
while (!p->stop)
{
WaitForSingleObject( hMutex, INFINITE );
for ( i = 0; i < 20; i++ ) y[i]=2.5*(n+i)*sin(3.9*i+n);
n++;
ReleaseMutex( hMutex );
}
return(dwResult);
}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{ int i; DWORD dwResult = 0;
struct PARAM *p= (struct PARAM *) pvParam;
while (!p->stop)
{
WaitForSingleObject( hMutex, INFINITE );
for ( i = 0, s=0; i < 20; i++ ) s+=y[i];
s/=20;
ReleaseMutex( hMutex );
}
return(dwResult);
}
// --- Функция WinMain
int APIENTRY WinMain(…)
{
// Создание объекта мьютекс
hMutex = CreateMutex( NULL, FALSE, NULL );

// Удаление объекта мьютекс (при необходимости)
CloseHandle( hMutex );

}
Потоки по очереди заполняют массив значениями и
производят его обработку.
Семафоры.
Семафоры, как и другие объекты ядра, содержат
счетчик числа пользователей, но, кроме того, имеют
два целых значения: одно определяет максимальное
число ресурсов (контролируемое семафором),
другое используется как счетчик текущего числа
ресурсов.
Объекты ядра «семафор» используются для учета
доступных ресурсов и координации работы
нескольких потоков с ними. В качестве ресурсов
могут выступать сообщения, запросы, ожидающие в
очереди.
Семафоры также применяются для ограничения
количества потоков, одновременно работающих с
ресурсом.
Объекту «семафор» при инициализации передается
максимальное число потоков или ресурсов, после
каждого "захвата" счетчик семафора уменьшается.
Когда счетчик равен нулю, семафор считается не
установленным (сброшенным) и ни один поток не
сможет "захватить" семафор. При освобождении
семафора одним из потоков счетчик увеличивается
на 1 и с семафором может работать любой другой
поток.
Функции для работы с семафорами следующие:
1. Создание объекта ядра «семафор» производится
функцией CreateSemaphore:
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES
lpSemaphAttrib, LONG lInitialCount,
LONG lMaximumCount, LPCTSTR lpName);
где
- lpSemaphAttrib - указатель на структуру
SECURITY_ATTRIBUTES;
- lMaximumCount - указывает максимальное число
ресурсов (потоков) для семафора, должен быть
больше 1;
- lInitialCount – задает начальное значение
доступных ресурсов в пределах от 0 до
lMaximumCount;
- lpName - указывает на строку, содержащую имя
объекта. Имя необходимо для доступа к объекту
других процессов. Если не нужен именованный
объект, то указывается NULL.
Функция возвращает дескриптор семафора,
используемый для работы с ним.
2. Освобождение семафора и увеличение его
счетчика производится функцией ReleaseSemaphore:
BOOL ReleaseSemaphore( HANDLE hSem,
LONG lReleaseCount, LPLONG lpPreviousCount);
где hSem – дескриптор семафора;
lReleaseCount – значение, на которое увеличивается
счетчик семафора, обычно это 1 или большее;
lpPreviousCount – указатель на предыдущее значе-
ние счетчика, если оно не используется, то NULL.
Функция возвращает значение TRUE, если новое
значение счетчика не превысило максимальное. В
противном случае возвращается FALSE и значение
счетчика не изменяется.
3. Закрытие (уничтожение) объекта semaphore и
освобождение его дескриптора
BOOL CloseHandle( HANDLE hSem );
Взаимодействие потоков с семафором происходит
следующим образом.
Поток получает доступ к ресурсу, вызывая одну из
Wait-функций и передавая ей описатель семафора,
который охраняет этот ресурс. Wait-функция
проверяет у семафора счетчик текущего числа
ресурсов: если его значение больше 0 (семафор
свободен), уменьшает значение этого счетчика на 1,
и вызывающий поток остается планируемым.
Если Wait-функция определяет, что счетчик
текущего числа ресурсов равен О (семафор занят),
система переводит вызывающий поток в состояние
ожидания. Когда другой поток увеличит значение
этого счетчика, вызвав функцию ReleaseSemaphore,
система снова начнет выделять ему процессорное
время (а он, захватив ресурс, уменьшит значение
счетчика на 1).
Пример использования семафоров.
Программа производит обработку клиентских
запросов, которые вначале помещают в очередь
длиной 20. Обработку запросов выполняют три
потока. Для учета количества запросов в очереди
используется семафор, для безопасного помещения
запросов в очередь и извлечения из нее
используются критические секции.
// Описание глобальных данных
HANDLE hSem;
CRITICAL_SECTION cs;
struct Query { // описание структуры запроса
LONG lClientId;
char cQueryText[256]; };
struct Query Queue[20]; // создание очереди
LONG lPreviousCount=0;
// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{ int i; DWORD dwResult=0;
struct Query Query1; // обрабатываемый запрос
struct PARAM *p= (struct PARAM *) pvParam;
while (!p->stop)
{
// ожидание наличия в очереди запросов
WaitForSingleObject(hSem, INFINITE);
// извлечение 1-го запроса из очереди
EnterCriticalSection(&cs);
Query1 = Queue[0];
for (i=0; i< lPreviousCount; i++)
Queue [i]= Queue [i+1];
LeaveCriticalSection(&cs);
// обработка запроса Query1 в потоке

}
return(dwResult);
}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{ // содержимое функции потока 2
// аналогично потоку 1

}
// Функция потока 3
DWORD WINAPI ThreadFunc3(PVOID pvParam)
{ // содержимое функции потока 3
// аналогично потоку 1

}
// --- Функция WinMain
int APIENTRY WinMain(…)
{
struct Query InQuery; // буфер для нового запроса
// Создание объекта семафор
hSem = CreateSemaphore( NULL, 0, 20, NULL );
// Создание объекта критический раздел
InitializeCriticalSection(&cs);

// получение запроса от клиента и помещение
// его в очередь
InQuery = GetQuery();
// увеличение числа запросов и счетчика на 1
if (ReleaseSemaphore(hSem, 1, &lPreviousCount))
{
EnterCriticalSection(&cs);
Queue[lPreviousCount] = InQuery;
LeaveCriticalSection(&cs);
}
else // очередь заполнена, запросу отказано
RejectQuery(InQuery);

// Удаление объектов семафор и
// критической раздел (при необходимости)
CloseHandle(hSem);
DeleteCriticalSection(&cs);

}
События

Объекты ядра "события" содержат счетчик числа


пользователей (как и все объекты ядра) и две
булевы переменные: одна сообщает тип данного
объекта, другая — его состояние (свободен или
занят).
Объекты-события бывают двух типов: со сбросом
вручную (manual-reset events) и с автосбросом
(auto-reset events). Первые позволяют возобновлять
выполнение сразу нескольких ждущих потоков,
вторые — только одного.
События используются для уведомления потоков об
окончании какой-либо операции. Например, первый
поток переводит объект "событие" в занятое
состояние и выполняет операцию ввода данных.
Закончив, он сбрасывает событие в свободное
состояние. Тогда другой поток, который должен
выполнять обработку введенных данных и ждал
перехода события в свободное состояние,
пробуждается и вновь становится планируемым.
Функции для работы с событиями следующие.
1. Объект ядра "событие" создается функцией
CreateEvent:
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES
lpEventAttrib, BOOL fManualReset,
BOOL flnitialState, LPCTSTR lpName);
Параметры функции:
- lpEventAttrib является указателем на структуру
типа SECURITY_ATTRIBUTES, если не используется,
то задается NULL;
- fManualReset указывает, какого типа событие будет
создано: событие со сбросом вручную (TRUE) или с
автосбросом (FALSE);
- flnitialState определяет начальное состояние
события — свободное (TRUE) или занятое (FALSE);
- lpName указывает на строку, содержащую имя
объекта, которое необходимо для доступа к объекту
других процессов. Если не нужен именованный
объект, то указывается NULL.
Функция CreateEvent возвращает описатель
события, специфичный для конкретного процесса.
2. Перевод события в свободное состояние
производится следующей функцией:
BOOL SetEvent(HANDLE hEvent);
где hEvent – дескриптор события, возвращенный
функцией CreateEvent.
3. Изменение состояния события (с ручным сбросом)
на занятое выполняется следующей функцией:
BOOL ResetEvent(HANDLE hEvent);
4. Освобождение события с последующим
переводом его в занятое состояние выполняет
следующая функция:
BOOL PulseEvent(HANDLE hEvent);
Выполнение этой функции эквивалентно
последовательному вызову SetEvent и ResetEvent.
5. Для закрытия (уничтожения) объекта Event и
освобождения его дескриптора используется
функция:
BOOL CloseHandle(HANDLE hEvent);
Взаимодействие потоков с объектом ядра "событие"
происходит следующим образом.
Когда поток, вызвав Wait-функцию, дождался
требуемого события в свободном состоянии
дальнейшие действия зависят от типа события:
1) если событие с автосбросом, такое событие
автоматически сбрасывается в занятое состояние,
при этом только один поток будет работать, а
остальные потоки останутся ждать. Для
возобновления работы потоков, ожидающих данное
событие необходимо вызвать функцию SetEvent();
2) если это событие со сбросом вручную, то все
потоки, ожидающие этого события, получат
управление, а событие так и останется в свободном
состоянии, пока какой-нибудь поток не вызовет
ResetEvent().
Количество объектов "событие", используемых в
программе для работы с потоками зависит от
порядка работы этих потоков:
а) если потоки могут выполнять действия в любом
порядке или параллельно, то достаточно иметь одно
событие. Оно должно быть с автосбросом, когда в
данный момент времени должен работать один из
потоков и иметь тип с ручным сбросом, когда потоки
могут работать одновременно.
б) если потоки должны выполнять свои операции в
определенной последовательности, то количество
событий будет равно количеству потоков, тип этих
событий – с автосбросом.

Пример 1. Два потока по очереди выполняют


операции записи-чтения в блоке памяти.
Используется одно событие с автосбросом.
HANDLE hEvent;
hEvent = CreateEvent( NULL, FALSE, TRUE, NULL );
// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освождения события hEvent
WaitForSingleObject(hEvent, INFINITE);
// выполнение операций записи-чтения потоком 1

SetEvent( hEvent );
}

}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освождения события hEvent
WaitForSingleObject(hEvent, INFINITE);
// выполнение операций записи-чтения потоком 2

SetEvent( hEvent );
}

}

Пример 2. Два потока параллельно выполняют


операции чтения из блока памяти. Используется
одно событие с ручным сбросом.
HANDLE hEvent;
hEvent = CreateEvent( NULL, TRUE, TRUE, NULL );
// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освождения события hEvent
WaitForSingleObject(hEvent, INFINITE);
// выполнение операций чтения потоком 1
}

}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освождения события hEvent
WaitForSingleObject(hEvent, INFINITE);
// выполнение операций чтения потоком 2
}

}
Пример 3. Два потока последовательно выполняют
операции ввода данных и их обработки.
Используются два события с автосбросом, одно в
свободном состоянии, другое – в занятом.
HANDLE hEvent1, hEvent2;
hEvent1=CreateEvent( NULL, FALSE, TRUE, NULL );
hEvent2=CreateEvent( NULL, FALSE, FALSE, NULL );
// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освождения события hEvent1
WaitForSingleObject(hEvent1, INFINITE);
// выполнение операций ввода данных потоком 1

// перевод события hEvent2 в свобод. состояние
SetEvent(hEvent2);
}

}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освождения события hEvent2
WaitForSingleObject(hEvent2, INFINITE);
// выполнение обработки данных потоком 2

// перевод события hEvent1 в свобод. состояние
SetEvent(hEvent1);
}

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

Ожидаемые таймеры (waitable timers) — это объекты


ядра, которые самостоятельно переходят в
свободное состояние в определенное время или
через регулярные промежутки времени.
Ожидаемые таймеры бывают двух типов: со сбросом
вручную и с автосбросом. Когда освобождается
таймер со сбросом вручную, возобновляется
выполнение всех потоков, ожидавших этот объект, а
когда в свободное состояние переходит таймер с
автосбросом — лишь одного из потоков.
Функции для работы с ожидаемыми таймерами
следующие:
1. Создание объекта "ожидаемый таймер"
производится следующей функцией:
HANDLE CreateWaitableTimer(
LPSECURITY_ATTRIBUTES lpWTimerAttrib,
BOOL fManualReset, LPCTSTR lpName);
Параметры функции:
- lpWTimerAttrib является указателем на структуру
типа SECURITY_ATTRIBUTES, если не используется,
то задается NULL;
- fManualReset указывает, какого типа таймер будет
создан: со сбросом вручную (TRUE) или с
автосбросом (FALSE);
- lpName указывает на строку, содержащую имя
таймера, которое необходимо для доступа к объекту
других процессов. Если не нужен именованный
объект, то указывается NULL.
Функция CreateWaitableTimer возвращает описатель
таймера, используемый для работы с ним.
Объекты «ожидаемый таймер» всегда создаются в
занятом состоянии.
2. Задание времени и периодичности, когда
ожидаемый таймер будет переходить в свободное
состояние производится следующей функцией:
BOOL SetWaitableTimer( HANDLE hWTimer,
const LARGE_INTEGER *pDueTime, LONG lPeriod,
PTIMERAPCROUTINE pfnCompletionRoutine,
PVOID pvArgToCompletionRoutine, BOOL fResume);
где
- hWTimer – дескриптор ожидаемого таймера,
возвращенный функцией CreateWaitableTimer;
- pDueTime указывает на целое 64-битное значение,
где хранится дата и время (в формате UTC-
Coordinated Universal Time) первого перехода
таймера в свободное состояние. Если это значение
имеет отрицательный знак, то его абсолютная
величина указывает, через какой промежуток (в
интервалах по 100 нс) после вызова данной
функции таймер станет свободным;
- lPeriod задает периодичность (в миллисекундах)
следующих переходов таймера в свободное
состояние;
- pfnCompletionRoutine и pvArgToCompletionRoutine
задают имя APC-функции, вызываемой при переходе
таймера в свободное состояние и адрес
передаваемых ей данных. APC – это асинхронный
вызов функций (Asynchronous Procedure Call). Если
такая функция не используется, то в обеих
параметрах указываются NULL;
- fResume используется на компьютерах, поддержи-
вающих режим сна. Если он равен TRUE, то при
срабатывании таймера компьютер выйдет из
режима сна и "разбудит" потоки, ожидающие этот
таймер. В противном случае объект-таймер
перейдет в свободное состояние, но ожидавшие его
потоки не получат процессорное время, пока
компьютер не выйдет из режима сна.
Функция возвращает TRUE, если ее вызов выполнен
успешно и FALSE в противном случае.
3. Отключение срабатываний таймера производится
функцией:
BOOL CancelWaitableTimer(HANDLE hWTimer);
После этого восстановить работу таймера можно
повторным вызовом функции SetWaitableTimer() с
заданием новых времени и периодичности.
4. Уничтожение объекта hWTimer и освобождение
его дескриптора производится функцией:
BOOL CloseHandle(HANDLE hWTimer);

Взаимодействие потоков с ожидаемыми таймерами


происходит следующим образом.
После создания ожидаемого таймера, он должен
быть настроен на первое срабатывание функцией
SetWaitableTimer(). При наступлении заданного
момента времени ожидаемый таймер переходит в
свободное состояние и в зависимости от его типа
происходит следующее:
1) если таймер с автосбросом, то он автоматически
сбрасывается в занятое состояние, при этом только
один поток будет работать, а остальные потоки
останутся ждать. Для возобновления работы
потоков, ожидающих данного таймера необходимо
вызвать функцию SetWaitableTimer(), задав новое
время его срабатывания;
2) если таймер со сбросом вручную, то все потоки,
ожидающие этот таймер, получат управление, а он
так и останется в свободном состоянии, пока какой-
нибудь поток не вызовет CancelWaitableTimer().
После этого возобновить работу таймера можно
вызовом функции SetWaitableTimer() с заданием
новых времени и периодичности.
Ожидаемые таймеры отличаются от обычных
таймеров Windows (работающих в пользовательском
режиме и настраиваемых функцией SetTimer())
следующим: во-первых они являются объектами
ядра, а значит более защищенными; во-вторых они
более точные, т.к. сообщения WM_TIMER,
посылаемые обычными таймерами имеют низкий
приоритет и в очереди сообщений обрабатываются
последними.
Пример 1. Два потока синхронизируются объектом
"ожидаемый таймер" с автосбросом. Время
срабатывания таймера является относительным и
задается в количестве интервалов по 100 нс. Первое
срабатывание – через 10 мс после создания
таймера, последующие – через 2 мс после
завершения потоком своих операций. (1 мс равна
10000 интервалов по 100 нс)
HANDLE hWTimer;
LARGE_INTEGER li;
hWTimer=CreateWaitableTimer(NULL, FALSE, NULL);
// Задаем отрицательное число в количестве
// интервалов по 100 нс
li.QuadPart = - (10 * 10000);
// Устанавливаем первое срабатывание через 10 мс
SetWaitableTimer(hWTimer,&li, 0, NULL, NULL, FALSE);

// Функция потока 1
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освобождения таймера hWTimer
WaitForSingleObject(hWTimer, INFINITE);
// выполнение операций потоком 1
...
// Задаем следующее срабатывание через 2 мс
li.QuadPart = - (2 * 10000);
SetWaitableTimer(hWTimer,&li,0,NULL,NULL,FALSE);
}

}
// Функция потока 2
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{

while (!p->stop)
{
// ожидание освобождения таймера hWTimer
WaitForSingleObject(hWTimer, INFINITE);
// выполнение операций потоком 2
...
// Задаем следующее срабатывание через 2 мс
li.QuadPart = - (2 * 10000);
SetWaitableTimer(hWTimer,&li,0,NULL,NULL,FALSE);
}

}

Пример 2. Устанавливаем ожидаемый таймер, чтобы


он срабатывал ежедневно в 12.00 (время перерыва
на обед) и вызывал APC-функцию, которая выдает
звуковой сигнал 1000 гц.
HANDLE hWTimer;
SYSTEMTIME st;
FILETIME ftLocal, ftUTC;
LARGE_INTEGER liUTC;
// callback функция таймера
VOID CALLBACK TimerAPCProc(LPVOID, DWORD,
DWORD)
{
Beep(1000,500); // выдается звуковой сигнал
};
...
// создаем таймер с автосбросом
hWTimer = CreateWaitableTimer(NULL, FALSE, NULL);
// узнаем текущую дату/время
GetLocalTime(&st);
// если назначенный час уже наступил,
// то ставим время на завтра
if (st.wHour > 12) st.wDay++;
st.wHour = 12;
st.wMinute = 0;
st.wSecond = 0;
// преобразуем время из SYSTEMTIME в FILETIME
SystemTimeToFileTime(&st, &ftLocal);
// преобразуем местное время в UTC-время
LocalFileTimeToFileTime(&ftLocal, &ftUTC);
// преобразуем FILETIME в LARGE_INTEGER из-за
// различий в выравнивании данных
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;
// устанавливаем таймер
SetWaitableTimer(hWTimer, &liUTC, 24*60*60*1000,
TimerAPCProc, NULL, FALSE);
...
Синхронизация потоков разных процессов

Для синхронизации потоков, выполняемых в разных


процессах, могут быть использованы рассмотренные
ранее средства синхронизации – объекты ядра,
кроме критических секций. Наиболее часто для этих
целей применяются именованные объекты.
Если именованный объект синхронизации создан и
используется потоком в одном процессе, то поток в
другом процессе может получить доступ к этому
объекту путем вызова Create-функции или Open-
функции с указанием имени объекта;
Рассмотрим подробнее эти способы.

Создание именованного объекта


Именованный объект синхронизации может быть
создан одной из Create-функций, если в последнем
ее параметре lpName указать не пустое значение
NULL, а строку с именем этого объекта, например:
hMutex1=CreateMutex(NULL, FALSE, "MyMutex");
Аналогично именованные объекты создаются
другими Create-функциями: CreateSemaphore(),
CreateEvent(), CreateWaitableTimer().
Имя объекту можно задать произвольно, но следует
избегать повторения имен объектов разных типов,
т.к. это приведет к ошибке при создании нового
объекта.

Получение доступа к именованному объекту с


помощью Create-функции
В другом процессе вызывается Create-функция,
соответствующая типу объекта, в ней задается имя
уже существующего объекта:
hMutex2=CreateMutex(NULL, FALSE, "MyMutex");
Перед тем, как создавать новый объект, Create-
функция проверяет, не существует ли уже объект с
таким именем. Если существует, то она возвращает
описатель этого объекта и увеличивает счетчик
числа его пользователей на 1. Если объект с таким
именем не существует, то функция создает новый
объект с этим именем.

Получение доступа к именованному объекту с


помощью Open-функции
В другом процессе вызывается Open-функция,
соответствующая типу объекта, в ней задается имя
уже существующего объекта:
HANDLE OpenMutex( DWORD dwDesiredAccess,
BOOL blnheritHandle, LPCTSTR lpName);
или
HANDLE OpenSemaphore(...);
HANDLE OpenEvent(...);
HANDLE OpenWaitableTimer(...);
с аналогичным списком параметров.
Параметры функций:
- dwDesiredAccess определяет права доступа в этом
процессе к существующему объекту. Если при
создании объекта Create-функцией в первом
параметре был указан NULL, то объект имеет
стандартный набор прав доступа. В этом случае в
Open-функции в первом параметре обычно
указывается MUTEX_ALL_ACCESS (аналогично для
других объектов с заменой MUTEX на тип объекта);
- blnheritHandle указывает, что описатель объекта
будет наследуемым от родительского процесса (при
TRUE) и не наследуемым в противном случае;
- lpName задает имя объекта.
Например:
hMutex2 = OpenMutex( MUTEX_ALL_ACCESS, FALSE,
"MyMutex");
Функция проверяет, существует ли в системе объект
ядра типа мьютекс с именем MyMutex. Если такого
объекта нет или есть объект с таким именем, но
другого типа, то возвращается пустой дескриптор
(NULL). Если требуемый объект существует, то
разрешен ли к нему доступ запрошенного вида. При
положительном исходе проверки счетчик
количества пользователей объекта увеличивается
на 1 и функция возвращает его описатель.
Таким образом, дескрипторы hMutex1 и hMutex2
указывают на один и тот же объект и могут
использоваться для синхронизации работы потоков
в двух разных процессах.

Другие средства синхронизации потоков


К ним можно отнести семейство Interlocked-
функций. Эти функции обеспечивают атомарный
доступ (монопольный захват) к глобальным
переменным, которые используются разными
потоками.
Имеется несколько видов Interlocked-функций:
1. Функции изменения переменных:
- увеличение значения переменной на 1
LONG InterlockedIncrement(PLONG plAddend);
- уменьшение значения переменной на 1
LONG InterlockedDecrement(PLONG plAddend);
- изменение значения переменной
LONG InterlockedExchangeAdd( PLONG plAddend,
LONG lIncrement);
Параметры функций:
- plAddend это указатель на переменную, значение
которой будет изменено;
- lIncrement величина, на которую будет изменена
исходная переменная.
InterlockedExchangeAdd позволяет не только
увеличить, но и уменьшить значение, для этого во
втором параметре задается отрицательная
величина.
Все три функции возвращают исходное значение,
хранящееся в *plAddend.
Пример изменения глобальной переменной.
// определяем глобальную переменную
long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam) {
InterlockedExchangeAdd(&g_x, 1);
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvParam) {
InterlockedExchangeAdd(&g_x, 1);
return(0);
}
2. Функция замены значения переменной на другое
LONG InterlockedExchange( PLONG plTarget,
LONG lValue);
Эта функция монопольно заменяет текущее
значение переменной типа LONG, адрес которой
передается в первом параметре (plTarget), на
значение, передаваемое во втором параметре
(lValue).
Функция InterlockedExchange возвращает исходное
значение, хранящееся в *plTarget.
Пример.
// глобальная переменная, используется как
// индикатор того, занят ли разделяемый ресурс
BOOL g_fResourceInUse = FALSE;
void Func1 () {
// ожидаем доступа к ресурсу
while (InterlockedExchange(&g_fResourceInUse, TRUE)
== TRUE)
Sleep(0);
// получаем ресурс в свое распоряжение
...
// доступ к ресурсу больше не нужен
InterlockedExchange(&g_fResourceInUse, FALSE);
}
В цикле while переменной g_fResourceInUse
присваивается значение TRUE и проверяется ее
предыдущее значение. Если оно было равно FALSE,
значит, ресурс не был занят, но вызывающий поток
только что занял его; на этом цикл завершается. В
ином случае (значение было равно TRUE) ресурс
занимал другой поток, и цикл повторяется.
3. Функция сравнения переменной с заданным
значением
PVOID InterlockedCompareExchange(
PLONG plDestination,
LONG lExchange,
LONG lComparand):
Функция сравнивает текущее значение переменной
типа LONG (на которую указывает параметр
plDestination) со значением, передаваемым в
параметре lComparand. Если значения совпадают,
*plDestination получает значение параметра
lExcbange; в ином случае *plDestination остается без
изменений.
Функция возвращает исходное значение,
хранящееся в *plDestination.
Пример кода функции, иллюстрирующий ее работу.
LONG InterlockedCompareExchange(
PLONG plDestination, LONG lExchange,
LONG lComparand)
{
LONG IRet = *plDestination; // исходное значение
if (*plDestination == lComparand)
*plDestination = lExchange;
return(lRet);
}

Interlocked-функции можно также использовать в


потоках различных процессов для синхронизации
доступа к переменной, которая находится в
разделяемой области памяти, например в проекции
файла.
Сравнение объектов синхронизации
Объект Относительная Доступ Подсчет числа
скорость нескольких обращений к
процессов ресурсу
Критическая быстро Нет Нет
секция
Мьютекс медленно Да Нет
Семафор медленно Да Автоматически
Событие медленно Да Да

Вам также может понравиться