Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых

Программирование для Windows NT

© Александр Фролов, Григорий Фролов
Том 26, часть 1, М.: Диалог-МИФИ, 1996, 272 стр.

[Назад] [Содеожание] [Дальше]

Последовательный доступ к ресурсам

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

Приведем еще один типичный пример, когда необходимо обеспечить последовательный доступ к ресурсу.

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

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

К чему это может привести?

Если перекрытия во времени не произойдет, то вначале задача, запущенная для первого торгового агента, прочитает общую сумму, которая будет равна 2 млн. долларов. Затем она вычтет из нее стоимость покупки и запишет результат (500 тыс. долларов) обратно. Второй торговый агент уже не сможет сделать покупку, так как он обнаружит, что на счету осталось слишком мало денег.

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

·       первый агент получает значение общей суммы (2 млн. долларов);

·       второй агент получает значение общей суммы (2 млн. долларов);

·       первый агент вычитает из этой суммы стоимость товара (1,5 млн. долларов) и записывает результат (500 тыс. долларов) в базу данных;

·       второй агент вычитает из общей суммы стоимость товара и записывает результат в базу данных

Кто пострадает в данной ситуации?

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

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

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

·       задача получает значения общей суммы;

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

·       задача разблокирует доступ к записи базы данных

Таким образом, при использовании приведенного выше сценария во время попытки одновременного изменения значения счета двумя торговыми агентами задача, запущенная чуть позже, перейдет в состояние ожидания. Когда же первый агент сделает покупку, задача второго агента получит правильное значение остатка (500 тыс. долларов), что не позволит ему приобрести товар на 1,5 млн. долларов.

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

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

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

Критическая секция XE "критическая секция" создается как структура типа CRITICAL_SECTION XE "CRITICAL_SECTION" :


CRITICAL_SECTION csWindowPaint; 

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

В файле winbase.h (который включается автоматически при включении файла windows.h) структура CRITICAL_SECTION и указатели на нее определены следующим образом:


typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
typedef PRTL_CRITICAL_SECTION PCRITICAL_SECTION;
typedef PRTL_CRITICAL_SECTION LPCRITICAL_SECTION;

Определение недокументированной структуры RTL_CRITICAL_SECTION XE "RTL_CRITICAL_SECTION" вы можете найти в файле winnt.h:


typedef struct _RTL_CRITICAL_SECTION 
{
  PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
  LONG   LockCount;      // счетчик блокировок
  LONG   RecursionCount; // счетчик рекурсий
  HANDLE OwningThread;   // идентификатор задачи, владеющей 
                         // секцией
  HANDLE LockSemaphore;  // идентификатор семафора
  DWORD  Reserved;       // зарезервировано
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

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

Инициализация критической секции

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


CRITICAL_SECTION csWindowPaint; 
InitializeCriticalSection(&csWindowPaint);

Функция InitializeCriticalSection имеет только один параметр (адрес структуры типа CRITICAL_SECTION) и не возвращает никакого значения.

Удаление критической секции

Если критическая секция больше не нужна, ее нужно удалить при помощи функции DeleteCriticalSection XE "DeleteCriticalSection" , как это показано ниже:


DeleteCriticalSection(&csWindowPaint);

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

Вход в критическую секцию и выход из нее

Две основные операции, выполняемые задачами над критическими секциями, это вход в критическую секцию и выход из критической секции. Первая операция выполняется при помощи функции EnterCriticalSection XE "EnterCriticalSection" , вторая - при помощи функции LeaveCriticalSection XE "LeaveCriticalSection" . Эти функции, не возвращающие никакого значения, всегда используются в паре, как это показано в следующем фрагменте исходного текста рассмотренного нами ранее приложения MultiSDI:


EnterCriticalSection(&csWindowPaint);
hdc = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rc);
DrawText(hdc, "SDI Window", -1, &rc, 
  DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
LeaveCriticalSection(&csWindowPaint);

В качестве единственного параметра функциям EnterCriticalSection и LeaveCriticalSection необходимо передать адрес стрктуры типа CRITICAL_SECTION, проинициализированной предварительно функцией InitializeCriticalSection XE "InitializeCriticalSection" .

Как работают критические секции?

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

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

Рекурсивный вход в критическую секцию

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


EnterCriticalSection(&csWindowPaint);
PaintClient(hWnd);
LeaveCriticalSection(&csWindowPaint);
. . .  
void PaintClient(HWND hWnd)
{
  . . .
  EnterCriticalSection(&csWindowPaint);
  hdc = BeginPaint(hWnd, &ps);
  GetClientRect(hWnd, &rc);
  DrawText(hdc, "SDI Window", -1, &rc, 
    DT_SINGLELINE | DT_CENTER | DT_VCENTER);
  EndPaint(hWnd, &ps);
  LeaveCriticalSection(&csWindowPaint);
}

Здесь мы выполняем вызов функции PaintClient, находясь в критической секции csWindowPaint. При этом сама функция PaintClient также пользуется той же критической секцией.

Рекурсивный вход задачи в ту же самую критическую секцию не приводит к тому, что задача переходит в состояние ожидания. Однако для освобождения критической секции необходимо вызывать функцию LeaveCriticalSection XE "LeaveCriticalSection" столько же раз, сколько раз вызывается функция EnterCriticalSection XE "EnterCriticalSection" .

Работа задачи с несколькими критическими секциями

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

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

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


CRITICAL_SECTION csWindowOnePaint; 
CRITICAL_SECTION csWindowTwoPaint; 

Пусть первая задача выполняет рисование в этих окнах следующим образом:


EnterCriticalSection(&csWindowOnePaint);
EnterCriticalSection(&csWindowTwoPaint);
PaintClientWindow(hWndOne);
PaintClientWindow(hWndTwo);
LeaveCriticalSection(&csWindowTwoPaint);
LeaveCriticalSection(&csWindowOnePaint);

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


EnterCriticalSection(&csWindowTwoPaint);
EnterCriticalSection(&csWindowOnePaint);
PaintClientWindow(hWndOne);
PaintClientWindow(hWndTwo);
LeaveCriticalSection(&csWindowOnePaint);
LeaveCriticalSection(&csWindowTwoPaint);

При этом есть вероятность того что когда первая задача войдет в критическую секцию csWindowOnePaint, управление будет передано второй задаче, которая войдет в критическую секцию csWindowTwoPaint и перейдет в состояние ожидания. Она будет ждать освобождения критической секции csWindowOnePaint, занятой первой задачей.

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

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


// Рисование в первом окне
EnterCriticalSection(&csWindowOnePaint);
PaintClientWindow(hWndOne);
LeaveCriticalSection(&csWindowOnePaint);

// Рисование во втором окне
EnterCriticalSection(&csWindowTwoPaint);
PaintClientWindow(hWndTwo);
LeaveCriticalSection(&csWindowTwoPaint);

Объекты Mutex

Если необходимо обеспечить последовательное использование ресурсов задачами, созданными в рамках разных процессов, вместо критических секций необходимо использовать объекты синхронизации Mutex XE "Mutex" . Свое название они получили от выражения “mutually exclusive”, что означает “взаимно исключающий”.

Также как и объект-событие, объект Mutex может находится в отмеченном или неотмеченном состоянии. Когда какая-либо задача, принадлежащая любому процессу, становится владельцем объекта Mutex, последний переключается в неотмеченное состояние. Если же задача “отказывается” от владения объектом Mutex, его состояние становится отмеченным.

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

Для того чтобы объект Mutex был доступен задачам, принадлежащим различным процессам, при создании вы должны присвоить ему имя (аналогично тому, как вы это делали для объекта-события).

Создание объекта Mutex

Для создания объекта Mutex вы должны использовать функцию CreateMutex XE "CreateMutex" , прототип которой мы привели ниже:


HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes, // атрибуты защиты 
  BOOL bInitialOwner, // начальное состояние 
  LPCTSTR lpName);    // имя объекта Mutex 

В качестве первого параметра (атрибуты защиты) вы можете указать значение NULL (как и во всех наших примерах).

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

Через параметр lpName вы должны передать указатель на имя объекта Mutex, для которого действуют те же правила, что и для имени объекта-события. Это имя не должно содержать символ ‘\’ и его длина не должна превышать значение MAX_PATH XE "MAX_PATH" .

Если объект Mutex будет использован только задачами одного процесса, вместо адреса имени можно указать значение NULL. В этом случае будет создан “безымянный” объект Mutex.

Функция CreateMutex возвращает идентификатор созданного объекта Mutex или NULL при ошибке.

Возможно возникновение такой ситуации, когда приложение пытается создать объект Mutex с именем, которое уже используется в системе другим объектом Mutex. В этом случае функция CreateMutex XE "CreateMutex" вернет идентификатор существующего объекта Mutex, а функция GetLastError XE "GetLastError" , вызыванная сразу после вызова функции CreateMutex, вернет значение ERROR_ALREADY_EXISTS XE "ERROR_ALREADY_EXISTS" . Заметим, что функция создания объектов-событий CreateEvent XE "CreateEvent" ведет себя в данной ситуации аналогичным образом.

Освобождение идентификатора объекта Mutex

Если объект Mutex больше не нужен, вы должны освободить его идентификатор при помощи универсальной функции CloseHandle XE "CloseHandle" . Заметим, тем не менее, что при завершении процесса освобождаются идентификаторы всех объектов Mutex, созданных для него.

Открытие объекта Mutex

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


HANDLE OpenMutex(
  DWORD   fdwAccess, // требуемый доступ 
  BOOL    fInherit,  // флаг наследования 
  LPCTSTR lpszMutexName ); // адрес имени объекта Mutex 

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

 Значение

 Описание

 EVENT_ALL_ACCESS

 Указаны все возможные флаги доступа

 SYNCHRONIZE

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

Параметр fInherit определяет возможность наследования полученного идентфикатора. Если этот параметр равен TRUE, идентфикатор может наследоваться дочерними процессами. Если же он равен FALSE, наследование не допускается.

Через параметр lpszEventName вы должны передать функции адрес символьной строки, содержащей имя объекта Mutex.

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

Как завладеть объектом Mutex

Зная идентификатор объекта Mutex, полученный от функций CreateMutex или OpenMutex, задача может завладеть объектом при помощи функций ожидания событий, например, при помощи функций WaitForSingleObject XE "WaitForSingleObject" или WaitForMultipleObjects XE "WaitForMultipleObjects" .

Напомним, что функция WaitForSingleObject возвращает управление, как только идентификатор объекта, передаваемый ей в качестве параметра, перейдет в отмеченное состояние. Если объект Mutex не принадлежит ни одной задаче, его состояние будет отмеченным.

Когда вы вызываете функцию WaitForSingleObject для объекта Mutex, который никому не принадлежит, она сразу возвращает управление. При этом задача, вызвавшая функцию WaitForSingleObject, становится владельцем объекта Mutex. Если теперь другая задача вызовет функцию WaitForSingleObject для этого же объекта Mutex, то она будет переведена в сотояние ожидания до тех пор, пока первая задача не “откажется от своих прав” на данный объект Mutex. Освобождение объекта Mutex выполняется функцией ReleaseMutex XE "ReleaseMutex" , которую мы сейчас рассмотрим.

Захват объекта Mutex во владение по своему значению аналогичен входу в критическую секцию.

Освобождение объекта Mutex

Для отказа от владения объектом Mutex (то есть для его освобождения) вы должны использовать функцию ReleaseMutex XE "ReleaseMutex" :


BOOL ReleaseMutex(HANDLE  hMutex);

Через единственный параметр этой функции необходимо передать идентификатор объекта Mutex. Функция возвращает значение TRUE при успешном завершении и FALSE при ошибке.

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

Рекурсивное использование объектов Mutex

Так же как и критические секции, объекты Mutex допускают рекурсивное использование. Задача может выполнять рекурсивные попытки завладеть одним и тем же объектом Mutex и при этом она не будет переводиться в состояние ожидания.

В случае рекурсивного использования каждому вызову функции ожидания должен соответствовать вызов функции освобождения объекта Mutex ReleaseMutex XE "ReleaseMutex" .

Блокирующие функции

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

Функции InterlockedIncrement и InterlockedDecrement выполняют, соответственно, увеличение и уменьшение на единицу значения переменной типа LONG, адрес которой передается им в качестве единственного параметра:


LONG InterlockedIncrement(LPLONG lpAddend);
LONG InterlockedDecrement(LPLONG lpAddend);

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

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

А как может произойти ошибка?

Пусть, например, первая задача увеличивает содержимое глобальной переменной lAddend следующим образом:


lAddend += 1;

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

Если теперь эта другая задача попытается выполнить увеличение той же самой переменной, она будет увеличивать старое значение, которое было до того момента, как первая задача начала его увеличение. В результате значение переменной будет увеличено не два раза, как это должно быть (так как две задачи пытались увеличить значение переменной), а только один раз. Эта ситуация напоминает случай с двумя торговыми агентами, которые перерасходовали деньги своей фирмы. Если же для увеличения переменной использовать функцию InterlockedIncrement XE "InterlockedIncrement" , такой ошибки не произойдет.

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


lTaget = lNewValue;

Специально для того чтобы избежать такой опасности, в программном интерфейсе Microsoft Windows NT предусмотрена функция InterlockedExchange XE "InterlockedExchange" :


LONG InterlockedExchange(
  LPLONG lpTarget,   // адрес изменяемой переменной 
  LONG   lNewValue); // новое значение для переменной 

Эта функция записывает значение lNewValue по адресу lpTarget. При этом гарантируется, что операция не будет прервана другой задачей, выполняющейся в рамках того же процесса.

Функция InterlockedExchange возвращает старое значение изменяемой переменной.

Что же касается значения, возвращаемого функциями InterlockedIncrement XE "InterlockedIncrement" и InterlockedDecrement XE "InterlockedDecrement" , то оно равно нулю, если в результате изменений значение переменной стало равно нулю. Если в результате увеличения или уменьшения значение переменной стало больше или меньше нуля, то эти функции возвращают, соответственно, значение, большее или меньшее нуля. Это значение, однако, можно использовать только для сравнения, так как абсолютная величина возвращенного значения не равна новому значению изменяемой переменной.

[Назад] [Содеожание] [Дальше]