Синхронизация с помощью критических секций

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

1) InitializeCriticalSection (p);

2) EnterCriticalSection (p);

3) TryEnterCriticalSection (p);

4) LeaveCriticalSection (p);

5) DeleteCriticalSection (p).

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

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

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

В программе описывается объект типа Critical_Section и считается, что имя этого объекта логически связано с используемым разделяемым ресурсом. Перед тем как начать работу с указанным объектом, его инициализируют путем вызова InitializeCriticalSection. После инициализации объекта, в каждом из параллельных потоков перед входом в собственную критическую секцию потока вызывается функция 2. Она исключает одновременный вход в критические секции в параллельных потоках. После завершения работы с разделяемым ресурсом поток должен покинуть свою критическую секцию. Для подтверждения этого факта, после критической секции следует вызов LeaveCriticalSection.

По окончании работы с CS все системные ресурсы, использованные этим объектом, освобождаются путем вызова DeleteCriticalSection.

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

Мьютексы.

Для решения проблемы взаимного исключения между параллельными потоками, которые выполняются в контекстах разных процессов, в Windows используется объект ядра системы с названием Mutex.

Мьютекс находится в сигнальном, то есть свободном состоянии, если он не принадлежит ни одному потоку. В противном случае, мьютекс находится в несигнальном, занятом, состоянии. Это состояние говорит о том, что один из потоков использует разделяемый ресурс. Одновременно, мьютекс может принадлежать только одному потоку. Остальные потоки находятся в состоянии ожидания освобождения мьютекса, то есть перехода его в сигнальное состояние. Все эти потоки обслуживаются в порядке дисциплины FIFO, то есть они становятся в очередь к мьютексу, и выбираются из очереди в соответствии с этой дисциплиной. Но в том случае, если какой-либо поток ожидает наступления некоторого асинхронного события, то есть внешнего по отношению к системе, то функции ядра системы могут исключить поток из очереди к мьютексу для обслуживания этого события. После этого, поток снова становится в очередь к мьютексу, но в её конец.

Мьютекс создается вызовом в потоке функции CreateMutex, которая имеет следующий формат:

CreateMutex (p1, p2, p3);

· p1 – атрибуты защиты объекта;

· p2 – начальный владелец;

· p3 – имя мьютекса.

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

Если p2 == true, то мьютекс сразу переходит во владение того потока, в котором был создан. Такой поток имеет все права доступа к мьютексу. Если p2 == false, то созданный мьютекс свободен.

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

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

Если мьютекс с заданным в p3 именем уже существует, то функция CreateMutex возвращает дескриптор уже созданного мьютекса, а функция GetLastError, вызванная после функции CreateMutex, возвращает значение ошибки в виде именованной константы ERROR_ALREADY_EXISTS.

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

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

Для доступа к уже существующему мьютексу поток может использовать одну из двух функций:

· CreateMutex (p1,p2,p3);

· OpenMutex ();

CreateMutex используется, если поток не имеет информации о том, создан, или нет, мьютекс с таким же именем в другом потоке. В этом случае, p2 нужно задавать как false, поскольку невозможно определить, какой из потоков создает мьютекс.

При использовании для доступа к уже созданному мьютексу CreateMutex, поток получает полный доступ к мьютексу.

Чтобы получить доступ к уже созданному мьютексу, может использоваться OpenMutex (po1, po2, po3);

· po1 – вид доступа к мьютексу;

· po2 – определяет свойства наследования мьютекса;

· po3 – имя мьютекса.

po1 может иметь два значения:

1) полный доступ к мьютексу;

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

Если po2 имеет значение истины, то дескриптор открываемого мьютекса является наследуемым в других дочерних потоках.

В случае успешного завершения, функция OpenMutex возвращает дескриптор открытого мьютекса, а в противном случае – ноль.

События.

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

В ОС Windows, событие описывается объектами ядра системы Events. Существует два типа событий:

1) события с ручным сбросом;

2) события с автоматическим сбросом.

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

События создаются путем вызова CreateEvent, которая имеет четыре параметра - pe1, pe2, pe3, pe4.

· pe1 – атрибуты защиты;

· pe2 – тип события;

· pe3 – начальное состояние;

· pe4 – имя события.

Чаще всего, первый параметр получает значение NULL. Если pe2 == true, то создается событие с ручным сбросом, в противном случае – с автоматическим. Если pe3==true, то начальное состояние события является сигнальным, а в противном случае – несигнальным. pe4 определяет имя события, которое позволяет обращаться к нему из потоков, выполняющихся в разных процессах. Событие может быть безымянным.

В случае удачного завершения, CreateEvent возвращает Handle, и NULL – в противном случае. Если событие с указанным в pe4 именем уже существует, то CreateEvent возвращает дескриптор существующего события, а GetLastError будет возвращать ERROR_ALREADY_EXISTS.

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

Функция ResetEvent (Handle) используется для перевода любого события в несигнальное состояние. Возвращает аналогичные значения.

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

Доступ к уже существующему событию может быть открыт с помощью CreateEvent или OpenEvent. При использовании CreateEvent, значения второго и третьего её параметров игнорируются, поскольку они уже заданы другим потоком. Поток, в котором вызывается функция CreateEvent, получает полный доступ к событию с именем, заданным в четвертом параметре. Функция OpenEvent используется в том случае, когда заранее известно, что поток с заданным именем уже существует. Формат:

OpenEvent (po1, po2, po3);

· po1 – определяет флаги доступа;;

· po2 – режим наследования;

· po3 – имя события.

Смысл второго и третьего параметров аналогичен этим параметрам в мьютексе.

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

Флаг a означает полный доступ – поток может выполнять над событием любые действия. Флаг b называется модификацией состояния. Он означает, что поток может использовать функции SetEvent и ResetEvent для изменения состояния события. Флаг c называется синхронизацией. Поток может использовать события в функциях ожидания.

Семафоры.

Объект «семафор» в ОС Windows описывается объектом ядра Semaphore. Семафор находится в сигнальном состоянии, если его значение, называемое счетчиком семафора, больше нуля. В противном случае, семафор находится в несигнальном состоянии. Потоки, ожидающие сигнального состояния семафора, выстраиваются в очередь к нему, и обслуживаются в соответствии с дисциплиной FIFO. Если какой-либо из этих потоков ожидает наступления асинхронного события, то функции ядра могут исключить поток из очереди к семафору для обслуживания этого события (при его наступлении). После этого поток переводится в конец очереди к семафору.

Семафоры создаются вызовом функции CreateSemaphore (ps1, ps2, ps3, ps4).

· ps1 определяет атрибуты защиты объекта;

· ps2 определяет начальное значение счетчика семафора, которое должно быть не меньше нуля, и не больше его максимального значения, которое задается параметром ps3.

· ps4 задает имя семафора, либо может иметь значение пустой строки. В этом случае создается безымянный семафор.

В случае успешного завершения возвращается Handle созданного объекта, и ноль – в противном случае. Если семафор с заданным именем уже существует, то функция возвращает дескриптор ранее созданного семафора, а GetLastError возвращает ERROR_ALREADY_EXISTS.

Значение счетчика семафора уменьшается на единицу при его использовании в функции ожидания. Увеличить значение счетчика можно путем вызова ReleaseSemaphore(Handle, pr2, pr3).

· Handle – дескриптор семафора;

· pr2 – целое положительное число, на которое увеличивается значение семафора;

· pr3 – предыдущее значение семафора.

В случае успешного завершения, функция возвращает ненулевое значение, и нулевое – в противном случае.

Если значение счетчика семафора в сумме с pr2 оказывается больше ps3, то данная функция ReleaseSemaphore возвращает ноль, и значение счетчика не меняется.

Доступ к существующему семафору можно открыть либо путем вызова CreateSemaphore (ps1, ps2, ps3, ps4), либо вызова OpenSemaphore(po1, po2, po3).

При использовании CreateSemaphore, второй и третий её параметры игнорируются, поскольку они уже заданы другим потоком, при этом поток, вызвавший эту функцию, получает полный доступ к семафору с именем, заданным в четвертом параметре.

Функция OpenSemaphore используется в случае, когда заранее известно, что семафор с заданным именем уже существует. Параметры аналогичны параметрам OpenEvent.

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

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

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