Синхронизация процессов в ос windows семафоры

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

Синхронизация процессов и потоков

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

Причем любой процесс имеет хотя бы одну нить. Эта нить называется главной (основной) нитью приложения.

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

В зависимости от ситуации нити могут находиться в трех состояниях. Во-первых, нить может выполняться, когда ей выделено процессорное время, т.е. она может находиться в состоянии активности. Во-вторых, она может быть неактивной и ожидать выделения процессора, т.е. быть в состоянии готовности. И есть еще третье, тоже очень важное состояние — состояние блокировки. Когда нить заблокирована, ей вообще не выделяется время. Обычно блокировка ставится на время ожидания какого-либо события. При возникновении этого события нить автоматически переводится из состояния блокировки в состояние готовности. Например, если одна нить выполняет вычисления, а другая должна ждать результатов, чтобы сохранить их на диск. Вторая могла бы использовать цикл типа «while( !isCalcFinished ) continue;», но легко убедиться на практике, что во время выполнения этого цикла процессор занят на 100 % (это называется активным ожиданием). Таких вот циклов следует по возможности избегать, в чем оказывает неоценимую помощь механизм блокировки. Вторая нить может заблокировать себя до тех пор, пока первая не установит событие, сигнализирующее о том, что чтение окончено.

Синхронизация нитей в ОС Windows

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

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

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

#include <windows.h>
#include <stdio.h>
int a[5];
HANDLE hThr;
unsigned long uThrID;
void Thread( void* pParams )
{ 
	int i, num = 0;
	while (1)
	{ 
		for (i=0; i<5; i++) a[i] = num;
		num++;
	}
}

int main( void )
{ 
	hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID);
	while(1)
	printf("%d %d %d %d %dn", a[0], a[1], a[2], a[3], a[4]);
	return 0;
}

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

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

Объектов синхронизации существует несколько, самые важные из них — это взаимоисключение (mutex), критическая секция (critical section), событие (event) и семафор (semaphore). Каждый из этих объектов реализует свой способ синхронизации. Также в качестве объектов синхронизации могут использоваться сами процессы и нити (когда одна нить ждет завершения другой нити или процесса); а также файлы, коммуникационные устройства, консольный ввод и уведомления об изменении.

Любой объект синхронизации может находиться в так называемом сигнальном состоянии. Для каждого типа объектов это состояние имеет различный смысл. Нити могут проверять текущее состояние объекта и/или ждать изменения этого состояния и таким образом согласовывать свои действия. При этом гарантируется, что когда нить работает с объектами синхронизации (создает их, изменяет состояние) система не прервет ее выполнения, пока она не завершит это действие. Таким образом, все конечные операции с объектами синхронизации являются атомарными (неделимыми.

Работа с объектами синхронизации

Чтобы создать тот или иной объект синхронизации, производится вызов специальной функции WinAPI типа Create… (напр. CreateMutex). Этот вызов возвращает дескриптор объекта (HANDLE), который может использоваться всеми нитями, принадлежащими данному процессу. Есть возможность получить доступ к объекту синхронизации из другого процесса — либо унаследовав дескриптор этого объекта, либо, что предпочтительнее, воспользовавшись вызовом функции открытия объекта (Open…). После этого вызова процесс получит дескриптор, который в дальнейшем можно использовать для работы с объектом. Объекту, если только он не предназначен для использования внутри одного процесса, обязательно присваивается имя. Имена всех объектов должны быть различны (даже если они разного типа). Нельзя, например, создать событие и семафор с одним и тем же именем.

По имеющемуся дескриптору объекта можно определить его текущее состояние. Это делается с помощью т.н. ожидающих функций. Чаще всего используется функция WaitForSingleObject. Эта функция принимает два параметра, первый из которых — дескриптор объекта, второй — время ожидания в мсек. Функция возвращает WAIT_OBJECT_0, если объект находится в сигнальном состоянии, WAIT_TIMEOUT — если истекло время ожидания, и WAIT_ABANDONED, если объект-взаимоисключение не был освобожден до того, как владеющая им нить завершилась. Если время ожидания указано равным нулю, функция возвращает результат немедленно, в противном случае она ждет в течение указанного промежутка времени. В случае, если состояние объекта станет сигнальным до истечения этого времени, функция вернет WAIT_OBJECT_0, в противном случае функция вернет WAIT_TIMEOUT. Если в качестве времени указана символическая константа INFINITE, то функция будет ждать неограниченно долго, пока состояние объекта не станет сигнальным.

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

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

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

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

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

#include <windows.h>
#include <stdio.h>
CRITICAL_SECTION cs;
int a[5];
HANDLE hThr;
unsigned long uThrID;

void Thread( void* pParams )
{ 
	int i, num = 0;
	while (1)
	{ 
		EnterCriticalSection( &cs );
		for (i=0; i<5; i++) a[i] = num;
		num++;
		LeaveCriticalSection( &cs );
	}
}

int main( void )
{ 
	InitializeCriticalSection( &cs );
	hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID);
	while(1)
	{
		EnterCriticalSection( &cs );
		printf("%d %d %d %d %dn", a[0], a[1], a[2], a[3], a[4]);
		LeaveCriticalSection( &cs );
	}
	return 0;
}

Взаимоисключения

Объекты-взаимоисключения (мьютексы, mutex — от MUTual EXclusion) позволяют координировать взаимное исключение доступа к разделяемому ресурсу. Сигнальное состояние объекта (т.е. состояние «установлен») соответствует моменту времени, когда объект не принадлежит ни одной нити и его можно «захватить». И наоборот, состояние «сброшен» (не сигнальное) соответствует моменту, когда какая-либо нить уже владеет этим объектом. Доступ к объекту разрешается, когда нить, владеющая объектом, освободит его.

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

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

  • Дочерний процесс, созданный при помощи функции CreateProcess может наследовать дескриптор мьютекса в случае, если при создании мьютекса функцией CreateMutex был указан параметр lpMutexAttributes.
  • Нить может получить дубликат существующего мьютекса с помощью функции DuplicateHandle.
  • Нить может указать имя существующего мьютекса при вызове функций OpenMutex или CreateMutex.

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

Для синхронизации нитей одного процесса более эффективно использование критических секций.

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

#include <windows.h>
#include <stdio.h>
HANDLE hMutex;
int a[5];
HANDLE hThr;
unsigned long uThrID;

void Thread( void* pParams )
{ 
	int i, num = 0;
	while (1)
	{ 
		WaitForSingleObject( hMutex, INFINITE );
		for (i=0; i<5; i++) a[i] = num;
		num++;
		ReleaseMutex( hMutex );
	}
}

int main( void )
{ 
	hMutex=CreateMutex( NULL, FALSE, NULL );
	hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID);
	while(1)
	{
		WaitForSingleObject( hMutex, INFINITE );
		printf("%d %d %d %d %dn", a[0], a[1], a[2], a[3], a[4]);
		ReleaseMutex( hMutex );
	}
return 0;
}

События

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

Функция CreateEvent создает объект-событие, SetEvent — устанавливает событие в сигнальное состояние, ResetEvent — сбрасывает событие. Функция PulseEvent устанавливает событие, а после возобновления ожидающих это событие нитей (всех при ручном сбросе и только одной при автоматическом), сбрасывает его. Если ожидающих нитей нет, PulseEvent просто сбрасывает событие.

Пример. Синхронизация нитей с помощью событий.

#include <windows.h>
#include <stdio.h>
HANDLE hEvent1, hEvent2;
int a[5];
HANDLE hThr;
unsigned long uThrID;
void Thread( void* pParams )
{ 
	int i, num = 0;
	while (1)
	{ 
		WaitForSingleObject( hEvent2, INFINITE );
		for (i=0; i<5; i++) a[i] = num;
		num++;
		SetEvent( hEvent1 );
	}
}

int main( void )
{ 
	hEvent1=CreateEvent( NULL, FALSE, TRUE, NULL );
	hEvent2=CreateEvent( NULL, FALSE, FALSE, NULL );
	hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID);
	while(1)
	{
		WaitForSingleObject( hEvent1, INFINITE );
		printf("%d %d %d %d %dn", a[0], a[1], a[2], a[3], a[4]);
		SetEvent( hEvent2 );
	}
	return 0;
}

Семафоры

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

Функция CreateSemaphore создает объект-семафор с указанием и максимально возможного начального его значения, OpenSemaphore – возвращает дескриптор существующего семафора, захват семафора производится с помощью ожидающих функций, при этом значение семафора уменьшается на единицу, ReleaseSemaphore — освобождение семафора с увеличением значения семафора на указанное в параметре число.

Пример. Синхронизация нитей с помощью семафоров.

#include <windows.h>
#include <stdio.h>
HANDLE hSem;
int a[5];
HANDLE hThr;
unsigned long uThrID;

void Thread( void* pParams )
{ 
	int i, num = 0;
	while (1)
	{ 
		WaitForSingleObject( hSem, INFINITE );
		for (i=0; i<5; i++) a[i] = num;
		num++;
		ReleaseSemaphore( hSem, 1, NULL );
	}
}

int main( void )
{ 
	hSem=CreateSemaphore( NULL, 1, 1, "MySemaphore1" );
	hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID);
	while(1)
	{
		WaitForSingleObject( hSem, INFINITE );
		printf("%d %d %d %d %dn", a[0], a[1], a[2], a[3], a[4]);
		ReleaseSemaphore( hSem, 1, NULL );
	}
return 0;
}

Защищенный доступ к переменным

Существует ряд функций, позволяющих работать с глобальными переменными из всех нитей, не заботясь о синхронизации, т.к. эти функции сами за ней следят – их выполнение атомарно. Это функции InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd и InterlockedCompareExchange. Например, функция InterlockedIncrement атомарно увеличивает значение 32-битной переменной на единицу, что удобно использовать для различных счетчиков.

Для получения полной информации о назначении, использовании и синтаксисе всех функций WIN32 API необходимо воспользоваться системой помощи MS SDK, входящей в состав сред программирования Borland Delphi или CBuilder, а также MSDN, поставляемым в составе системы программирования Visual C.

Функции синхронизации

   Функции, ожидающие единственный объект

   Функции, ожидающие несколько объектов

   Прерывание ожидания по запросу на завершение операции ввода-вывода
или APC

Объекты синхронизации

   Event (событие)

   Mutex (Mutually Exclusive)

   Semaphore (семафор)

   Waitable timer (таймер ожидания)

   Дополнительные объекты синхронизации

      Сообщение об изменении
папки (change notification)

      Устройство стандартного
ввода с консоли (console input)

      Задание (Job)

      Процесс (Process)

      Поток (thread)

Дополнительные механизмы синхронизации

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

   Защищенный доступ к переменным (Interlocked Variable Access)

Резюме

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

Главной идеей, заложенной в основе
синхронизации потоков в Win32,
является использование объектов
синхронизации и функций ожидания. Объекты
могут находиться в одном из двух состояний —
Signaled
или Not
Signaled. Функции
ожидания блокируют выполнение потока до
тех пор, пока заданный объект находится в
состоянии Not Signaled. Таким
образом, поток, которому необходим
эксклюзивный доступ к ресурсу, должен
выставить какой-либо объект синхронизации
в несигнальное состояние, а по окончании —
сбросить его в сигнальное. Остальные
потоки должны перед доступом к этому
ресурсу вызвать функцию ожидания, которая
позволит им дождаться освобождения ресурса.

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

Функции синхронизации

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

Функции, ожидающие единственный объект

Простейшей функцией ожидания является
функция WaitForSingleObject:

function WaitForSingleObject(
     hHandle: THandle;       // идентификатор    объекта
     dwMilliseconds: DWORD   // период ожидания
   ): DWORD; stdcall; 

Функция ожидает перехода объекта hHandle в сигнальное состояние в течение dwMilliseconds
миллисекунд. Если в качестве параметра dwMilliseconds передать значение INFINITE,
функция будет ждать в течение неограниченного времени. Если dwMilliseconds равен
0, то функция проверяет состояние объекта и немедленно возвращает управление.

Функция
возвращает одно из следующих значений:

WAIT_ABANDONED 
Поток, владевший объектом, завершился, не переведя объект в сигнальное
состояние
WAIT_OBJECT_0 
Объект перешел в сигнальное состояние
WAIT_TIMEOUT 
Истек срок ожидания. Обычно в этом случае генерируется ошибка либо функция
вызывается в цикле до получения другого результата
WAIT_FAILED 
Произошла ошибка (например, получено неверное значение hHandle). Более
подробную информацию можно получить, вызвав GetLastError

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

var
     Reason: DWORD;
     ErrorCode: DWORD;
   
   Action1.Enabled := FALSE;
   try
     repeat
       Application.ProcessMessages;
       Reason := WailForSingleObject(ObjectHandle, 10);
       if Reason = WAIT_FAILED then begin
         ErrorCode := GetLastError;
         raise Exception.CreateFmt(
           ‘Wait for object failed with error:    %d’, [ErrorCode]);
       end;
     until Reason <> WAIT_TIMEOUT;
   finally
     Actionl.Enabled := TRUE;
   end; 

В случае когда одновременно с ожиданием объекта требуется перевести в сигнальное
состояние другой объект, может использоваться функция SignalObjectAndWait:

function SignalObjectAndWait(
     hObjectToSignal: THandle;  // объект, который будет переведен в
                                   // сигнальное состояние
     hObjectToWaitOn: THandle;  // объект, который     ожидает    функция
     dwMilliseconds: DWORD;     // период ожидания
     bAlertable: BOOL              // задает, должна ли функция возвращать
                                //    управление в случае запроса на
                                   // завершение операции ввода-вывода
   ): DWORD; stdcall; 

Возвращаемые значения аналогичны функции WaitForSingleObject.

! В модуле Windows.pas эта функция ошибочно объявлена как возвращающая
значение BOOL. Если вы намерены ее использовать – объявите ее корректно
или используйте приведение типа возвращаемого значения к DWORD.

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

Функции, ожидающие несколько объектов

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

type
     TWOHandleArray = array[0..MAXIMUM_WAIT_OBJECTS - 1] of THandle;
     PWOHandleArray = ^TWOHandleArray;
   
   function WaitForMultipleObjects(
     nCount: DWORD;                 // Задает количество объектов
     lpHandles: PWOHandleArray;  // Адрес массива объектов
     bWaitAll: BOOL;                // Задает, требуется ли ожидание всех
                                    // объектов или любого
     dwMilliseconds: DWORD       // Период ожидания
   ): DWORD; stdcall; 

Функция возвращает одно из следующих значений:

Число в диапазоне от

       WAIT_OBJECT_0 до
       WAIT_OBJECT_0 + nCount – 1              
Если bWaitAll равно TRUE, то это число означает, что все объекты перешли
в сигнальное состояние. Если FALSE — то, вычтя из возвращенного значения
WAIT_OBJECT_0, мы получим индекс объекта в массиве lpHandles

Число в диапазоне от

       WAIT_ABANDONED_0 до
       WAIT_ABANDONED_0 + nCount – 1              
Если bWaitAll равно TRUE,  это означает, что все объекты перешли
в сигнальное состояние, но хотя бы один из владевших ими потоков завершился,
не сделав объект сигнальным Если FALSE — то, вычтя из возвращенного значения
WAIT_ABANDONED_0,  мы получим в массиве lpHandles индекс объекта,
при этом поток, владевший этим объектом,
завершился,  не сделав его сигнальным
WAIT_TIMEOUT 
Истек период ожидания
WAIT_FAILED 
Произошла ошибка

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

var
     Handles: array[0..1] of THandle;
     Reason: DWORD;
     RestIndex: Integer;
   
   ...
   
   Handles[0] := OpenMutex(SYNCHRONIZE, FALSE, ‘FirstResource’);
   Handles[1] := OpenMutex(SYNCHRONIZE, FALSE, ‘SecondResource’);
   // Ждем первый из объектов
   Reason := WaitForMultipleObjects(2, @Handles, FALSE, INFINITE);
   case Reason of
     WAIT_FAILED: RaiseLastWin32Error;
     WAIT_OBJECT_0, WAIT_ABANDONED_0:
       begin
         ModifyFirstResource;
         RestIndex := 1;
       end;
     WAIT_OBJECT_0 + 1, WAIT_ABANDONED_0 + 1:
       begin
         ModifySecondResource;
         RestIndex := 0;
       end;
     // WAIT_TIMEOUT возникнуть не может
   end;
   // Теперь ожидаем освобождения следующего объекта
   if WailForSingleObject(Handles[RestIndex],
        INFINITE) = WAIT_FAILED then
          RaiseLastWin32Error;
   // Дождались, модифицируем оставшийся ресурс
   if RestIndex = 0 then
     ModifyFirstResource
   else
    ModifySecondResource; 

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

function MsgWaitForMultipleObjects(
   nCount: DWORD;     // количество объектов синхронизации
   var pHandles;      // адрес массива объектов
   fWaitAll: BOOL;    // Задает, требуется ли ожидание всех
                      // объектов или любого
   dwMilliseconds,    // Период ожидания
   dwWakeMask: DWORD  // Тип события, прерывающего ожидание
 ): DWORD; stdcall;  

Главное отличие этой функции от
предыдущей — параметр dwWakeMask,
который является комбинацией битовых
флагов QS_XXX
и задает типы сообщений, прерывающих
ожидание функции независимо от состояния
ожидаемых объектов. Например, маска QS_KEY
позволяет прервать ожидание при появлении
в очереди сообщений WM_KEYUP,
WM_KEYDOWN,
WM_SYSKEYUP
или WM_SYSKEYDOWN,
а маска QS_PAINT
— сообщения WM_PAINT.
Полный список значений, допустимых для dwWakeMask,
имеется в документации по Windows
SDK. При
появлении в очереди потока, вызвавшего
функцию, сообщений, соответствующих
заданной маске, функция возвращает
значение WAIT_OBJECT_0
+ nCount.
Получив это значение, ваша программа может
обработать его и снова вызвать функцию
ожидания. Рассмотрим пример с запуском
внешнего приложения (необходимо, чтобы на время его работы
вызывающая программа не реагировала на
ввод пользователя, однако ее окно должно
продолжать перерисовываться):

procedure TForm1.Button1Click(Sender: TObject);
 var
   PI: TProcessInformation;
   SI: TStartupInfo;
   Reason: DWORD;
   Msg: TMsg;
 begin
   // Инициализируем структуру TStartupInfo
   FillChar(SI, SizeOf(SI), 0);
   SI.cb := SizeOf(SI);
   // Запускаем внешнюю программу
   Win32Check(CreateProcess(NIL, 'COMMAND.COM', NIL,
     NIL, FALSE, 0, NIL, NIL, SI, PI));
 //**************************************************
 // Попробуйте заменить нижеприведенный код на строку
 // WaitForSingleObject(PI.hProcess, INFINITE);
 // и посмотреть, как будет реагировать программа на
 // перемещение других окон над ее окном
 //**************************************************
   repeat
     // Ожидаем завершения дочернего процесса или сообщения
     // перерисовки WM_PAINT
     Reason := MsgWaitForMultipleObjects(1, PI.hProcess, FALSE,
       INFINITE, QS_PAINT);
     if Reason = WAIT_OBJECT_0 + 1 then begin
       // В очереди сообщений появился WM_PAINT – Windows
       // требует обновить окно программы.
       // Удаляем сообщение из очереди
       PeekMessage(Msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE);
       // И перерисовываем наше окно
       Update;
     end;
     // Повторяем цикл, пока не завершится дочерний процесс
   until Reason = WAIT_OBJECT_0;
   // Удаляем из очереди накопившиеся там сообщения
   while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do;
   CloseHandle(PI.hProcess);
   CloseHandle(PI.hThread)
 end;  

Если в потоке, вызывающем функции
ожидания, явно (функцией CreateWindow)
или неявно (используя TForm, DDE, COM)
создаются окна Windows
— поток должен
обрабатывать сообщения. Поскольку
широковещательные сообщения посылаются
всем окнам в системе, то поток, не
обрабатывающий сообщения, может вызвать
взаимоблокировку (система ждет, когда поток
обработает сообщение, поток — когда
система или другие потоки освободят объект)
и привести к зависанию Windows.
Если в вашей программе имеются подобные
фрагменты, необходимо использовать MsgWaitForMultipleObjects
или MsgWaitForMultipleObjectsEx
и позволять прервать ожидание для
обработки сообщений. Алгоритм
аналогичен вышеприведенному примеру.

Прерывание ожидания по запросу на завершение операции ввода-вывода
или APC

Windows
поддерживает асинхронные вызовы процедур.
При создании каждого потока (thread) с ним
ассоциируется очередь асинхронных вызовов
процедур (APC queue). Операционная
система (или приложение пользователя — при
помощи функции QueueUserAPC)
может помещать в нее запросы на выполнение
функций в контексте данного потока. Эти
функции не могут быть выполнены немедленно,
поскольку поток может быть занят. Поэтому
операционная система вызывает их, когда
поток вызывает одну из следующих функций
ожидания:

function SleepEx(
   dwMilliseconds: DWORD;   // Период ожидания
   bAlertable: BOOL         // задает, должна ли функция возвращать
                            // управление в случае запроса на
                            // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function WaitForSingleObjectEx(
   hHandle: THandle;      // Идентификатор объекта
   dwMilliseconds: DWORD; // Период ожидания
   bAlertable: BOOL       // задает, должна ли функция возвращать
                          // управление в случае запроса на
                          // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function WaitForMultipleObjectsEx(
   nCount: DWORD;            // количество объектов
   lpHandles: PWOHandleArray;// адрес массива идентификаторов объектов
   bWaitAll: BOOL;           // Задает, требуется ли ожидание всех
                             // объектов или любого
   dwMilliseconds: DWORD;    // Период ожидания
   bAlertable: BOOL          // задает, должна ли функция возвращать
                             // управление в случае запроса на
                             // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function SignalObjectAndWait(
   hObjectToSignal: THandle;  // объект, который будет переведен в
                              // сигнальное состояние
   hObjectToWaitOn: THandle;  // объект, которого ожидает функция
   dwMilliseconds: DWORD;     // период ожидания
   bAlertable: BOOL           // задает, должна ли функция возвращать
                              // управление в случае запроса на
                              // асинхронный вызов процедуры
 ): DWORD; stdcall;  
   
function MsgWaitForMultipleObjectsEx(
 nCount: DWORD;     // количество объектов синхронизации
   var pHandles;      // адрес массива объектов
   fWaitAll: BOOL;    // Задает, требуется ли ожидание всех
                      // объектов или любого
   dwMilliseconds,    // Период ожидания
   dwWakeMask: DWORD  // Тип события, прерывающего ожидание
   dwFlags: DWORD     // Дополнительные флаги
 ): DWORD; stdcall;  

Если параметр bAlertable
равен TRUE
(либо если dwFlags
в функции MsgWaitForMultipleObjectsEx
содержит MWMO_ALERTABLE),
то при появлении в очереди APC
запроса на
асинхронный вызов процедуры операционная
система выполняет вызовы всех имеющихся в
очереди процедур, после чего функция
возвращает значение WAIT_IO_COMPLETION.

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

Объекты синхронизации

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

Event (событие)

Event
позволяет
известить один или несколько ожидающих
потоков о наступлении события. Event
бывает:

Отключаемый
вручную

Будучи
установленным в сигнальное состояние,
остается в нем до тех пор, пока не будет
переключен явным вызовом функции ResetEvent

Автоматически
отключаемый

Автоматически
переключается в несигнальное состояние
операционной системой, когда один из
ожидающих его потоков завершается

Для создания объекта используется функция CreateEvent:

function CreateEvent(
   lpEventAttributes: PSecurityAttributes;  // Адрес структуры
                                            // TSecurityAttributes
    bManualReset,         // Задает, будет Event переключаемым
                          // вручную (TRUE) или автоматически (FALSE)
    bInitialState: BOOL;  // Задает начальное состояние. Если TRUE -
                          // объект в сигнальном состоянии
    lpName: PChar         // Имя или NIL, если имя не требуется
 ): THandle; stdcall;     // Возвращает идентификатор созданного
                          // объекта  
Структура TSecurityAttributes описана, как:  
TSecurityAttributes = record
   nLength: DWORD;                // Размер структуры, должен
                                  // инициализироваться как
                                  // SizeOf(TSecurityAttributes)
   lpSecurityDescriptor: Pointer; // Адрес дескриптора защиты. В
                                  // Windows 95 и 98 игнорируется
                                  // Обычно можно указывать NIL
   bInheritHandle: BOOL;          // Задает, могут ли дочерние
                                  // процессы наследовать объект
 end;  

Если не требуется задание особых прав
доступа под Windows
NT или
возможности наследования объекта
дочерними процессами, в качестве параметра lpEventAttributes
можно передавать NIL.
В этом случае объект не может
наследоваться дочерними процессами и ему
задается дескриптор защиты «по умолчанию».

Параметр
lpName позволяет разделять объекты между
процессами. Если lpName совпадает с именем уже существующего
объекта типа Event,
созданного текущим или любым другим
процессом, то функция не
создает нового объекта, а возвращает
идентификатор уже существующего. При этом
игнорируются параметры bManualReset, bInitialState и lpSecurityDescriptor.
Проверить, был ли объект создан или
используется уже существующий, можно
следующим образом:

hEvent := CreateEvent(NIL, TRUE, FALSE, ‘EventName’);
 if hEvent = 0 then
   RaiseLastWin32Error;
 if GetLastError = ERROR_ALREADY_EXISTS then begin
   // Используем ранее созданный объект
 end;  

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

Имя
объекта не должно совпадать с именем любого
из существующих объектов типов Semaphore, Mutex, Job,
Waitable Timer или FileMapping. В случае совпадения имен функция
возвращает ошибку.

Если известно, что Event
уже создан, для получения доступа к нему
можно вместо CreateEvent воспользоваться функцией OpenEvent:

function OpenEvent(
   dwDesiredAccess: DWORD;  // Задает права доступа к объекту
   bInheritHandle: BOOL;    // Задает, может ли объект наследоваться
                            // дочерними процессами
   lpName: PChar            // Имя объекта
 ): THandle; stdcall;  

Функция возвращает идентификатор
объекта либо 0 — в случае ошибки. Параметр
dwDesiredAccess может принимать одно из следующих
значений:

EVENT_ALL_ACCESS              

Приложение
получает полный доступ к объекту

EVENT_MODIFY_STATE              

Приложение
может изменять состояние объекта
функциями SetEvent и ResetEvent

SYNCHRONIZE              

Только для Windows
NT
— приложение может использовать объект
только в функциях ожидания

После получения идентификатора можно
приступать к его использованию. Для
этого имеются следующие функции:

function SetEvent(hEvent: THandle): BOOL; stdcall;  

— устанавливает объект в сигнальное состояние

function ResetEvent(hEvent: THandle): BOOL; stdcall;  

— сбрасывает объект, устанавливая его в несигнальное состояние

function PulseEvent(hEvent: THandle): BOOL; stdcall  

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

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

var
   Events: array[0..1] of THandle;  // Массив объектов синхронизации
   Overlapped: array[0..1] of TOverlapped;
 
 ...
 
 // Создаем объекты синхронизации
 Events[0] := CreateEvent(NIL, TRUE, FALSE, NIL);
 Events[1] := CreateEvent(NIL, TRUE, FALSE, NIL);
 
 // Инициализируем структуры TOverlapped
 FillChar(Overlapped, SizeOf(Overlapped), 0);
 Overlapped[0].hEvent := Events[0];
 Overlapped[1].hEvent := Events[1];
 
 // Начинаем асинхронную запись в файлы
 WriteFile(hFirstFile, FirstBuffer, SizeOf(FirstBuffer),
   FirstFileWritten, @Overlapped[0]);
 WriteFile(hSecondFile, SecondBuffer, SizeOf(SecondBuffer),
   SecondFileWritten, @Overlapped[1]);
 
 // Ожидаем завершения записи в оба файла
 WaitForMultipleObjects(2, @Events, TRUE, INFINITE);
 
 // Уничтожаем объекты синхронизации
 CloseHandle(Events[0]);
 CloseHandle(Events[1]) 

По завершении работы с объектом он должен
быть уничтожен функцией CloseHandle.

Delphi
предоставляет класс TEvent,
инкапсулирующий функциональность объекта Event.
Класс расположен в модуле
SyncObjs.pas и объявлен следующим образом:

type
   TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError);
 
   TEvent = class(THandleObject)
   public
     constructor Create(EventAttributes: PSecurityAttributes;
       ManualReset, InitialState: Boolean; const Name: string);
     function WaitFor(Timeout: DWORD): TWaitResult;
     procedure SetEvent;
     procedure ResetEvent;
   end;  

Назначение методов очевидно следует из
их названий. Использование
этого класса позволяет не вдаваться в
тонкости реализации вызываемых функций
Windows API. Для простейших случаев объявлен еще один
класс с упрощенным конструктором:

type
   TSimpleEvent = class(TEvent)
   public
     constructor Create;
   end;
 
 …
 
 constructor TSimpleEvent.Create;
 begin
   FHandle := CreateEvent(nil, True, False, nil);
 end;  

Mutex (Mutually Exclusive)

Мьютекс — это объект синхронизации,
который находится в сигнальном состоянии
только тогда, когда не принадлежит ни
одному из процессов. Как только хотя бы один
процесс запрашивает владение мьютексом, он
переходит в несигнальное состояние и
остается таким до тех пор, пока не будет
освобожден владельцем. Такое поведение
позволяет использовать мьютексы для
синхронизации совместного доступа
нескольких процессов к разделяемому
ресурсу. Для создания мьютекса
используется функция:

function CreateMutex(
   lpMutexAttributes: PSecurityAttributes;  // Адрес структуры
                                            // TSecurityAttributes
   bInitialOwner: BOOL;  // Задает, будет ли процесс владеть
                         // мьютексом сразу после создания
   lpName: PChar         // Имя мьютекса
 ): THandle; stdcall;  

Функция возвращает идентификатор
созданного объекта либо 0. Если мьютекс с
заданным именем уже был создан,
возвращается его идентификатор. В
этом случае функция GetLastError вернет код
ошибки ERROR_ALREDY_EXISTS. Имя
не должно совпадать с именем уже
существующего объекта типов Semaphore,
Event, Job,
Waitable
Timer или
FileMapping.

Если неизвестно, существует ли уже
мьютекс с таким именем, программа не должна
запрашивать владение объектом при создании
(то есть должна передать в качестве bInitialOwner значение FALSE).

Если мьютекс уже существует, приложение
может получить его идентификатор функцией OpenMutex:

function OpenMutex(
   dwDesiredAccess: DWORD;  // Задает права доступа к объекту
   bInheritHandle: BOOL;    // Задает, может ли объект наследоваться
                            // дочерними процессами
   lpName: PChar            // Имя объекта
 ): THandle; stdcall;  

Параметр dwDesiredAccess
может принимать одно из следующих значений:

MUTEX_ALL_ACCESS 
Приложение получает полный доступ к объекту
SYNCHRONIZE
Только для Windows NT — приложение может использовать объект только в
функциях ожидания и функции ReleaseMutex

Функция возвращает идентификатор
открытого мьютекса либо 0 — в случае ошибки.
Мьютекс переходит в сигнальное состояние
после срабатывания функции ожидания, в
которую был передан его идентификатор. Для
возврата в несигнальное состояние служит
функция ReleaseMutex:

function ReleaseMutex(hMutex: THandle): BOOL; stdcall;  

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

var
   Mutex: THandle;
 
 // При инициализации программы
 Mutex := CreateMutex(NIL, FALSE, ‘UniqueMutexName’);
 if Mutex = 0 then
   RaiseLastWin32Error;
 
 ...
 // Доступ к ресурсу
 WaitForSingleObject(Mutex, INFINITE);
 try
   // Доступ к ресурсу, захват мьютекса гарантирует,
   // что остальные процессы, пытающиеся получить доступ,
   // будут остановлены на функции WaitForSingleObject
   ...
 finally
   // Работа с ресурсом окончена, освобождаем его
   // для остальных процессов
   ReleaseMutex(Mutex);
 end;
 
 ...
 // При завершении программы
 CloseHandle(Mutex);  

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

Разумеется,
если работа с ресурсом может потребовать
значительного времени, то необходимо либо
использовать функцию MsgWaitForSingleObject, либо
вызывать WaitForSingleObject в цикле с нулевым
периодом ожидания, проверяя код возврата. В
противном случае ваше
приложение окажется замороженным. Всегда
защищайте захват-освобождение объекта
синхронизации при помощи блока try … finally,
иначе ошибка во время работы с ресурсом
приведет к блокированию работы всех
процессов, ожидающих его освобождения.

Semaphore (семафор)

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

Для создания семафора служит функция CreateSemaphore:

function CreateSemaphore(
   lpSemaphoreAttributes: PSecurityAttributes; // Адрес структуры
                                               // TSecurityAttributes
   lInitialCount,           // Начальное значение счетчика
   lMaximumCount: Longint;  // Максимальное значение счетчика
   lpName: PChar            // Имя объекта
 ): THandle; stdcall;  

Функция возвращает идентификатор
созданного семафора либо 0, если создать
объект не удалось.

Параметр
lMaximumCount задает максимальное значение
счетчика семафора, lInitialCount задает начальное
значение счетчика и должен быть в диапазоне
от 0 до lMaximumCount. lpName задает имя семафора. Если
в системе уже есть семафор с таким именем,
то новый не создается, а возвращается
идентификатор существующего семафора. В
случае если семафор используется внутри
одного процесса, можно создать его без
имени, передав в качестве lpName значение NIL. Имя
семафора не должно совпадать с именем уже
существующего объекта типов event,
mutex, waitable
timer, job
или file-mapping.

Идентификатор ранее созданного семафора
может быть также получен функцией OpenSemaphore:

function OpenSemaphore(
     dwDesiredAccess: DWORD;  // Задает права доступа к объекту
     bInheritHandle: BOOL;    // Задает, может ли объект наследоваться
                                 // дочерними процессами
     lpName: PChar            //    Имя объекта
   ): THandle; stdcall; 

Параметр dwDesiredAccess
может принимать одно из следующих значений:

SEMAPHORE_ALL_ACCESS              

Поток
получает все права на семафор

SEMAPHORE_MODIFY_STATE              

Поток
может увеличивать счетчик семафора
функцией ReleaseSemaphore

SYNCHRONIZE              

Только для Windows
NT
— поток может использовать семафор в
функциях ожидания

Для увеличения счетчика семафора
используется функция ReleaseSemaphore:

function ReleaseSemaphore(
   hSemaphore: THandle;      // Идентификатор семафора
   lReleaseCount: Longint;   // Счетчик будет увеличен на эту величину
   lpPreviousCount: Pointer  // Адрес 32-битной переменной,
                             // принимающей предыдущее значение
                             // счетчика
 ): BOOL; stdcall;  

Если значение счетчика после выполнения
функции превысит заданный для него
функцией CreateSemaphore максимум, то ReleaseSemaphore
возвращает FALSE
и значение семафора не изменяется. В
качестве параметра lpPreviousCount можно передать
NIL, если это значение нам не нужно.

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

unit LimitedThread;
 
 interface
 
 uses Classes;
 
 type
   TLimitedThread = class(TThread)
     procedure Execute; override;
   end;
  
 implementation
 
 uses Windows;
 
 const
   MAX_THREAD_COUNT = 10;
 
 var
   Semaphore: THandle;
 
 procedure TLimitedThread.Execute;
 begin
   // Уменьшаем счетчик семафора. Если к этому моменту уже запущено
   // MAX_THREAD_COUNT потоков — счетчик равен 0 и семафор в
   // несигнальном состоянии. Поток будет заморожен до завершения
   // одного из запущенных ранее.
   WaitForSingleObject(Semaphore, INFINITE);
  
   // Здесь располагается код, отвечающий за функциональность потока,
   // например загрузка файла
   ...
 
   // Поток завершил работу, увеличиваем счетчик семафора и позволяем
   // начать обработку другим потокам.
   ReleaseSemaphore(Semaphore, 1, NIL);
 end;
 
 initialization
   // Создаем семафор при старте программы
   Semaphore := CreateSemaphore(NIL, MAX_THREAD_COUNT,
     MAX_THREAD_COUNT, NIL);
 
 finalization
   // Уничтожаем семафор по завершении программы
   CloseHandle(Semaphore);
 end;  

Вопрос№26 5.1. Блокировка потоков

Примитив синхронизации – это программное
или аппаратное средство (механизм,
инструмент) высокого уровня для решения
задач синхронизации потоков. Примитивы
синхронизации обычно реализованы как
объекты ядра ОС.

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

Спецификация очереди блокированных
потков

class ThreadQueue

{

Thread* tp; //список потоков

public:

ThreadQueue():tp(NULL){}

~ThreadQueue(){…}//здесь нетривиально

void unqueueThread(Thread&t);

bool dequeueThread();

};

void ThreadQueue::enqueueThread(Thread&t)

{

includeThreadToList(t);

t.suspendThread();

}

bool ThreadQueue::dequeueThread()

{

Thread t;

if (!tp)

return false;

else

{

t = excludeThreadFromQueue();

t.resumeThread();

return true;

}

}

Вопрос№27
5.2.
Примитив
синхронизации
Lock (
замок)

Схема реализации примитива
синхронизации Lock

class Lock

{

bool free;

ThreadQueue tq; //очередь потоков

public:

Lock():free(true){}

~Lock(){…}

void acquire();
//закрываем замок

void release();
//открываем замок

};

void Lock::acquire() //закрываем замок

{

disableInterrupt();

if (!free)

tq.enqueueThread(currentThread());

else

free = false;

enableInterrupt();

}

void Lock::release() //открываем замок

{

disableInterrupt();

if (!tq.dequeueThread())

free = true;

enableInterrupt();

}

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

Lock lock;

void thread_1()

{

lock.acquire():

if (n%2==0)

n=a;

else

n=b;

lock.release();

}

void thread_2()

{

lock.acquire();

++n;

lock.release();

}

В ОС
Windows аналогами примитива
синхронизации Lock являются
примитивы синхронизации
CRITICAL_SECTION и Mutex.

Вопрос№28 5.3. Примитив синхронизации
Condition (условие)

Схема реализации примитива синхронизации
Condition

class Condition

{

bool event;

ThreadQueue tq; //очередь потоков

public:

Condition():event(false){}

~Condition(){…}

void wait();
//ждем выполнения условия

void signal();
//сигнализируем о выполнении условия

};

//ждем выполнения условия

void Condition::wait()

{

disableInterrupt();

if (event)

event = false;

else

tq.enqueueThread(currentThread());

enableInterrupt();

}

//сигнализируем о выполнении условия

void signal()

{

disableInterrupt();

if (!tq.dequeueThread())

event = true;

enableInterrupt();

}

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

Condition c;
//начальное состояние несигнальное

void thread_1()

{

c.wait();

if (n%2==0)

n=a;

else

n=b;

}

void thread_2()

{

n++;

c.signal();

}

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

Condition c;
//начальное состояние несигнальное

с.signal(); //устанавливаем в сигнальное
состояние

void thread_1()

{

c.wait();

if (n%2==0)

n=a;

else

n=b;

}

void thread_2()

{

с.wait();

n++;

c.signal();

}

В OС Windows аналогом примитива синхронизации
Сondition является примитив синхронизации
Event.

Вопрос№29 5.4. Семафоры Дейкстры

Cемафор – это неотрицательная
целочисленная переменная, значение
которой может изменяться только при
помощи атомарных операций.

Семафор считается свободным, если его
значение больше нуля, в противном случае
семафор считаетсязанятым.

Пусть s – семафор, тогда над ним можно
определить следующие атомарные операции:

P(s) //захватить
семафор

{

если s>0

то s = s-1; //поток продолжает работу

иначе

ждать освобождения s; //поток переходит
в состояние ожидания

}

V(s) //освободить
семафор

{

если потоки ждут освобождения s

то освободить один поток

иначе s = s+1;

}

Cемафор с операциями P
и V называется семафором
Дейкстры, голландского математика,
который первым использовал семафоры
для решения задач синхронизации.

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

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

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

Семафор, который может принимать только
значения 0 или 1, называется двоичным
или бинарным семафором.

Семафор, значение которого может быть
больше 1, обычно называют считающим
семофором.

Решение проблемы взаимного исключения
с помощью семафора

Semaphor s=1; //семафор свободен

void thread_1()

{

P(s);

if (n%2==0)

n=a;

else

n=b;

V(s);

}

void thread_2()

{

P(s);

n++;

V(s);

}

Решение проблемы условной синхронизации
с помощью семафора

Semaphor s=0; //семафор свободен

void thread_1()

{

P(s);

if (n%2==0)

n=a;

else

n=b;

}

void thread_2()

{

n++;

V(s);

}

Вопрос№30 5.5. Примитив синхронизации
Semaphore (семафор)

Схема реализации примитива синхронизации
семафор

class Semaphore

{

int count;
//счетчик

ThreadQueue tq;
// очередь потоков

public:

Semaphore(int&n):count(n){}

~Semaphore(){…}

void wait(); //закрыть семафор

void signal(); //открыть семафор

}

void Semaphore::wait() //закрыть семафор

{

disableInterrupt();

if (count>0)

—count;

else

tq.enqueueThread(currentThread());

enableInterrupt();

}

void Semaphore::signal() //открыть семафор

{

disableInterrupt();

if (!tq.dequeueThread())

++count;

enableInterrupt();

}

Вопрос№31 5.6. Объекты синхронизации
и функции ожидания в Windows

Объекты синхронизации

В ОС Windows объектами синхронизации
называются объекты ядра, которые могут
находиться в одном из двух состояний:

— сигнальном (signaled);

— несигнальном (nonsignaled).

Объекты синхронизации могут быть
разбиты на 3 класса:

— собственно объекты синхронизации,
которые служат только для решения
проблемы синхронизации параллельных
потоков. К таким объектам синхронизации
в Windows относятся:

— мьютекс (mutex);

— событие (event);

— семафор (semaphore);

— ожидающий таймер (waitable
timer);

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

Функции ожидания

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

— если объект синхронизации находится
в несигнальном состоянии, то поток,
вызвавший функцию ожидания, блокируется
до перехода этого объкта синхронизации
в сигнальное состояние;

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

Функции ожидания в Windows:

— WaitForSingleObject- ждет перехода
в сигнальное состояние одного объекта
синхронизации;

— WaitForMultipleObjects – ждет перехода в сигнальное
состояние одного или нескольких объектов
из массива объектов синхронизации

Вопрос№32 5.7. Критические секции в
Windows

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

Для работы с объектами типа CRITICAL_SECTION
используются следующие функции:

— InitializeCriticalSection – инициализация объекта;

— EnterCriticalSection – вход в критическую
секцию;

— TryEnterCriticalSection – попытка входа в
критическую секцию;

— LeaveCriticalSection – выход из критической
секции;

— DeleteCriticalSection – завершение работы с
объектом

Вопрос№33 5.8. Мьютексы в Windows

Для решения проблемы взаимного исключения
для параллельных потоков, выполняющихся
в контексте разных процессов, в ОС
Windows предназначен объект
ядра мьютекс (mutex).

Мьютекс находится в сигнальном состоянии,
если он не принадлежит ни одному потоку.
В противном случае мьютекс находится
в несигнальном состоянии. Одновременно
мьютекс может принадлежать только
одному потоку.

Для работы с мьютеками используются
следующие функции:

— CreateMutex – создание
мьютекса;

— OpenMutex – получение доступа
к существующему мьютексу;

— ReleaseMutex – освобождение мьютекса (переход
мьютекса в сигнальное состояние);

— WaitForSingleObject или

— WaitForMultipleObjects – захват мьютекса (ожидание
сигнального состояния мьютекса).

Вопрос№34 5.9. События в Windows

В ОС Windows события описываются объектами
ядра Event. При этом различают два типа
событий:

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

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

Что лучше: +2 или -2?

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

Для работы с событиями используются
следующие функции:

— CreateEvent – создания события;

— OpenEvent – получение доступа к существующему
событию;

— SetEvent – перевод события в сигнальное
состояние;

— ResetEvent – перевод события в несигнальное
состояние;

— PulseEvent – освобождение нескольких
потоков, ждущих сигнального состояния
события с ручным сбросом;

— WaitForSingleObject или WaitForMultipleObjects – ожидание
наступления события (перехода события
в сигнальное состояние)

Вопрос№35 5.10. Семафоры в Windows

Семафоры в ОС Windows
описываются объектами ядра Semaphore.
Семафор находится в сигнальном состоянии,
если его значение больше нуля. В противном
случае семафор находится в несигнальном
состоянии.

Для работы с семафорами используются
следующие функции:

— CreateSemaphore – создание семафора;

— OpenSemaphore – получение доступа к
существующему семафору;

— ReleaseSemaphore – увеличение значения семафора
на положительное число;

— WaitForSingleObject или WaitForMultipleObjects – ожидание
перехода семафора в сигнальное состояние.

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

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • Download demo project for CriticalSection Win32 — 122 Kb
  • Download demo project for CriticalSection MFC — 9.17 Kb

Introduction

In my previous article, we discussed simple multithreaded programming in C, Win32 and MFC. Now, we see simple thread synchronization with Win32 API and MFC.

In a multithreaded environment, each thread has its own local thread stack and registers. If multiple threads access the same resource for read and write, the value may not be the correct value. For example, let’s say our application contains two threads, one thread for reading content from the file and another thread writing the content to the file. If the write thread tries to write and the read thread tries to read the same data, the data might become corrupted. In this situation, we want to lock the file access. The thread synchronization has two stages. Signaled and non-signaled.

The signaled state allows objects to access and modify data. The non-signaled state does allow accessing or modifying the data in the thread local stack.

Thread Synchronization methods:

Many of the thread synchronization methods are used to synchronize multiple threads. The following methods are used to synchronize between objects.

Thread Synchronization on different processes:

Event:

Event is a thread synchronization object used to set the signaled or non-signaled state. The signaled state may be manual or automatic depending on the event declaration.

Mutex:

Mutex is the thread synchronization object which allows to access the resource only one thread at a time. Only when a process goes to the signaled state are the other resources allowed to access.

Semaphore:

Semaphore is a thread synchronization object that allows zero to any number of threads access simultaneously.

Thread Synchronization in same process:

Critical Section

The critical section is a thread synchronization object. The other synchronization objects like semaphore, event, and mutex are used to synchronize the resource with different processes. But, the critical section allows synchronization within the same process.

The given difference is the main difference between the thread synchronization objects. The other differences between the thread synchronization are the following:

Win32 Wait Functions:

The Wait family of functions are used to wait the thread synchronization object while a process completes. The widely used functions are WaitForSingleObject and WaitForMultipleObjects functions. The WaitForSingleObject function is used for waiting on a single Thread synchronization object. This is signaled when the object is set to signal or the time out interval is finished. If the time interval is INFINITE, it waits infinitely.

The WaitForMultipleObjects is used to wait for multiple objects signaled. In the Semaphore thread synchronization object, when the counters go to zero the object is non-signaled. The Auto reset event and Mutex is non-signaled when it releases the object. The manual reset event does affect the wait functions’ state.

MFC Lock/Unlock Resource:

The MFC CMutex, CCriticalSection, CSemaphore, and CEvent classes are used to synchronize the threads in Microsoft Foundation Class library.

The CSingleLock and CMultiLock are used to control the access to the resources in multithread programming. The CSingleLock and CMultiLock classes have no base class. CSingleLock is used to lock the single synchronization object at a time. CMultiLock is used to control more than one thread synchronization objects with a particular time interval. The CSingleLock/CMultiLock Lock and Unlock member functions are used to the lock or release the resource.

The CSingleLock and CMultiLock constructors use the CSyncObject object for locking and unlocking the resource. All the Thread Synchronization classes are derived from CSyncObject base class. So, the constructor has any one of the thread synchronization classes derived from CSyncObject. CSingleLock IsLocked member is use to find if the object is locked already or not.

The CMultiLock class is used to control the access to the resources in multiple objects. The CMultiLock constructor has an array of CSyncObject objects and the total number of counts in the thread synchronization classes. The IsLocked member function is used to check the particular synchronization object state.

Thread Synchronization Objects:

Critical Section:

The Critical section object is same as the Mutex object. But, the Mutex object allows synchronizing objects across the process. But the Critical section object does not allow synchronization with different processes. The critical section is used to synchronize the threads within the process boundary.

It is possible to use Mutex instead of critical section. But, the critical section thread synchronization object is slightly faster compared to other synchronization objects. The critical section object synchronizes threads within the process. Critical section allows accessing only one thread at a time.

Win32 Critical Section Object:

The process allocates memory for the critical section using the CRITICAL_SECTION structure. The critical section structure declared in the Winnt.h is as follows:

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
    
    
    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;   
    HANDLE LockSemaphore;
    DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

In critical section, we allocate memory for CRITICAL_SECTION structure and initializes the critical section. The IniailizeCriticalSection and InitializeCriticalSectionAndSpinCount are used to initialize the critical section. If we initialize the critical section, then only, we use any one of the EnterCriticalsection, TryEnterCriticalSection, or LeaveCriticalSection functions. The EnterCriticalsection function is used to enter the critical section, and TryEnterCriticalSection to enter the critical section without blocking. LeaveCricalSection is used to leave the critical section.

If any of the other synchronization object names is same as Critical section object, the Critical section object waits for the ownership infinitely. The Critical section object does not allow moving or copying the object. If we have to synchronize the thread on different processes, use Mutex object. DeleteCriticalSection function releases all the critical section objects. After calling the DeleteCriticalSection, it is not possible to call EnterCriticalsection or LeaveCriticalSection.

Example:

CRITICAL_SECTION m_cs;


InitializeCriticalSection(&m_cs);

The two threads try to access the same variable. The global variable g_n tries to access two threads. The global m_cs CRITICAL_SECTION structure is used to synchronize the two threads.

UINT ThreadOne(LPVOID lParam)
{
    
    
    EnterCriticalSection(&m_cs);

    

    
    LeaveCriticalSection(&m_cs);

return 0;
}

Thread two:

UINT ThreadTwo(LPVOID lParam)
{

    EnterCriticalSection(&m_cs); 

    

    
    LeaveCriticalSection(&m_cs);

    
    return 0;
}

MFC Critical Section object:

The CCriticalSection class provides the functionality of critical section synchronization object. The default constructor is used to construct the critical section object. The Lock and Unlock functions are used to control the resource access in the synchronization object.

The CRITICAL_SECTION‘s m_sect data member allows initializing the CRITICAL_SECTION structure. The Lock function overloaded in two forms. The Lock function without any arguments is used to lock the resource. The other form of Lock function needs the number of milliseconds to wait. All the critical section members are declared in afxmt.inl file as inline functions.

Example:

CCriticalSection c_s;
int g_C;


UINT ThreadFunction1(LPVOID lParam)
{

    
    CSingleLock lock(&c_s);

    
    lock.Lock();

    

    
    lock.Unlock();

    
    return 0;
}


UINT ThreadFunction2(LPVOID lParam)
{

    
    CSingleLock lock(&c_s);

    
    lock.Lock();

    

    
    lock.Unlock();

    
    return 0;
}

Event:

Event is the thread synchronization object to set signaled state or non-signaled state. The Event has two types. They are manual reset event and auto reset event.

The manual event has signaled user set to non-signaled state, uses ResetEvent function manually. The auto reset event automatically occurs when the object is raised to the non-signaled state. The event thread synchronization object is used to synchronize the particular event entered in the thread. The Event object is used to set the operating system kernel flag in the thread.

Win32 Event Object:

The CreateEvent function is used to create the event thread synchronization object. The manual or auto reset event choice is mentioned at the CreateEvent function parameter initialization. The Wait family functions (WaitForSingleObject, WaitForMultipleObjects) are use to wait when a particular event occurs. The group of objects waits for the events: the single object signaled or the entire events are signaled in the thread.

CreateEvent function is used to create manual or auto reset events. This function is used to create named and unnamed event objects. SetEvent function is used to set the event object to signal state. The ResetEvent function is used to set the event object to non-signaled state. If the function is successful, it returns the handle of that event. If the named event is already available, the GetLastError function returns the ERROR_ALREADY_EXISTS flag. If the named event is already available, the OpenEvent function is used to access the event previously created by the CreateEvent function.

Example:

HANDLE g_Event; 


g_Event = CreateEvent(  NULL,      TRUE,    TRUE,  "Event"     ); 

ResetEvent(g_Event);



SetEvent(g_Event);

MFC Event Object:

Event is useful for waiting if something happens (or an event occurs). CEvent class is used for event object functionality. The CEvent class is derived from the abstract CSyncObject Class. The CSyncObject is derived from the CObject mother class. The CSingleLock or CMultiLock constructor use one of the CSyncObject derived classes. The CEvent constructor specifies the manual or auto reset event options.

MFC CEvent class provides the constructor with default arguments. If we want to control our needs, we set the Ownership for the event object, manual or auto reset event flag, the name of the event object (if named event), and the security attributes. If the name matches with that of an existing event object, check the type of the object. If that object is an event, the CEvent constructor simply replies the handle of the previous event. If the name exists for any of the other synchronization object, the CEvent constructor gives an error message.

The Manual event object signaled/non-signaled uses SetEvent or ResetEvent function .The Auto Reset event occurs when it releases any one of threads in the event object. We don’t use CSyncObject directly. Because, the CSyncObject is an abstract base class. All the event members are declared in the afxmt.inl file as inline functions.

Mutex Synchronization Object:

Mutex is the synchronization object used to synchronize the threads with more than one process. The Mutex is as the name tells, mutually exclusive. The Mutex object allows accessing the resource single thread at a time.

Win32 Mutex Object:

The CreateMutex function is used to create the Mutex object. In the CreateMutex function, we initialize the named Mutex or unnamed Mutex, and set the ownership to true or false arguments.

If we create two-named Mutex using CreateMutex function within the same process, the second CreateMutex function returns error. If we create two or more Mutex objects on different processes, with the same name, when we call first time, the CreateMutex function creates the Mutex. The other CreateMutex function returns the handle of the previous Mutex object.

The OpenMutex function is used to open an existing Mutex using the supplied Mutex name. The ReleaseMutex is used to release a Mutex object. The threads wait in first in first out order for taking the ownership for the waiting threads. If we try to take the ownership twice, deadlock occurs. We the call ReleaseMutex, and then try to take the ownership.

Example

HANDLE g_Mutex;
DWORD dwWaitResult;


g_Mutex = CreateMutex( NULL, TRUE, "MutexToProtectDatabase");
 

dwWaitResult = WaitForSingleObject( g_Mutex, 5000L);




ReleaseMutex(g_Mutex))

MFC Mutex Object:

The MFC CMutex class is used to control the Mutex objects. The CMutex constructor has three parameters. We can specify the name of the Mutex as a parameter. If this is null, an unnamed Mutex is created. The Security attributes are used to set the security attributes for the Mutex object. If the name already exists for a Mutex object, the constructor simply returns the existing Mutex object. If the name exists for some object, the constructor fails.

To control resource access for single Mutex object, use CSingleLock class. If you wish to control multiple Mutex objects, the CMultiLock is used to control the access to resources in multithreaded programming.

CMutex g_m;
int g_C;

UINT ThreadFunction1(LPVOID lParam)
{

    
    CSingleLock lock(&g_m);

    lock.Lock();

    

    lock.Unlock();

    
    return 0;
}


UINT ThreadFunction2(LPVOID lParam)
{

    
    CSingleLock lock(&g_m);

    lock.Lock();

    

    lock.Unlock();

    
    return 0;
}

Semaphore Thread Synchronization Object:

Semaphore is a thread synchronization object that allows accessing the resource for a count between zero and maximum number of threads. If the Thread enters the semaphore, the count is incremented. If the thread completed the work and is removed from the thread queue, the count is decremented. When the thread count goes to zero, the synchronization object is non-signaled. Otherwise, the thread is signaled.

Win32 Semaphore Synchronization Object:

The CreateSemaphore function is used to create a named or unnamed semaphore thread synchronization object. The initial count and maximum count is mentioned in the CreateSemaphore function. The count is never negative and less then the total count value. The WaitForSingleObject waits for more than one object in semaphore object.

The WaitForMultipleObjects function is non-signaled when all the objects are returned. The OpenSemaphore function is used to open an existing handle to a semaphore object created within the process or another process. The Releasesemaphore function is used to release the semaphore from the Thread synchronization queue. If the CreateSemaphore function has created the same named Thread synchronization object within the process, the CreateSemaphore returns 0. The GetLastError function is used to retrieve the reason for the failure.

Example:

HANDLE g_Semaphore;


g_Semaphore = CreateSemaphore(  NULL,   4, 4,  NULL);  

DWORD dwWaitResult; 

dwWaitResult = WaitForSingleObject(   g_Semaphore, 0L);




ReleaseSemaphore(  g_Semaphore,  1,  NULL) ;

MFC Semaphore Synchronization Object:

The CSemaphore class allows us to create the CSemaphore object. The CSemaphore class is derived from the abstract CSyncObject base class. The CSemaphore class constructor is used to specify the count and maximum number of resource access.

The virtual destructor is used to delete the CSemaphore class object without affecting the CSyncObject base class. The Unlock function is used to unlock the resources.

Conclusion:

Thread Synchronization is used to access the shared resources in a multithread environment. The programmer decides the situation for when to use the synchronization object efficiently. The MFC Thread Synchronization classes internally call the Win32 API functions. The MFC Thread Synchronization classes wrap many of the functionalities form the Windows environment.

  • Download demo project for CriticalSection Win32 — 122 Kb
  • Download demo project for CriticalSection MFC — 9.17 Kb

Introduction

In my previous article, we discussed simple multithreaded programming in C, Win32 and MFC. Now, we see simple thread synchronization with Win32 API and MFC.

In a multithreaded environment, each thread has its own local thread stack and registers. If multiple threads access the same resource for read and write, the value may not be the correct value. For example, let’s say our application contains two threads, one thread for reading content from the file and another thread writing the content to the file. If the write thread tries to write and the read thread tries to read the same data, the data might become corrupted. In this situation, we want to lock the file access. The thread synchronization has two stages. Signaled and non-signaled.

The signaled state allows objects to access and modify data. The non-signaled state does allow accessing or modifying the data in the thread local stack.

Thread Synchronization methods:

Many of the thread synchronization methods are used to synchronize multiple threads. The following methods are used to synchronize between objects.

Thread Synchronization on different processes:

Event:

Event is a thread synchronization object used to set the signaled or non-signaled state. The signaled state may be manual or automatic depending on the event declaration.

Mutex:

Mutex is the thread synchronization object which allows to access the resource only one thread at a time. Only when a process goes to the signaled state are the other resources allowed to access.

Semaphore:

Semaphore is a thread synchronization object that allows zero to any number of threads access simultaneously.

Thread Synchronization in same process:

Critical Section

The critical section is a thread synchronization object. The other synchronization objects like semaphore, event, and mutex are used to synchronize the resource with different processes. But, the critical section allows synchronization within the same process.

The given difference is the main difference between the thread synchronization objects. The other differences between the thread synchronization are the following:

Win32 Wait Functions:

The Wait family of functions are used to wait the thread synchronization object while a process completes. The widely used functions are WaitForSingleObject and WaitForMultipleObjects functions. The WaitForSingleObject function is used for waiting on a single Thread synchronization object. This is signaled when the object is set to signal or the time out interval is finished. If the time interval is INFINITE, it waits infinitely.

The WaitForMultipleObjects is used to wait for multiple objects signaled. In the Semaphore thread synchronization object, when the counters go to zero the object is non-signaled. The Auto reset event and Mutex is non-signaled when it releases the object. The manual reset event does affect the wait functions’ state.

MFC Lock/Unlock Resource:

The MFC CMutex, CCriticalSection, CSemaphore, and CEvent classes are used to synchronize the threads in Microsoft Foundation Class library.

The CSingleLock and CMultiLock are used to control the access to the resources in multithread programming. The CSingleLock and CMultiLock classes have no base class. CSingleLock is used to lock the single synchronization object at a time. CMultiLock is used to control more than one thread synchronization objects with a particular time interval. The CSingleLock/CMultiLock Lock and Unlock member functions are used to the lock or release the resource.

The CSingleLock and CMultiLock constructors use the CSyncObject object for locking and unlocking the resource. All the Thread Synchronization classes are derived from CSyncObject base class. So, the constructor has any one of the thread synchronization classes derived from CSyncObject. CSingleLock IsLocked member is use to find if the object is locked already or not.

The CMultiLock class is used to control the access to the resources in multiple objects. The CMultiLock constructor has an array of CSyncObject objects and the total number of counts in the thread synchronization classes. The IsLocked member function is used to check the particular synchronization object state.

Thread Synchronization Objects:

Critical Section:

The Critical section object is same as the Mutex object. But, the Mutex object allows synchronizing objects across the process. But the Critical section object does not allow synchronization with different processes. The critical section is used to synchronize the threads within the process boundary.

It is possible to use Mutex instead of critical section. But, the critical section thread synchronization object is slightly faster compared to other synchronization objects. The critical section object synchronizes threads within the process. Critical section allows accessing only one thread at a time.

Win32 Critical Section Object:

The process allocates memory for the critical section using the CRITICAL_SECTION structure. The critical section structure declared in the Winnt.h is as follows:

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
    
    
    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;   
    HANDLE LockSemaphore;
    DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

In critical section, we allocate memory for CRITICAL_SECTION structure and initializes the critical section. The IniailizeCriticalSection and InitializeCriticalSectionAndSpinCount are used to initialize the critical section. If we initialize the critical section, then only, we use any one of the EnterCriticalsection, TryEnterCriticalSection, or LeaveCriticalSection functions. The EnterCriticalsection function is used to enter the critical section, and TryEnterCriticalSection to enter the critical section without blocking. LeaveCricalSection is used to leave the critical section.

If any of the other synchronization object names is same as Critical section object, the Critical section object waits for the ownership infinitely. The Critical section object does not allow moving or copying the object. If we have to synchronize the thread on different processes, use Mutex object. DeleteCriticalSection function releases all the critical section objects. After calling the DeleteCriticalSection, it is not possible to call EnterCriticalsection or LeaveCriticalSection.

Example:

CRITICAL_SECTION m_cs;


InitializeCriticalSection(&m_cs);

The two threads try to access the same variable. The global variable g_n tries to access two threads. The global m_cs CRITICAL_SECTION structure is used to synchronize the two threads.

UINT ThreadOne(LPVOID lParam)
{
    
    
    EnterCriticalSection(&m_cs);

    

    
    LeaveCriticalSection(&m_cs);

return 0;
}

Thread two:

UINT ThreadTwo(LPVOID lParam)
{

    EnterCriticalSection(&m_cs); 

    

    
    LeaveCriticalSection(&m_cs);

    
    return 0;
}

MFC Critical Section object:

The CCriticalSection class provides the functionality of critical section synchronization object. The default constructor is used to construct the critical section object. The Lock and Unlock functions are used to control the resource access in the synchronization object.

The CRITICAL_SECTION‘s m_sect data member allows initializing the CRITICAL_SECTION structure. The Lock function overloaded in two forms. The Lock function without any arguments is used to lock the resource. The other form of Lock function needs the number of milliseconds to wait. All the critical section members are declared in afxmt.inl file as inline functions.

Example:

CCriticalSection c_s;
int g_C;


UINT ThreadFunction1(LPVOID lParam)
{

    
    CSingleLock lock(&c_s);

    
    lock.Lock();

    

    
    lock.Unlock();

    
    return 0;
}


UINT ThreadFunction2(LPVOID lParam)
{

    
    CSingleLock lock(&c_s);

    
    lock.Lock();

    

    
    lock.Unlock();

    
    return 0;
}

Event:

Event is the thread synchronization object to set signaled state or non-signaled state. The Event has two types. They are manual reset event and auto reset event.

The manual event has signaled user set to non-signaled state, uses ResetEvent function manually. The auto reset event automatically occurs when the object is raised to the non-signaled state. The event thread synchronization object is used to synchronize the particular event entered in the thread. The Event object is used to set the operating system kernel flag in the thread.

Win32 Event Object:

The CreateEvent function is used to create the event thread synchronization object. The manual or auto reset event choice is mentioned at the CreateEvent function parameter initialization. The Wait family functions (WaitForSingleObject, WaitForMultipleObjects) are use to wait when a particular event occurs. The group of objects waits for the events: the single object signaled or the entire events are signaled in the thread.

CreateEvent function is used to create manual or auto reset events. This function is used to create named and unnamed event objects. SetEvent function is used to set the event object to signal state. The ResetEvent function is used to set the event object to non-signaled state. If the function is successful, it returns the handle of that event. If the named event is already available, the GetLastError function returns the ERROR_ALREADY_EXISTS flag. If the named event is already available, the OpenEvent function is used to access the event previously created by the CreateEvent function.

Example:

HANDLE g_Event; 


g_Event = CreateEvent(  NULL,      TRUE,    TRUE,  "Event"     ); 

ResetEvent(g_Event);



SetEvent(g_Event);

MFC Event Object:

Event is useful for waiting if something happens (or an event occurs). CEvent class is used for event object functionality. The CEvent class is derived from the abstract CSyncObject Class. The CSyncObject is derived from the CObject mother class. The CSingleLock or CMultiLock constructor use one of the CSyncObject derived classes. The CEvent constructor specifies the manual or auto reset event options.

MFC CEvent class provides the constructor with default arguments. If we want to control our needs, we set the Ownership for the event object, manual or auto reset event flag, the name of the event object (if named event), and the security attributes. If the name matches with that of an existing event object, check the type of the object. If that object is an event, the CEvent constructor simply replies the handle of the previous event. If the name exists for any of the other synchronization object, the CEvent constructor gives an error message.

The Manual event object signaled/non-signaled uses SetEvent or ResetEvent function .The Auto Reset event occurs when it releases any one of threads in the event object. We don’t use CSyncObject directly. Because, the CSyncObject is an abstract base class. All the event members are declared in the afxmt.inl file as inline functions.

Mutex Synchronization Object:

Mutex is the synchronization object used to synchronize the threads with more than one process. The Mutex is as the name tells, mutually exclusive. The Mutex object allows accessing the resource single thread at a time.

Win32 Mutex Object:

The CreateMutex function is used to create the Mutex object. In the CreateMutex function, we initialize the named Mutex or unnamed Mutex, and set the ownership to true or false arguments.

If we create two-named Mutex using CreateMutex function within the same process, the second CreateMutex function returns error. If we create two or more Mutex objects on different processes, with the same name, when we call first time, the CreateMutex function creates the Mutex. The other CreateMutex function returns the handle of the previous Mutex object.

The OpenMutex function is used to open an existing Mutex using the supplied Mutex name. The ReleaseMutex is used to release a Mutex object. The threads wait in first in first out order for taking the ownership for the waiting threads. If we try to take the ownership twice, deadlock occurs. We the call ReleaseMutex, and then try to take the ownership.

Example

HANDLE g_Mutex;
DWORD dwWaitResult;


g_Mutex = CreateMutex( NULL, TRUE, "MutexToProtectDatabase");
 

dwWaitResult = WaitForSingleObject( g_Mutex, 5000L);




ReleaseMutex(g_Mutex))

MFC Mutex Object:

The MFC CMutex class is used to control the Mutex objects. The CMutex constructor has three parameters. We can specify the name of the Mutex as a parameter. If this is null, an unnamed Mutex is created. The Security attributes are used to set the security attributes for the Mutex object. If the name already exists for a Mutex object, the constructor simply returns the existing Mutex object. If the name exists for some object, the constructor fails.

To control resource access for single Mutex object, use CSingleLock class. If you wish to control multiple Mutex objects, the CMultiLock is used to control the access to resources in multithreaded programming.

CMutex g_m;
int g_C;

UINT ThreadFunction1(LPVOID lParam)
{

    
    CSingleLock lock(&g_m);

    lock.Lock();

    

    lock.Unlock();

    
    return 0;
}


UINT ThreadFunction2(LPVOID lParam)
{

    
    CSingleLock lock(&g_m);

    lock.Lock();

    

    lock.Unlock();

    
    return 0;
}

Semaphore Thread Synchronization Object:

Semaphore is a thread synchronization object that allows accessing the resource for a count between zero and maximum number of threads. If the Thread enters the semaphore, the count is incremented. If the thread completed the work and is removed from the thread queue, the count is decremented. When the thread count goes to zero, the synchronization object is non-signaled. Otherwise, the thread is signaled.

Win32 Semaphore Synchronization Object:

The CreateSemaphore function is used to create a named or unnamed semaphore thread synchronization object. The initial count and maximum count is mentioned in the CreateSemaphore function. The count is never negative and less then the total count value. The WaitForSingleObject waits for more than one object in semaphore object.

The WaitForMultipleObjects function is non-signaled when all the objects are returned. The OpenSemaphore function is used to open an existing handle to a semaphore object created within the process or another process. The Releasesemaphore function is used to release the semaphore from the Thread synchronization queue. If the CreateSemaphore function has created the same named Thread synchronization object within the process, the CreateSemaphore returns 0. The GetLastError function is used to retrieve the reason for the failure.

Example:

HANDLE g_Semaphore;


g_Semaphore = CreateSemaphore(  NULL,   4, 4,  NULL);  

DWORD dwWaitResult; 

dwWaitResult = WaitForSingleObject(   g_Semaphore, 0L);




ReleaseSemaphore(  g_Semaphore,  1,  NULL) ;

MFC Semaphore Synchronization Object:

The CSemaphore class allows us to create the CSemaphore object. The CSemaphore class is derived from the abstract CSyncObject base class. The CSemaphore class constructor is used to specify the count and maximum number of resource access.

The virtual destructor is used to delete the CSemaphore class object without affecting the CSyncObject base class. The Unlock function is used to unlock the resources.

Conclusion:

Thread Synchronization is used to access the shared resources in a multithread environment. The programmer decides the situation for when to use the synchronization object efficiently. The MFC Thread Synchronization classes internally call the Win32 API functions. The MFC Thread Synchronization classes wrap many of the functionalities form the Windows environment.

Critical Sections

В составе API ОС Windows имеются специальные и эффективные функции для организации входа в критическую секцию и выхода из нее потоков одного процесса в режиме пользователя. Они называются EnterCriticalSection и LeaveCriticalSection и имеют в качестве параметра предварительно проинициализированную структуру типа CRITICAL_SECTION.

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

CRITICAL_SECTION cs; 
DWORD WINAPI SecondThread() 
{ 
 InitializeCriticalSection(&cs);

EnterCriticalSection(&cs); 
… критический участок кода
LeaveCriticalSection(&cs); 
} 

main () 
{
InitializeCriticalSection(&cs);
CreateThread(NULL, 0, SecondThread,…);


EnterCriticalSection(&cs); 
… критический участок кода
LeaveCriticalSecLion(&cs); 
DeleteCriticalSection(&cs);
}

Функции EnterCriticalSection и LeaveCriticalSection реализованы на основе Interlocked-функций, выполняются атомарным образом и работают очень быстро. Существенным является то, что в случае невозможности входа в критический участок поток переходит в состояние ожидания. Впоследствии, когда такая возможность появится, поток будет «разбужен» и сможет сделать попытку входа в критическую секцию. Механизм пробуждения потока реализован с помощью объекта ядра «событие» (event), которое создается только в случае возникновения конфликтной ситуации.

Уже говорилось, что иногда, перед блокированием потока, имеет смысл некоторое время удерживать его в состоянии активного ожидания. Чтобы функция EnterCriticalSection выполняла заданное число циклов спин-блокировки, критическую секцию целесообразно проинициализировать с помощью функции InitalizeCriticalSectionAndSpinCount.

Прогон программы

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

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

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

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

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

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);

где hObject — описатель ожидаемого объекта ядра, а второй параметр — максимальное время ожидания объекта.

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

Другим, более распространенным способом получения описателя является открытие существующего объекта по имени, поскольку многие объекты имеют имена в пространстве имен объектов.
Имя объекта — один из параметров Create -функций. Зная имя объекта, поток, обладающий нужными правами доступа, получает его описатель с помощью Open -функций. Напомним, что в структуре, описывающей объект, имеется счетчик ссылок на него, который увеличивается на 1 при открытии объекта и уменьшается на 1 при его закрытии.

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

Семафоры

Известно, что семафоры, предложенные Дейкстрой в 1965 г., представляет собой целую переменную в пространстве ядра, доступ к которой, после ее инициализации, может осуществляться через две атомарные операции: wait и signal (в ОС Windows это функции WaitForSingleObject и ReleaseSemaphore соответственно).

wait(S):  если S <= 0 процесс блокируется 
   (переводится в состояние ожидания);    
              в противном случае S = S - 1;                               
signal(S):  S = S + 1

Семафоры обычно используются для учета ресурсов (текущее число ресурсов задается переменной S ) и создаются при помощи функции CreateSemaphore, в число параметров которой входят начальное и максимальное значение переменной. Текущее значение не может быть больше максимального и отрицательным. Значение S, равное нулю, означает, что семафор занят.

Ниже приведен пример синхронизации программы async с помощью семафоров.

#include <windows.h>
#include <stdio.h>
#include <math.h>

int Sum = 0, iNumber=5, jNumber=300000;
HANDLE hFirstSemaphore, hSecondSemaphore;

DWORD WINAPI SecondThread(LPVOID)
{
  int i,j;
  double a,b=1.;
    for (i = 0; i < iNumber; i++)
  {
    WaitForSingleObject(hSecondSemaphore, INFINITE); 
    for (j = 0; j < jNumber; j++)
    {
      Sum = Sum + 1;  a=sin(b); 
     }
    
     ReleaseSemaphore(hFirstSemaphore, 1, NULL);
    }
  return 0;
}

void main()
{
  int i,j;
  HANDLE  hThread;
  DWORD  IDThread;
  double a,b=1.;

  hFirstSemaphore = CreateSemaphore(NULL, 0, 1, "MyFirstSemaphore");
  hSecondSemaphore = CreateSemaphore(NULL, 1, 1, "MySecondSemaphore1");
  hThread=CreateThread(NULL, 0, SecondThread, NULL, 0, &IDThread);
  if (hThread == NULL) return;    
  
  for (i = 0; i < iNumber; i++)
  {
	WaitForSingleObject(hFirstSemaphore, INFINITE);
        for (j = 0; j < jNumber; j++)
    {
      Sum = Sum - 1;  a=sin(b);
	}
    	printf(" %d ",Sum);	
	ReleaseSemaphore(hSecondSemaphore, 1, NULL);  
  }	  

WaitForSingleObject(hThread, INFINITE); // ожидание окончания потока SecondThread
CloseHandle(hFirstSemaphore);
CloseHandle(hSecondSemaphore);
printf(" %d ",Sum);	
return;
}

В данной программе синхронизация действий двух потоков , обеспечивающая одинаковый результат для всех запусков программы, выполнена с помощью двух семафоров, примерно так, как это делается в задаче producer-consumer, см., например
[
Таненбаум
]
. Потоки поочередно открывают друг другу дорогу к критическому участку. Первым начинает работать поток SecondThread, поскольку значение счетчика удерживающего его семафора проинициализировано единицей при создании этого семафора.
Синхронизацию с помощью семафоров потоков разных процессов рекомендуется выполнить в качестве самостоятельного упражнения.

Мьютексы

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

Если поток завершается, не освободив мьютекс, последний переходит в свободное состояние.
Отличие от семафоров в том, что поток, занявший мьютекс, получает права на владение им. Только этот поток может освободить мьютекс. Поэтому мнение о мьютексе как о семафоре с максимальным значением 1 не вполне соответствует действительности.

События

Объекты «события» — наиболее примитивные объекты ядра. Они предназначены для информирования одного потока другим об окончании какой-либо операции. События создаются функцией CreateEvent. Простейший вариант синхронизации: переводить событие в занятое состояние функцией WaitForSingleObject и в свободное — функцией SetEvent.

В руководстве по программированию
[
Рихтер
]
,
[
Харт
]
, рассматриваются более сложные сценарии, связанные с типом события (сбрасываемые вручную и сбрасываемые автоматически) и с управлением синхронизацией групп потоков, а также ряд дополнительных полезных функций.

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

Суммарные сведения об объектах ядра

В руководствах по программированию, см., например,
[
Рихтер
]
, и в MSDN содержатся сведения и о других объектах ядра применительно к синхронизации потоков.

В частности, существуют следующие свойства объектов:

  • процесс и поток находятся в занятом состоянии, когда активны, и в свободном состоянии, когда завершаются;
  • файл находится в занятом состоянии, когда выдан запрос на ввод-вывод, и в свободном состоянии, когда операция ввода-вывода завершена;
  • уведомление об изменении файла находится в занятом состоянии, когда в файловой системе нет изменений, и в свободном — когда изменения обнаружены;
  • и т.д.

Синхронизация в ядре

Решение проблемы взаимоисключения особенно актуально для такой сложной системы, как ядро ОС Windows.

Одна из проблем связана с тем, что код ядра зачастую работает на приоритетных IRQL (уровни IRQL рассмотрены в
«Базовые понятия ОС Windows»
) уровнях «DPC/dispatch» или «выше», известных как «высокий IRQL». Это означает, что традиционные средства синхронизации, связанные с приостановкой потока, не могут быть использованы, поскольку процедура планирования и запуска другого потока имеет более низкий приоритет.
Вместе с тем существует опасность возникновения события, чей IRQL выше, чем IRQL критического участка, который будет в этом случае вытеснен. Поэтому в подобных ситуациях прибегают к приему, который называется «запрет прерываний»
[
Карпов
]
,
[
Таненбаум
]
. В случае Windows этого добиваются, искусственно повышая IRQL критического участка до самого высокого уровня, используемого любым возможным источником прерываний. В результате критический участок может беспрепятственно выполнить свою работу.

К сожалению, для мультипроцессорных систем подобная стратегия не годится. Запрет прерываний на одном из процессоров не исключает прерываний на другом процессоре, который может продолжить свою работу и получить доступ к критическим данным. В этом случае нужен специальный протокол установки взаимоисключения. Основой этого протокола является установка блокирующей переменной (переменой-замка), сопоставленной с каждой глобальной структурой данных, с помощью TSL команды. Поскольку установка замка происходит в результате активного ожидания, то говорят, что код ядра устанавливает (захватывает) спин-блокировку. Установка спин-блокировки происходит при высоких IRQL уровнях, поэтому код ядра, захватывающего спин-блокировку и удерживающего ее для выполнения критической секции кода, никогда не вытесняется. Установка и освобождение спин-блокировок осуществляется функциями ядра KeAcquireSpinlock и KeReleaseSpinlock, которые активно используются в ядре и драйверах устройств.
На однопроцессорных системах установка и снятие спин-блокировок реализуется простым повышением и понижением IRQL.

Наконец, имея набор глобальных ресурсов, в данном случае — спин-блокировок, необходимо решить проблему возникновения потенциальных тупиков
[
Сорокина
]
. Например, поток 1 захватывает блокировку 1, а поток 2 захватывает блокировку 2. Затем поток 1 пытается захватить блокировку 2, а поток 2 — блокировку 1. В результате оба потока ядра виснут. Одним из решений данной проблемы является нумерация всех ресурсов и выделение их только в порядке возрастания номеров
[
Карпов
]
. В случае Windows имеется иерархия спин-блокировок: все они помещаются в список в порядке убывания частоты использования и должны захватываться в том порядке, в каком они указаны в списке.

В случае низких IRQL синхронизация осуществляется традиционным образом — при помощи объектов ядра.

Заключение

Проблема недетерминизма является одной из ключевых в параллельных вычислительных средах. Традиционное решение — организация взаимоисключения. Для синхронизации с применением переменной-замка используются Interlocked-функции, поддерживающие атомарность некоторой последовательности операций. Взаимоисключение потоков одного процесса легче всего организовать с помощью примитива Crytical Section. Для более сложных сценариев рекомендуется применять объекты ядра, в частности, семафоры, мьютексы и события. Рассмотрена проблема синхронизации в ядре, основным решением которой можно считать установку и освобождение спин-блокировок.

Понравилась статья? Поделить с друзьями:
  • Синхронизация почты mail ru с почтой windows 10
  • Система windows защитила ваш компьютер как отключить для программы
  • Синхронизация потоков в ос windows возможна
  • Система windows защитила ваш компьютер исключения
  • Синхронизация папок на разных дисках в windows 10