
Лабораторная работа № 3. Синхронизация потоков в Win32api Цель работы
Изучение основных механизмов синхронизации потоков, реализованных в операционных системах семейства Windows.
Получение навыков по реализации синхронизации потоков при разработке приложений Windows.
Овладение методами обнаружения взаимоблокировок.
Краткие теоретические сведения
В многопоточных приложениях часто возникает ситуация, когда нескольким потокам необходимо получить доступ к одним и тем же ресурсам. Если при этом одним потокам нужно модифицировать данные, а другим потокам их читать, то возникает проблема, связанная с необходимостью предотвратить одновременный доступ к ресурсу сразу со стороны нескольких потоков. Решается эта проблема с помощью синхронизации потоков.
В общем случае поток синхронизирует себя с другими так: он засыпает, и операционная система, не выделяя ему процессорное время, приостанавливает его исполнение. Однако, перед тем как заснуть, поток сообщает системе, какое событие должно произойти, чтобы его исполнение возобновилось. Как только указанное событие произойдет, поток вновь получит право на выделение ему процессорного времени.
Для синхронизации потоков системы, базирующиеся на Win32, предлагают несколько синхронизирующих объектов, основными из которых являются: критические секции (critical section), взаимные исключения (mutex – сокращение от mutual exclusion), семафоры и события. Все они кроме критических секций являются объектами ядра. Кроме того, в качестве синхронизирующих объектов могут использоваться процессы, потоки, файлы, консольный ввод, уведомления об изменении файлов.
Критическая секция – это некоторый участок кода, который в каждый момент времени может выполняться только одним из потоков. Другие потоки не могут войти в этот участок кода до тех пор, пока вошедший в этот участок кода поток не завершит его выполнение. Критические секции могут быть использованы для синхронизации потоков одного процесса.
Взаимное исключение (mutex) – это объект ядра операционной системы, который позволяет только одному потоку в данное время владеть им.
Семафор – это объект ядра операционной системы, который позволяет лишь определенному количеству потоков иметь доступ к нему. Семафор устанавливается на предельное число потоков, которым доступ разрешен. Когда это число будет достигнуто, последующие потоки будут приостановлены до тех пор, пока один или более потоков не отсоединятся и не освободят доступ.
Событие – это объект ядра операционной системы, который может иметь состояние «свободный» и состояние «занятый», перевод между которыми может осуществляться потоком. Используется в том случае когда некий инициирующий поток выполняет некие действия, после которых должны начать работать рабочие потоки. Пока событие занято рабочие потоки приостанавливают своё выполнение. Инициирующий поток, выполнив необходимые действия, переводит объект в «свободное» состояние. Рабочие потоки, обнаружив перевод объекта в свободное состояние, возобновляют свою работу.
К основным функциям Win32 API по управлению синхронизацией относятся:
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection) – инициализировать критическую секцию; в функцию необходимо передать адрес структуры CRITICAL_SECTION.
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection) – освободить все ресурсы, включенные в критическую секцию (удалить критическую секцию); в функцию необходимо передать адрес структуры CRITICAL_SECTION.
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection) – захватить блокировку для критической секции; в функцию необходимо передать адрес переменной-структуры CRITICAL_SECTION.
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection) – освободить блокировку для критической секции; в функцию необходимо передать адрес переменной-структуры CRITICAL_SECTION.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsa, BOOL fInitialOwner, LPTSTR lpszMutexName) – создать новый мьютекс; возвращает описатель, идентифицирующий новый объект mutex. Параметр lpsa указывает на структуру SECURITY_ATTRIBUTES (можно передать NULL). Параметр fInitialOwner определяет: должен ли поток, создающий mutex, быть первоначальным владельцем этого объекта. Если он равен TRUE, данный поток становится его владельцем, и, следовательно, объект mutex оказывается в занятом состоянии. Передача FALSE в параметре fInitialOwner подразумевает, что объект mutex не принадлежит ни одному из потоков и поэтому “рождается свободным”. Параметр lpszMutexName содержит либо NULL, либо адрес нуль терминированной строки, содержащей имя мьютекса, используемое для получения его описателя из других процессов.
HANDLE OpenMutex(DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName) – открыть существующий мьютекс; возвращает описатель объекта mutex по его имени. Параметр fdwAccess может быть равен либо SYNCHRONIZE либо MUTEX_ALL_ACCESS. Параметр fInherit определяет: должен ли любой порожденный процесс наследовать данный описатель данного объекта mutex. Параметр lpszName – это имя объекта mutex в виде строки с нулевым символом на конце.
BOOL ReleaseMutex(HANDLE hMutex) –освободить мьютекс, чтобы другой поток мог его захватить; функции необходимо передать описатель объекта mutex. При успешном завершении возвращает ненулевое значение (TRUE), при ошибке – 0 (FALSE).
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpsa, LONG cSemInitial, LONG cSemMax, LPTSTR lpszSemName)– создать новый семафор; возвращает дескриптор вновь созданного семафора. Параметр lpsa указывает на структуру SECURITY_ATTRIBUTES (можно передать NULL). Параметр cSemInitial устанавливает начальное значение счетчика. Параметр cSemMax устанавливает максимальное значение счетчика. Параметр lpszSemName содержит либо NULL, либо адрес нуль терминированной строки, содержащей имя семафора, используемое для получения его описателя из других процессов.
HANDLE OpenSemaphore(DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName) – открыть существующий семафор; возвращает описатель объекта семафор по его имени. Параметр fdwAccess может быть равен либо SYNCHRONIZE (в Windows NT) либо SEMAPHORE_ALL_ACCESS, либо SEMAPHORE_MODIFY_STATE. Параметр fInherit определяет: должен ли любой порожденный процесс наследовать данный описатель данного объекта семафор. Параметр lpszName – это имя объекта семафор в виде строки с нулевым символом на конце. Используется для получения описателя семафора по его имени из другого процесса, создавшего его.
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG cRelease, LPLONG lplPrevious) – увеличить счетчик семафора на величину, указанную в параметре cRelease, чтобы другой поток мог его захватить. В параметр hSemaphore передается описатель семафора. Параметр lplPrevious – указатель на переменную типа long, куда заносится значение счетчика ресурсов, предшествующее тому, что получится после увеличения его на cRelease. Если это значение не нужно, то в параметр можно передать NULL. При успешном завершении возвращает ненулевое значение (TRUE), при ошибке – 0 (FALSE).
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpsa, BOOL fManualReset, BOOL fInitialState, LPTSTR lpszEventName) – создать новое событие; возвращает процессно-зависимый описатель события. Параметр lpsa указывает на структуру SECURITY_ATTRIBUTES (можно передать NULL). Параметр fManualReset определяет: создавать ли событие с ручным сбросом (TRUE) или с автоматическим сбросом (FALSE). Параметр fInitialState указывает начальное состояние события: свободное (TRUE) или занятое (FALSE). Параметр lpszEventName содержит либо NULL, либо адрес нуль терминированной строки, содержащей имя события, используемое для получения его описателя из других процессов.
HANDLE OpenEvent(DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName) – получить доступ к событию; возвращает описатель объекта событие по его имени. Параметр fdwAccess может быть равен либо SYNCHRONIZE (в Windows NT) либо EVENT_ALL_ACCESS, либо EVENTE_MODIFY_STATE. Параметр fInherit определяет: должен ли любой порожденный процесс наследовать данный описатель данного объекта событие. Параметр lpszName – это имя объекта событие в виде строки с нулевым символом на конце. Используется для получения описателя события по его имени из другого процесса, создавшего его. Получить описатель события из другого процесса можно также вызовом функции CreateEvent, передав в него тоже имя семафора, что и при создании в другом процессе.
BOOL SetEvent(HANDLE hEvent) – перевести событие в свободное состояние. Функция принимает в качестве аргумента описатель события и возвращает TRUE в случае успешного завершения.
BOOL ResetEvent(HANDLE hEvent) – перевести событие в занятое состояние. Функция принимает в качестве аргумента описатель события и возвращает TRUE в случае успешного завершения.
BOOL PulseEvent(HANDLE hEvent) – перевести событие в сигнализирующее (свободное) состояние, а затем вернуть в несигнализирующее (занятое) . Функция принимает в качестве аргумента описатель события и возвращает TRUE в случае успешного завершения.
Для закрытия объектов мьютекс, семафор, событие используется функция CloseHandle, в которую в качестве аргумента необходимо передать дескриптор закрываемого объекта.
DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeout) – блокироваться на одном объекте (семафоре, мьютексе и т.д.). Возвращаемые значения: WAIT_OBJECT_0 – объект перешел в состояние свободного; WAIT_TIMEOUT – объект не перешел в состояние свободного за указанный в dwTimeout период времени; WAIT_ABANDONED – объект mutex стал свободен из-за отказа от него (когда поток занял его и завершился); WAIT_FAILED – произошла ошибка, для получения развернутой информации об ошибке нужно вызвать (сразу же) функцию GetLastError. Параметр hObject указывает на описатель объекта ядра, освобождение которого ожидает поток. Параметр dwTimeout определяет, сколько времени (в миллисекундах) поток готов ждать освобождение объекта. В параметре dwTimeout можно передать два особых значения: 0 – означает, что нужно не ждать, а выяснить, занят объект или нет (если функция возвратит WAIT_OBJECT_0, то объект свободен, а если возвратит WAIT_TIMEOUT, то объект – занят); INFINITE – означает, что нужно пока объект не освободится столько времени, сколько придется.
DWORD WaitForMultipleObject(DWORD cObjects, LPHANDLE lpHandles, BOOL bWaitAll, DWORD dwTimeout) – блокироваться на множестве объектов, чьи дескрипторы перечисляются. В параметре cObjects передается количество объектов, освобождение которых ждет поток (не должно превышать MAXIMUM_WAIT_OBJECTS). Параметр lpHandles – указатель на массив описателей, идентифицирующих эти объекты. Параметр bWaitAll определяет: поток ждет освобождения всех объектов (TRUE) или лишь одного объекта (FALSE). Параметр dwTimeout определяет, сколько времени (в миллисекундах) поток готов ждать освобождение объекта. Если освобождается сразу несколько объектов, функция возвращает индекс описателя в массиве, идентифицирующего первый освобожденный объект. Функция возвращает одно из следующих значений: от WAIT_OBJECT_0 до (WAIT_OBJECT_0+cObject-1) – при ожидании всех объектов это значение указывает на то, что ожидание закончилось успешно; при ожидании одного из объектов это значение представляет собой индекс того описателя в массиве lpHandles, что принадлежит освобожденному объекту; WAIT_TIMEOUT – объект или объекты не освободились за указанный в dwTimeout период времени; от WAIT_ABANDONED_0 до (WAIT_ ABANDONED_0+cObject-1) – при ожидании всех объектов это значение указывает на то, что ожидание закончилось успешно и что по крайней мере один объект был объектом mutex, который освобожден из-за отказа от него; при ожидании одного из объектов это значение представляет собой индекс того описателя в массиве lpHandles, что принадлежит объекту mutex, освобожденному по причине от него; WAIT_FAILED – произошла ошибка, для получения развернутой информации об ошибке нужно вызвать (сразу же) функцию GetLastError.
Осуществление синхронизации потоков создает дополнительные проблемы. Одна из них взаимоблокировки (тупики). Взаимная блокировка – это состояние, когда каждый из двух потоков ждет освобождения ресурса, заблокированного другим потоком.
Пусть имеется множество процессов P={P1, P2, …, Pn}, всего n процессов и множество ресурсов E={E1, E2, …, Em}, где m – число классов ресурсов. В любой момент времени некоторые из ресурсов могут быть заняты и, соответственно, недоступны. Пусть A – вектор доступных ресурсов A={A1, A2, …, Am}. Очевидно, что Aj≤Ej, j=1, 2, …, m.
Введем в рассмотрение две матрицы:
C={cij| i=1, 2,…,n; j=1, 2,…,m} – матрица текущего распределения ресурсов, где cij – количество ресурсов j-го класса, которые занимает процесс Pi;
R={rij| i=1, 2,…,n; j=1,2,…,m} – матрица требуемых (запрашиваемых) ресурсов, где rij – количество ресурсов j-го класса, которые хочет получить процесс Pi.
Справедливо m соотношений по ресурсам:
,
j=1,
2, …, m.
Алгоритм обнаружения взаимоблокировок основан на сравнении векторов доступных и требуемых объектов. В исходном состоянии все процессы не маркированы (не отмечены). По мере реализации алгоритма на процессы будет ставиться отметка, служащая признаком того, что они могут закончить свою работу, и, следовательно, не находятся в тупике. После завершения алгоритма любой немаркированный процесс находится в тупиковой ситуации. Алгоритм обнаружения тупиков состоит из следующих шагов.
Ищется процесс Pi, для которого i-я строка матрицы R меньше вектора A, то есть Ri ≤ A, или rji ≤ Aj, j =1, 2, …, m.
Если такой процесс найден, это означает, что он может завершиться, а, следовательно, освободить занятые ресурсы. Найденный процесс маркируется, и далее прибавляется i-я строка матрицы C к вектору A, то есть Aj= Aj+cij, j=1,2,…,m. Возвращаемся к первому шагу.
Если таких процессов не существует, работа алгоритма заканчивается. Немаркированные процессы попадают в тупик.