4. Синхронизация задач и процессов

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

В чем заключается синхронизация задач и приложений?

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

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

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

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

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

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

Легко ли ждать

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

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

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

Ожидание завершения задачи или процесса

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

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


DWORD WaitForSingleObject(
  HANDLE hObject,    // идентификатор объекта 
  DWORD  dwTimeout); // время ожидания в миллисекундах

В качестве параметра hObject этой функции нужно передать идентификатор объекта, для которого выполняется ожидание, а в качестве параметра dwTimeout - время ожидания в миллисекундах (ожидание может быть и бесконечным, если для времени указать значение INFINITE).

Многие объекты операционной системы Microsoft Windows NT, такие, например, как идентификаторы задач, процессов, файлов, могут находиться в двух состояниях - отмеченном (signaled) и неотмеченном (nonsignaled). В частности, если задача или процесс находятся в состоянии выполнения (то есть работают), соответствующие идентификаторы находятся в неотмеченном состоянии. Когда же задача или процесс завершают свою работу, их идентификаторы отмечаются (то есть переходят в отмеченное состояние).

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

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

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

На рис. 4.1 показано, как задача с номером 1 ожидает завершение задачи с номером 2, имеющей идентификатор hThread2.

Рис. 4.1. Ожидание завершения задачи

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

В качестве примера использования функции WaitForSingleObject XE "WaitForSingleObject" для ожидания завершения дочернего процесса, рассмотрим немного измененный фрагмент исходного текста приложения PSTART, описанного в предыдущей главе:


if(CreateProcess(NULL, ofn.lpstrFile, NULL, NULL,
        FALSE, dwCreationFlags, NULL, NULL, &si, &pi))
{
 ...
  if(WaitForSingleObject(pi.hProcess, INFINITE) != 
    WAIT_FAILED)
  {
    GetExitCodeProcess(pi.hProcess, &dwExitCode);
   ...
  }
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
}

Здесь главная задача с помощью функции CreateProcess запускает процесс. Идентификатор этого процесса сохраняется в поле hProcess структуры pi. Если запуск процесса произошел успешно, главная задача приложения приостанавливает свою работу до тех пор, пока запущенный процесс не завершит свою работу. Для этого она вызывает функцию WaitForSingleObject XE "WaitForSingleObject", передавая ей идентификатор запущенного процесса.

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


if(CreateProcess(NULL, ofn.lpstrFile, NULL, NULL,
        FALSE, dwCreationFlags, NULL, NULL, &si, &pi))
{
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
}

Запущенный таким образом процесс называется отсоединенным (detached). Он будет жить своей жизнью независимо от состояния запустившего его процесса.

Теперь поговорим о коде завершения функции WaitForSingleObject.

В случае ошибки функция возвращает значение WAIT_FAILED. При этом код ошибки можно получить при помощи функции GetLastError.

Если же функция завершилась успешно, она может вернуть одно из следующих трех значений: WAIT_OBJECT_0, WAIT_TIMEOUT или WAIT_ABANDONED.

Если состояние идентификатора объекта, для которого выполнялось ожидание, стало отмеченным, функция фозвращает значение WAIT_OBJECT_0. Таким образом, когда мы ожидаем завершение задачи и задача завершилась “естественным образом”, функция WaitForSingleObject XE "WaitForSingleObject" вернет именно это значение.

Если время ожидания, заданное во втором параметре функции WaitForSingleObject истекло, но объект так и не перешел в отмеченное состояние, возвращается значение WAIT_TIMEOUT. Очевидно, при бесконечном ожидании вы никогда не получите этот код завершения.

Код завершения WAIT_ABANDONED возвращается для объекта синхронизации типа Mutex (его мы рассмотрим позже), который не был освобожден задачей, завершившей свою работу. Таким образом, в этом случае ожидание было отменено и соответствующий объект (Mutex) не перешел в отмеченное состояние.

Ожидание завершения нескольких задач или процессов

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


DWORD WaitForMultipleObjects(
  DWORD cObjects,     // количество идентификаторов в массиве 
  CONST HANDLE *lphObjects, // адрес массива идентификаторов 
  BOOL  fWaitAll,           // тип ожидания 
  DWORD dwTimeout);         // время ожидания в миллисекундах 

Через параметр lphObjects функции WaitForMultipleObjects нужно передать адрес массива идентификаторов. Размер этого массива передается через параметр cObjects.

Если содержимое параметра fWaitAll равно TRUE, задача переводится в состояние ожидания до тех пор, пока все задачи или процессы, идентификаторы которых хранятся в массиве lphObjects, не завершат свою работу. В том случае, когда значение параметра fWaitAll равно FALSE, ожидание прекращается, когда одна из указанных задач или процессов завершит свою работу. Для выполнения бесконечного ожидания, как и в случае функции WaitForSingleObject XE "WaitForSingleObject", через параметр dwTimeout следует передать значение INFINITE.

Как пользоваться этой функцией?

Пример вы можете найти в исходных текстах приложеения MultiSDI, описанного ранее.

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


HANDLE hThreads[3];

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


hThreads[0] = (HANDLE)_beginthread(PaintEllipse, 0, 
  (void*)hWnd);
hThreads[1] = (HANDLE)_beginthread(PaintRect,    0, 
  (void*)hWnd);
hThreads[2] = (HANDLE)_beginthread(PaintText,    0, 
  (void*)hWnd);

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


WaitForMultipleObjects(3, hThreads, TRUE, INFINITE);

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

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

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

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

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

Схема использования событий достаточно проста.

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

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

Другая задача, принадлежащая тому же самому или другому процессу, может получить идентификатор события по его имени, например, с помощью функции OpenEvent XE "OpenEvent". Далее, пользуясь функциями SetEvent XE "SetEvent", ResetEvent XE "ResetEvent" или PulseEvent, XE "PulseEvent" эта задача может изменить состояние события.

На рис. 4.2 приведен пример использования события для синхронизации двух задач, работающих одновременно.

Рис. 4.2. Пример использования события для синхронизации двух задач

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

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

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

Создание события

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


HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes, // атрибуты защиты 
  BOOL    bManualReset,  // флаг ручного сброса события 
  BOOL    bInitialState, // флаг начального состояния события 
  LPCTSTR lpName);       // адрес имени объекта-события 

Параметр lpEventAttributes задает атрибуты защиты и в большинстве случаев может быть указан как NULL.

С помощью параметра bManualReset вы можете выбрать один из двух режимов работы объекта-события: ручной или автоматический. Если значение этого параметра равно TRUE, событие нужно сбрасывать вручную при помощи функции ResetEvent XE "ResetEvent", которую мы рассмотрим немного позже. Если же для параметра bManualReset указать значение FALSE, событие будет сброшено (то есть переведено в неотмеченное состояние) автоматически сразу после того как задача завершит ожидание этого события.

Параметр bInitialState определяет начальное состояние события. Если этот параметр равен TRUE, объект-событие создается в отмеченном состоянии, а если FALSE - в неотмеченном.

Для того чтобы событием могли пользоваться задачи, созданные разными процессами, необходимо с помощью параметра lpName задать имя объекта-события. В качестве имени вы можете выбрать любое имя размером не более MAX_PATH XE "MAX_PATH" символов, не содержащее символ “\”.

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

В случае успешного завершения функция CreateEvent возвращает идентификатор события, которым нужно будет пользоваться при выполнении всех операций над объектом-событием. При ошибке возвращается значение NULL (код ошибки можно получить при помощи функции GetLastError XE "GetLastError" ).

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

Открытие события

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

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

Прототип функции OpenEvent представлен ниже:


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

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

Значение Описание
EVENT_ALL_ACCESS Указаны все возможные флаги доступа
EVENT_MODIFY_STATE Полученный идентификатор можно будет использовать для функций SetEvent и ResetEvent
SYNCHRONIZE Полученный идентификатор можно будет использовать в любых функциях ожидания события

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

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

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

Установка события

Для установки объекта-события в отмеченное состояние используется функция SetEvent:


BOOL SetEvent(HANDLE hEvent);

В качестве единственного параметра этой функции необходимо передать идентификатор объекта-события, полученного от функции CreateEvent XE "CreateEvent" или OpenEvent XE "OpenEvent". При успешном завершении возвращается значение TRUE, при ошибке - FALSE. В последнем случае можно получить код ошибки при помощи функции GetLastError XE "GetLastError".

Cброс события

Сброс события (то есть установка его в неотмеченное состояние) выполняется функцией ResetEvent XE "ResetEvent" :


BOOL ResetEvent(HANDLE hEvent);

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

Функция PulseEvent

Функция PulseEvent выполняет установку объекта-события в отмеченное состояние с последующим сбросом события в неотмеченное состояние:


BOOL PulseEvent(HANDLE hEvent);

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

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

Приложения EVENT и EVENTGEN

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

Первым необходимо запускать приложение EVENT. Его задача заключается в том, чтобы следить за работой второго приложения EVENTGEN. Приложение EVENTGEN позволяет пользователю вводить с помощью клавиатуры и отображать в своем окне произвольные символы. Каждый раз когда пользователь вводит в окне приложения EVENTGEN какой-либо символ, в окне контролирующего приложения EVENT отображается символ ‘*’ (рис. 4.3).

Рис. 4.3. Окна приложений EVENT и EVENTGEN

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

Исходные тексты приложения EVENT

Рассмотрим сначала исходные тексты приложения EVENT, представленные в листинге 4.1.

Листинг 4.1. Файл event/event1.c


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

// Идентификаторы объектов-событий, которые используются
// для синхронизации задач, принадлежащих разным процессам
HANDLE hEvent;
HANDLE hEventTermination;

// Имя объекта-события для синхронизации ввода и отображения
CHAR lpEventName[] = 
  "$MyVerySpecialEventName$";

// Имя объекта-события для завершения процесса
CHAR lpEventTerminationName[] = 
  "$MyVerySpecialEventTerminationName$";

int main()
{
  DWORD dwRetCode;

  printf("Event demo application, master process\n"
    "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n");
  
  // Создаем объект-событие для синхронизации 
  // ввода и отображения, выполняемого в разных процессах
  hEvent = CreateEvent(NULL, FALSE, FALSE, lpEventName);
  
  // Если произошла ошибка, получаем и отображаем ее код,
  // а затем завершаем работу приложения
  if(hEvent == NULL)
  {
    fprintf(stdout,"CreateEvent: Error %ld\n", 
      GetLastError());
    getch();
    return 0;
  }

  // Если объект-событие с указанным именем существует,
  // считаем, что приложение EVENT уже было запущено
  if(GetLastError() == ERROR_ALREADY_EXISTS)
  {
    printf("\nApplication EVENT already started\n"
      "Press any key to exit...");
    getch();
    return 0;
  }

  // Создаем объект-событие для определения момента
  // завершения работы процесса ввода
  hEventTermination = CreateEvent(NULL, 
    FALSE, FALSE, lpEventTerminationName);

  if(hEventTermination == NULL)
  {
    fprintf(stdout,"CreateEvent (Termination): Error %ld\n", 
      GetLastError());
    getch();
    return 0;
  }
  
  // Цикл отображения. Этот цикл завершает свою работу
  // при завершении процесса ввода
  while(TRUE)
  {
    // Проверяем состояние объекта-события, отвечающего
    // за контроль завершения процесса ввода. Так как
    // указано нулевое время ожидания, такая проверка
    // не уменьшает заметно скорость работы приложения
    dwRetCode = WaitForSingleObject(hEventTermination, 0);

    // Если объект-событие перешел в отмеченное состояние,
    // если процесс ввода завершил свою работу, или
    // если при ожидании произошла ошибка,
    // останавливаем цикл отображения
    if(dwRetCode == WAIT_OBJECT_0  || 
       dwRetCode == WAIT_ABANDONED ||
       dwRetCode == WAIT_FAILED)
      break;

    // Выполняем ожидание ввода символа в процессе,
    // который работает с клавиатурой
    dwRetCode = WaitForSingleObject(hEvent, INFINITE);
    
    // При возникновении ошибки прерываем цикл
    if(dwRetCode == WAIT_FAILED || 
       dwRetCode == WAIT_ABANDONED)
      break;

    // В ответ на каждый символ, введенный процессом, который
    // работает с клавиатурой, отображаем символ '*'
    putch('*');
  }
 
  // Закрываем идентификаторы объектов-событий  
  CloseHandle(hEvent);
  CloseHandle(hEventTermination);

  return 0;
}

В глобальных переменных lpEventName и lpEventTerminationName мы храним имена двух объектов-событий, которые будут использоваться для синхронизации. Эти же имена будут указаны функции OpenEvent XE "OpenEvent" в приложении EVENTGEN, которое мы рассмотрим чуть позже.

Объекты-события создаются нашим приложением при помощи функции CreateEvent, вызываемой следующим способом:


hEvent = CreateEvent(NULL, FALSE, FALSE, lpEventName);
hEventTermination = CreateEvent(NULL, FALSE, FALSE, 
  lpEventTerminationName);

Здесь в качестве атрибутов защиты мы указываем значение NULL. Через второй и третий параметр функции CreateEvent XE "CreateEvent" имеют значение FALSE, поэтому создается автоматический объект-событие, которое изначально находится в неотмеченном состоянии. Имя этого события, доступное всем запущенным приложениям, передается функции CreateEvent через последний параметр.

Обратите внимание на следующий фрагмент кода, который расположен сразу после вызова функции, создающей объект-событие с именем lpEventName:


if(GetLastError() == ERROR_ALREADY_EXISTS)
{
  printf("\nApplication EVENT already started\n"
    "Press any key to exit...");
  getch();
  return 0;
}

Функция CreateEvent может завершиться с ошибкой и в этом случае она возвращает значение NULL. Однако пользователь может попытаться запустить приложение EVENT два раза. В первый раз при этом будет создан объект-событие с именем lpEventName. Когда же функция CreateEvent будет вызвана для этого имени еще раз при попытке повторного запуска приложения, она не вернет признак ошибки, несмотря на то что объект-событие с таким именем уже существует в системе. Вместо этого функция вернет идентификатор для уже существующего объекта-события.

Как распознать такую ситуацию?

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

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

Продолжим изучение исходный текстов приложения EVENT.

После того как приложение создаст два объекта-события, оно входит в цикл отобаржения символа ‘*’.

В этом цикле прежде всего проверяется состояние объекта-события с идентификатором hEventTermination:


dwRetCode = WaitForSingleObject(hEventTermination, 0);
if(dwRetCode == WAIT_OBJECT_0  || 
   dwRetCode == WAIT_ABANDONED ||
   dwRetCode == WAIT_FAILED)
  break;

Приложение EVENTGEN переводит этот объект в отмеченное состояние перед своим завершением.

Обращаем ваше внимание на то что функции WaitForSingleObject указано нулевое время ожидания. В этом случае функция проверяет состояние объекта и сразу же возвращает управление.

Далее выполняется ожидание объекта-события с идентификатором hEvent:


dwRetCode = WaitForSingleObject(hEvent, INFINITE);
if(dwRetCode == WAIT_FAILED || 
   dwRetCode == WAIT_ABANDONED)
  break;

Если это ожидание завершилось с ошибкой, выполняется выход из цикла. Если же оно было выполнено без ошибок, приложение EVENT отображает символ ‘*’, пользуясь для этого функцией putch, известной вам из практики программирования для MS-DOS.

Перед завершением работы приложения мы выполняем освобождение идентификаторов созданных объектов-событий, пользуясь для этого функцией CloseHandle XE "CloseHandle" :


CloseHandle(hEvent);
CloseHandle(hEventTermination);

Исходные тексты приложения EVENTGEN

Исходные тексты приложения EVENTGEN, которое работает в паре с только что рассмотренным приложением EVENT, приведены в листинге 4.2.

Листинг 4.2. Файл event/eventgen/event2.c


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

// Идентификаторы объектов-событий, которые используются
// для синхронизации задач, принадлежащих разным процессам
HANDLE hEvent;
HANDLE hEventTermination;

// Имя объекта-события для синхронизации ввода и отображения
CHAR lpEventName[] = 
  "$MyVerySpecialEventName$";

// Имя объекта-события для завершения процесса
CHAR lpEventTerminationName[] = 
  "$MyVerySpecialEventTerminationName$";

int main()
{
  CHAR chr;

  printf("Event demo application, slave process\n"
    "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n"
    "\n\nPress <ESC> to terminate...\n");
  
  // Открываем объект-событие для синхронизации 
  // ввода и отображения
  hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, lpEventName);
  
  if(hEvent == NULL)
  {
    fprintf(stdout,"OpenEvent: Error %ld\n", 
      GetLastError());
    getch();
    return 0;
  }

  // Открываем объект-событие для сигнализации о
  // завершении процесса ввода
  hEventTermination = OpenEvent(EVENT_ALL_ACCESS, 
    FALSE, lpEventTerminationName);
  
  if(hEventTermination == NULL)
  {
    fprintf(stdout,"OpenEvent (Termination): Error %ld\n", 
      GetLastError());
    getch();
    return 0;
  }

  // Цикл ввода. Этот цикл завершает свою работу,
  // когда пользователь нажимает клавишу <ESC>, 
  // имеющую код 27
  while(TRUE)
  {
    // Проверяем код введенной клавиши
    chr = getche();
    
    // Если нажали клавишу <ESC>, прерываем цикл
    if(chr == 27)
      break;

    // Устанавливаем объект-событие в отмеченное
    // состояние. В ответ на это процесс отображения
    // выведет на свою консоль символ '*'
    SetEvent(hEvent);
  }
 
  // После завершения цикла переключаем оба события
  // в отмеченное состояние для отмены ожидания в
  // процессе отображения и для завершения этого процесса
  SetEvent(hEvent);
  SetEvent(hEventTermination);
  
  // Закрываем идентификаторы объектов-событий
  CloseHandle(hEvent);
  CloseHandle(hEventTermination);

  return 0;
}

В глобальных переменных lpEventName и lpEventTerminationName записаны имена объектов-событий, которые в точности такие же, как и в приложении EVENT.

Функция main приложения EVENTGEN прежде всего открывает объекты-события, созданные приложением EVENT. Для этого она вызывает функцию OpenEvent XE "OpenEvent", как это показано ниже:


hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, lpEventName);
hEventTermination = OpenEvent(EVENT_ALL_ACCESS, 
  FALSE, lpEventTerminationName);

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

Далее функция main приложения EVENTGEN входит в цикл, в котором она выполняет ввод символов с клавиатуры. Этот цикл прерывается, если пользователь нажимает клавишу <ESC>.

После ввода каждого символа приложение EVENTGEN устанавливает объект-событие hEvent в отмеченное состояние, вызывая для этого функцию SetEvent XE "SetEvent" :


SetEvent(hEvent);

При этом приложение EVENT выходит из состояния ожидания и отображает в своем окне один символ ‘*’. Так как объект-событие работает в автоматическом режиме, после выхода главной задачи приложения EVENT из состояния ожидания объект-событие hEvent автоматически сбрасывается в неотмеченное состояние. В результате после отображения одного символа ‘*’ приложение EVENT вновь переводится в состояние ожидания до тех пор, пока пользователь не нажмет какую-либо клавишу в окне приложения EVENTGEN.

После того как пользователь нажал клавишу <ESC>, оба объекта-события переключаются в отмеченное состояние:


SetEvent(hEvent);
SetEvent(hEventTermination);

Это приводит к тому что в приложении EVENT завершается цикл отображения.

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

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

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

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

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

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

Если перекрытия во времени не произойдет, то вначале задача, запущенная для первого торгового агента, прочитает общую сумму, которая будет равна 2 млн. долларов. Затем она вычтет из нее стоимость покупки и запишет результат (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", то оно равно нулю, если в результате изменений значение переменной стало равно нулю. Если в результате увеличения или уменьшения значение переменной стало больше или меньше нуля, то эти функции возвращают, соответственно, значение, большее или меньшее нуля. Это значение, однако, можно использовать только для сравнения, так как абсолютная величина возвращенного значения не равна новому значению изменяемой переменной.

Приложение MutexSDI

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

Исходный текст приложения приведен в листинге 4.3. Так как он сделан из исходного текста приложения MultiSDI, мы опишем только основные отличия.

Листинг 4.3. Файл mutexsdi/mutexsdi.c


#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <process.h>
#include <stdio.h>
#include "resource.h"
#include "afxres.h"
#include "mutexsdi.h"

HINSTANCE hInst;
char szAppName[]   = "MutexMultiSDI";
char szAppTitle[]  = "Multithread SDI Application with Mutex";

// Имя объекта Mutex
char szMutexName[] = "$MyMutex$MutexMultiSDI$";

// Идентификатор объекта Mutex
HANDLE hMutex;

// Признак завершения всех задач
BOOL fTerminate = FALSE;

// Массив идентификаторов запущенных задач
HANDLE hThreads[3];

// -----------------------------------------------------
// Функция WinMain
// -----------------------------------------------------
int APIENTRY 
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
{
  WNDCLASSEX wc;
  HWND hWnd;
  MSG msg;
  
  // Сохраняем идентификатор приложения
  hInst = hInstance;

  // Создаем объект Mutex  
  hMutex = CreateMutex(NULL, FALSE, szMutexName);
  if(hMutex == NULL)
  {
    MessageBox(NULL, "CreateMutex Error",
        szAppTitle, MB_OK | MB_ICONEXCLAMATION);
    return 0l;
  }

  // Преверяем, не было ли это приложение запущено ранее
  if(GetLastError() == ERROR_ALREADY_EXISTS)
  {
    MessageBox(NULL, "MutexSDI already started",
        szAppTitle, MB_OK | MB_ICONEXCLAMATION);
    return 0l;
  }

  // Регистрируем класс окна
  memset(&wc, 0, sizeof(wc));
  wc.cbSize = sizeof(WNDCLASSEX);
  wc.hIconSm = LoadImage(hInst,
    MAKEINTRESOURCE(IDI_APPICONSM), 
    IMAGE_ICON, 16, 16, 0);
  wc.style = 0;
  wc.lpfnWndProc = (WNDPROC)WndProc;
  wc.cbClsExtra  = 0;
  wc.cbWndExtra  = 0;
  wc.hInstance = hInst;
  wc.hIcon = LoadImage(hInst,
    MAKEINTRESOURCE(IDI_APPICON), 
    IMAGE_ICON, 32, 32, 0);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
  wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU);
  wc.lpszClassName = szAppName;
  if(!RegisterClassEx(&wc))
    if(!RegisterClass((LPWNDCLASS)&wc.style))
	  return FALSE;
    
  // Создаем главное окно приложения
  hWnd = CreateWindow(szAppName, szAppTitle, 
     WS_OVERLAPPEDWINDOW, 
     CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 
     NULL, NULL, hInst, NULL);
  if(!hWnd) return(FALSE);

  // Отображаем окно и запускаем цикл 
  // обработки сообщений
  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);
  while(GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// -----------------------------------------------------
// Функция WndProc
// -----------------------------------------------------
LRESULT WINAPI
WndProc(HWND hWnd, UINT msg, WPARAM wParam, 
        LPARAM lParam)
{
  switch(msg)
  {
    HANDLE_MSG(hWnd, WM_CREATE,     WndProc_OnCreate);
    HANDLE_MSG(hWnd, WM_DESTROY,    WndProc_OnDestroy);
    HANDLE_MSG(hWnd, WM_PAINT,      WndProc_OnPaint);
    HANDLE_MSG(hWnd, WM_COMMAND,    WndProc_OnCommand);

    default:
      return(DefWindowProc(hWnd, msg, wParam, lParam));
  }
}

// -----------------------------------------------------
// Функция WndProc_OnCreate
// -----------------------------------------------------
BOOL WndProc_OnCreate(HWND hWnd, 
                      LPCREATESTRUCT lpCreateStruct)
{
  // Сбрасываем флаг завершения задач
  fTerminate = FALSE;

  // Запускаем три задачи, сохраняя их идентификаторы
  // в массиве
  hThreads[0] = (HANDLE)_beginthread(PaintEllipse, 
    0, (void*)hWnd);
  hThreads[1] = (HANDLE)_beginthread(PaintRect, 
    0, (void*)hWnd);
  hThreads[2] = (HANDLE)_beginthread(PaintText, 
    0, (void*)hWnd);

  return TRUE;
}

// -----------------------------------------------------
// Функция WndProc_OnDestroy
// -----------------------------------------------------
#pragma warning(disable: 4098)
void WndProc_OnDestroy(HWND hWnd)
{
  // Устанавливаем флаг завершения задач
  fTerminate = TRUE;

  // Дожидаемся завершения всех трех задач
  WaitForMultipleObjects(3, hThreads, TRUE, INFINITE);
  
  // Перед завершением освобождаем идентификатор 
  // объекта Mutex
  CloseHandle(hMutex);
  
  // Останавливаем цикл обработки сообщений, расположенный 
  // в главной задаче
  PostQuitMessage(0);
  return 0L;
}

// -----------------------------------------------------
// Функция WndProc_OnPaint
// -----------------------------------------------------
#pragma warning(disable: 4098)
void WndProc_OnPaint(HWND hWnd)
{
  HDC hdc;
  PAINTSTRUCT ps;
  RECT rc;
  DWORD dwRetCode;

  // Ожидаем, пока объект Mutex не перейдет в
  // отмеченное состояние
  dwRetCode = WaitForSingleObject(hMutex, INFINITE);
  
  // Если не было ошибок, выполняем рисование
  if(dwRetCode == WAIT_OBJECT_0)
  {
    // Перерисовываем внутреннюю область окна
    hdc = BeginPaint(hWnd, &ps);
    GetClientRect(hWnd, &rc);
    DrawText(hdc, "SDI Window", -1, &rc,
      DT_SINGLELINE | DT_CENTER | DT_VCENTER);
    EndPaint(hWnd, &ps);

    // Переводим объект Mutex в неотмеченное состояние
    ReleaseMutex(hMutex);
  }

  return 0;
}
// -----------------------------------------------------
// Функция WndProc_OnCommand
// -----------------------------------------------------
#pragma warning(disable: 4098)
void WndProc_OnCommand(HWND hWnd, int id, 
  HWND hwndCtl, UINT codeNotify)
{
  switch (id)
  {
    case ID_FILE_EXIT:  
    {
      // Завершаем работу приложения
      PostQuitMessage(0);
      return 0L;
        break;
    }
	  
    case ID_HELP_ABOUT:
    {
      MessageBox(hWnd, 
        "Multithread SDI Application with Mutex\n"
        "(C) Alexandr Frolov, 1996\n"
        "Email: frolov@glas.apc.org",
        szAppTitle, MB_OK | MB_ICONINFORMATION);
	     return 0L;
	     break;
    }
    default:
      break;
  }
  return FORWARD_WM_COMMAND(hWnd, id, hwndCtl, codeNotify,
    DefWindowProc);
}

// -----------------------------------------------------
// Функция задачи PaintEllipse
// -----------------------------------------------------
void PaintEllipse(void *hwnd)
{
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;
  HBRUSH hBrush, hOldBrush;
  DWORD dwRetCode;

  srand((unsigned int)hwnd);
  while(!fTerminate) 
  {
    // Ожидаем, пока объект Mutex не перейдет в
    // отмеченное состояние
    dwRetCode = WaitForSingleObject(hMutex, INFINITE);
    
    // Если не было ошибок, выполняем рисование
    if(dwRetCode == WAIT_OBJECT_0)
    {
      hDC = GetDC(hwnd);

      nRed   = rand() % 255;
      nGreen = rand() % 255;
      nBlue  = rand() % 255;
    
      GetWindowRect(hwnd, &rect);
    
      xLeft   = rand() % (rect.left   + 1);
      xRight  = rand() % (rect.right  + 1);
      yTop    = rand() % (rect.top    + 1);
      yBottom = rand() % (rect.bottom + 1);

      hBrush = CreateSolidBrush(RGB(nRed, nGreen, nBlue));
      hOldBrush = SelectObject(hDC, hBrush);
      Ellipse(hDC, min(xLeft, xRight), min(yTop, yBottom),
                   max(xLeft, xRight), max(yTop, yBottom));
      SelectObject(hDC, hOldBrush);
      DeleteObject(hBrush);
      ReleaseDC(hwnd, hDC);

      ReleaseMutex(hMutex);
    }
    // Если ожидание было отменено или произошла ошибка,
    // прерываем цикл
    else
    {
      break;
    }
    Sleep(100);
  }
}

// -----------------------------------------------------
// Функция задачи PaintRect
// -----------------------------------------------------
void PaintRect(void *hwnd)
{
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;
  HBRUSH hBrush, hOldBrush;
  DWORD dwRetCode;

  srand((unsigned int)hwnd + 1);
  while(!fTerminate) 
  {
    dwRetCode = WaitForSingleObject(hMutex, INFINITE);
    if(dwRetCode == WAIT_OBJECT_0)
    {
      hDC = GetDC(hwnd);
      nRed   = rand() % 255;
      nGreen = rand() % 255;
      nBlue  = rand() % 255;
      GetWindowRect(hwnd, &rect);
      xLeft   = rand() % (rect.left   + 1);
      xRight  = rand() % (rect.right  + 1);
      yTop    = rand() % (rect.top    + 1);
      yBottom = rand() % (rect.bottom + 1);
      hBrush = CreateSolidBrush(RGB(nRed, nGreen, nBlue));
      hOldBrush = SelectObject(hDC, hBrush);
      Rectangle(hDC, min(xLeft, xRight), min(yTop, yBottom),
                   max(xLeft, xRight), max(yTop, yBottom));
      SelectObject(hDC, hOldBrush);
      DeleteObject(hBrush);
      ReleaseDC(hwnd, hDC);
      ReleaseMutex(hMutex);
    }
    else
    {
      break;
    }
    Sleep(100);
  }
}

// -----------------------------------------------------
// Функция задачи PaintText
// -----------------------------------------------------
void PaintText(void *hwnd)
{
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;
  DWORD dwRetCode;

  srand((unsigned int)hwnd + 2);
  while(!fTerminate) 
  {
    dwRetCode = WaitForSingleObject(hMutex, INFINITE);
    if(dwRetCode == WAIT_OBJECT_0)
    {
      hDC = GetDC(hwnd);
      GetWindowRect(hwnd, &rect);
      xLeft   = rand() % (rect.left   + 1);
      xRight  = rand() % (rect.right  + 1);
      yTop    = rand() % (rect.top    + 1);
      yBottom = rand() % (rect.bottom + 1);
      nRed   = rand() % 255;
      nGreen = rand() % 255;
      nBlue  = rand() % 255;
      SetTextColor(hDC, RGB(nRed, nGreen, nBlue));
      nRed   = rand() % 255;
      nGreen = rand() % 255;
      nBlue  = rand() % 255;
      SetBkColor(hDC, RGB(nRed, nGreen, nBlue));
      TextOut(hDC, xRight - xLeft,
        yBottom - yTop,"TEXT", 4);
      ReleaseDC(hwnd, hDC);
      ReleaseMutex(hMutex);
    }
    else
    {
      break;
    }
    Sleep(100);
  }
}

В области глобальных переменных определен массив szMutexName, в котором хранится имя объекта Mutex, используемого нашим приложением. Идентификатор этого объекта после его создания будет записан в глобальную переменную hMutex.

Функция WinMain создает объект Mutex, вызывая для этого функцию CreateMutex, как это показано ниже:


hMutex = CreateMutex(NULL, FALSE, szMutexName);
if(hMutex == NULL)
{
 ...
  return 0l;
}

В качестве атрибутов защиты передается значение NULL. Так как второй параметр функции CreateMutex XE "CreateMutex" равен FALSE, объект Mutex после создания будет находиться в отмеченном состоянии.

Если при создании объекта функция CreateMutex XE "CreateMutex" не вернула признак ошибки (значение NULL), приложение проверяет, действительно ли был создан новый объект Mutex или же функция вернула идентификатор для уже существующего в системе объекта с таким же именем. Проверка выполняется с помощью функции GetLastError XE "GetLastError" :


if(GetLastError() == ERROR_ALREADY_EXISTS)
{
  MessageBox(NULL, "MutexSDI already started",
      szAppTitle, MB_OK | MB_ICONEXCLAMATION);
  return 0l;
}

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

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

Главная задача выполняет рисование при обработке сообщения WM_PAINT. Обработкой этого сообщения занимается функция WndProc_OnPaint.

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


dwRetCode = WaitForSingleObject(hMutex, INFINITE);

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


ReleaseMutex(hMutex);

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

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


while(!fTerminate) 
{
  dwRetCode = WaitForSingleObject(hMutex, INFINITE);
  if(dwRetCode == WAIT_OBJECT_0)
  {
    hDC = GetDC(hwnd);
    ...
    Ellipse(hDC, min(xLeft, xRight), min(yTop, yBottom),
                 max(xLeft, xRight), max(yTop, yBottom));
    ...
    ReleaseDC(hwnd, hDC);
    ReleaseMutex(hMutex);
  }
  else
    break;
  Sleep(100);
}

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

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

Кратко перечислим другие файлы, имеющие отношение к приложению MutexSDI.

В файле mutexsdi.h (листинг 4.4) находятся прототипы функций, определенных в приложении.

Листинг 4.4. Файл mutexsdi/mutexsdi.h


LRESULT WINAPI
WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
BOOL WndProc_OnCreate(HWND hWnd, 
  LPCREATESTRUCT lpCreateStruct);
void WndProc_OnDestroy(HWND hWnd);
void WndProc_OnPaint(HWND hWnd);
void WndProc_OnCommand(HWND hWnd, int id, 
  HWND hwndCtl, UINT codeNotify);

void PaintEllipse(void *hwnd);
void PaintRect(void *hwnd);
void PaintText(void *hwnd);

Файл resource.h (листинг 4.5) создается автоматически и содержит определения констант для файла ресурсов приложения.

Листинг 4.5. Файл mutexsdi/resource.h


//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by MutexSDI.RC
//
#define IDR_APPMENU                     102
#define IDI_APPICON                     103
#define IDI_APPICONSM                   104
#define ID_FILE_EXIT                    40001
#define ID_HELP_ABOUT                   40003
#define ID_FORMAT_BOLD                  40010
#define ID_FORMAT_ITALIC                40011
#define ID_FORMAT_UNDERLINE             40012
#define ID_FORMAT_PARAGRAPH_LEFT        40014
#define ID_FORMAT_PARAGRAPH_RIGHT       40015
#define ID_FORMAT_PARAGRAPH_CENTER      40016
#define ID_EDIT_DELETE                  40021
#define ID_FILE_SAVEAS                  40024
#define ID_EDIT_SELECTALL               40028
#define ID_SETPROTECTION_PAGENOACCESS   40035
#define ID_SETPROTECTION_PAGEREADONLY   40036
#define ID_SETPROTECTION_PAGEREADWRITE  40037
#define ID_SETPROTECTION_PAGEGUARD      40038
#define ID_MEMORY_READ                  40039
#define ID_MEMORY_WRITE                 40040
#define ID_MEMORY_LOCK                  40041
#define ID_MEMORY_UNLOCK                40042

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        121
#define _APS_NEXT_COMMAND_VALUE         40043
#define _APS_NEXT_CONTROL_VALUE         1000
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

И, наконец, файл ресурсов приложения mutexsdi.rc приведен в листинге 4.6.

Листинг 4.6. Файл mutexsdi/mutexsdi.rc


//Microsoft Developer Studio generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
//////////////////////////////////////////////////////////////
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"
//////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

//////////////////////////////////////////////////////////////
// English (U.S.) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

//////////////////////////////////////////////////////////////
// Menu
//

IDR_APPMENU MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "E&xit",                       ID_FILE_EXIT
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&About...",                   ID_HELP_ABOUT
    END
END

#ifdef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED

//////////////////////////////////////////////////////////////
// Icon
//

// Icon with lowest ID value placed first to ensure 
// application icon
// remains consistent on all systems.
IDI_APPICON             ICON    DISCARDABLE     "mutexsdi.ico"
IDI_APPICONSM           ICON    DISCARDABLE     "mutexssm.ico"

//////////////////////////////////////////////////////////////
// String Table
//

STRINGTABLE DISCARDABLE 
BEGIN
    ID_FILE_EXIT            "Quits the application"
END

#endif    // English (U.S.) resources
//////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
// Generated from the TEXTINCLUDE 3 resource.
//
//////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

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

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

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

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

Поясним это на примере.

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

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

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

Если теперь в нашем новом варианте приложения пользователь закроет MDI-окно, в котором выполняется рисование, то одно из двух “спящих” MDI-окон должно “проснутся”. При этом в нем начнется процесс рисования эллипсов. Если закрыть еще одно активное окно, процесс рисования должен начаться во втором “спящем” окне.

Таким образом, сколько бы MDI-окон не создал пользователь, эллипсы будут отображаться только в трех из них, так как только три задачи будут работать, а остальные будут находиться в состоянии ожидания.

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

Как работает семафор

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

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

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

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

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

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

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

Как долго продлится ожидание?

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

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

Рис. 4.4. Изменение значения счетчика семафора от трех до единицы

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

Рис. 4.5. После запуска четвертой задачи семафор закрывается

Функции для работы с семафорами

Рассмотрим функции программного интерфейса операционной системы Microsoft Windows NT, предназначенные для работы с семафорами.

Создание семафора

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


HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // атрибуты
                                               // защиты 
  LONG lInitialCount,  // начальное значение счетчика семафора 
  LONG lMaximumCount,  // максимальное значение 
                       // счетчика семафора 
  LPCTSTR  lpName);     // адрес строки с именем семафора 

Этой функцией пользоваться достаточно просто.

В качестве атрибутов защиты вы можете передать значение NULL.

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

Имя семафора указывается аналогично имени рассмотренного нами ранее объекта Mutex с помощью параметра lpName.

В случае удачного создания семафора функция CreateSemaphore возвращает его идентификатор. В случае возникновения ошибки возвращается значение NULL, при этом код ошибки можно узнать при помощи функции GetLastError XE "GetLastError".

Так как имена семафоров доступны всем приложениям в системе, возможно возникновение ситуации, когда приложение пытается создать семафор с уже использованным именем. При этом новый семафор не создается, а приложение получает идентификатор для уже существующего семафора. Если возниклав такая ситуация, функция GetLastError XE "GetLastError", вызванная сразу после функции CreateSemaphore XE "CreateSemaphore", возвращает значение ERROR_ALREADY_EXISTS XE "ERROR_ALREADY_EXISTS".

Уничтожение семафора

Для уничтожения семафора вы должны передать его идентификатор функции CloseHandle. Заметим, что при завершении процесса все созданные им семафоры уничтожаются автоматически.

Открывание семафора

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

Существующий семафор можно открыть функцией OpenSemaphore XE "OpenSemaphore", прототип которой приведен ниже:


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

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

Значение Описание
SEMAPHORE_ALL_ACCESS Указаны все возможные флаги доступа
SEMAPHORE_MODIFY_STATE Возможно изменение значение счетчика семафора функцией ReleaseSemaphore XE "ReleaseSemaphore"
SYNCHRONIZE Полученный идентификатор можно будет использовать в любых функциях ожидания события

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

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

Если семафор открыт успешно, функция OpenSemaphore возвращает его идентификатор. При ошибке возвращается значение NULL. Код ошибки вы можете определить при помощи фукнции GetLastError XE "GetLastError".

Увеличение значения счетчика семафора

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


BOOL ReleaseSemaphore(
  HANDLE hSemaphore,        // идентификатор семафора 
  LONG   cReleaseCount,     // значение инкремента 
  LPLONG lplPreviousCount); // адрес переменной для записи 
                   // предыдущего значения счетчика семафора

Функция ReleaseSemaphore увеличивает значение счетчика семафора, идентификатор которого передается ей через параметр hSemaphore, на значение, указанное в параметре cReleaseCount.

Заметим, что через параметр cReleaseCount вы можете передавать только положительное значение, большее нуля. При этом если в результате увеличения новое значение счетчика должно будет превысить максимальное значение, заданное при создании семафора, функция ReleaseSemaphore XE "ReleaseSemaphore" возвращает признак ошибки и не изменяет значение счетчика.

Предыдущее значение счетчика, которое было до использования функции ReleaseSemaphore, записывается в переменную типа LONG. Адрес этой переменной передается функции через параметр lplPreviousCount.

Если функция ReleaseSemaphore завершилась успешно, она возвращает значение TRUE. При ошибке возвращается значение FALSE. Код ошибки в этом случае можно определить, как обычно, при помощи функции GetLastError XE "GetLastError".

Функция используется обычно для решения двух задач.

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

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

Уменьшение значения счетчика семафора

В программном интерфейсе операционной системы Microsoft Windows NT нет фукнции, специально предназначенной для уменьшения значения счетчика семафора. Этот счетчик уменьшается, когда задача вызывает функции ожидания, такие как WaitForSingleObject XE "WaitForSingleObject" или WaitForMultipleObject. Если задача вызывает несколько раз функцию ожидания для одного и того же семафора, содержимое его счетчика каждый раз будет уменьшаться.

Определение текущего значения счетчика семафора

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

Заметим, что в операционной системе Microsoft Windows NT не предусмотрено средств, с помощью которых можно было бы определить текущее значение семафора, не изменяя его. В частности, вы не можете задать функции ReleaseSemaphore XE "ReleaseSemaphore" нулевое значение инкремента, так как в этом случае указанная функция просто вернет соответствующий код ошибки.

Приложение SEMMDI

Приложение SEMMDI создано на базе приложения MultiMDI и демонстрирует использование семфафоров для ограничения количества работающих задач, запущенных для MDI-окон.

На рис. 4.6 показано главное окно приложения SEMMDI. Мы запустили это приложение в среде операционной системы Microsoft Windows 95, так как она также является мультизадачной операционной системой и может работать с семафорами и другими объектами синхронизации. Разумеется, что в среде Microsoft Windows NT приложение SEMMDI также будет работать.

Рис. 4.6. Главное окно приложения SEMMDI, запущенного в среде операционной системы Microsoft Windows 95

Выбирая из меню File строку New, вы можете создавать новые MDI-окна. В первых двух созданных вами окнах начнется процесс рисования эллипсов случайных размеров и цвета. В остальных окнах отображается только строка Waiting.

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

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

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

Исходные тексты приложения

Главный файл исходных текстов приложения SEMMDI представлен в листинге 4.7. Заметим, что для сборки проекта необходимо использовать мультизадачный вариант библиотеки времени выполнения.

Листинг 4.7. Файл semmdi/semmdi.c


#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <stdio.h>
#include "resource.h"
#include "afxres.h"
#include "semmdi.h"

// Имена классов окна
char const szFrameClassName[] = "MDISemAppClass";
char const szChildClassName[] = "MDISemChildAppClass";

// Заголовок окна
char const szWindowTitle[] = 
  "Multithread MDI Application with Semaphores";

HINSTANCE hInst;

HWND hwndFrame;  // окно Frame Window
HWND hwndClient; // окно Client Window
HWND hwndChild;  // окно Child Window

// Структура, которая создается для каждого дочернего окна
typedef struct _CHILD_WINDOW_TAG
{
  // Признак активности задачи
  BOOL fActive;

  // Флаг ожидания семафора
  BOOL fWaiting;
  
  // Критическая секция для рисования в окне
  CRITICAL_SECTION csChildWindowPaint; 

  // Идентификатор задачи
  HANDLE hThread;
} CHILD_WINDOW_TAG;

typedef CHILD_WINDOW_TAG *LPCHILD_WINDOW_TAG;

// Семафор для ограничения количества работающих задач
HANDLE hSemaphore;

// Имя семафора
char const lpSemaphoreName[] =
 "$MyVerySpecialSemaphoreName$For$SemMDI$Application$";

// =====================================
// Функция WinMain
// =====================================
int APIENTRY 
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
{
  MSG  msg;          // структура для работы с сообщениями
  hInst = hInstance; // сохраняем идентификатор приложения

  // Инициализируем приложение
  if(!InitApp(hInstance))
    return FALSE;

  // Создаем семафор для ограничения количества
  // задач, работающих одновременно
  hSemaphore = CreateSemaphore(NULL, 2, 2, lpSemaphoreName);
  if(hSemaphore == NULL)
  {
    MessageBox(NULL,
      "Ошибка при создании семафора",
       szWindowTitle, MB_OK | MB_ICONEXCLAMATION);
    return FALSE;
  }
  
  // Создаем главное окно приложения - Frame Window
  hwndFrame = CreateWindow(
    szFrameClassName,    // имя класса окна
    szWindowTitle,       // заголовок окна
    WS_OVERLAPPEDWINDOW, // стиль окна
    CW_USEDEFAULT, 0,    // задаем размеры и расположение
    CW_USEDEFAULT, 0,    // окна, принятые по умолчанию
    0,                   // идентификатор родительского окна
    0,                   // идентификатор меню
    hInstance,           // идентификатор приложения
    NULL);   // указатель на дополнительные параметры

  // Если создать окно не удалось, завершаем приложение
  if(!hwndFrame)
    return FALSE;

  // Рисуем главное окно
  ShowWindow(hwndFrame, nCmdShow);
  UpdateWindow(hwndFrame);

  // Запускаем цикл обработки сообщений
  while(GetMessage(&msg, NULL, 0, 0))
  {
    // Трансляция для MDI-приложения
    if(!TranslateMDISysAccel(hwndClient, &msg))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }
  
  // Освобождаем идентификатор семафора
  CloseHandle(hSemaphore);
  return msg.wParam;
}

// =====================================
// Функция InitApp
// Выполняет регистрацию класса окна
// =====================================

BOOL InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации

  // Регистрируем класс для главного окна приложения
  // (для окна Frame Window)
  memset(&wc, 0, sizeof(wc));
  wc.lpszMenuName  = "APP_MENU";
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC)FrameWndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(hInstance, "APP_ICON");
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
  wc.lpszClassName = (LPSTR)szFrameClassName;
  aWndClass = RegisterClass(&wc);

  if(!aWndClass)
    return FALSE;

  // Регистрируем класс окна для 
  //дочернего окна Document Window
  memset(&wc, 0, sizeof(wc));
  wc.lpszMenuName  = 0;
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC)ChildWndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(hInstance, "APPCLIENT_ICON");
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszClassName = (LPSTR)szChildClassName;
  aWndClass = RegisterClass(&wc);

  if(!aWndClass)
    return FALSE;

  return TRUE;
}

// =====================================
// Функция FrameWndProc
// =====================================

LRESULT CALLBACK 
FrameWndProc(HWND hwnd, UINT msg, 
             WPARAM wParam, LPARAM lParam)
{
  HWND   hwndChild;
  HANDLE hThread;
  DWORD  dwIDThread;
  CHAR   szBuf[255];
  LPCHILD_WINDOW_TAG lpMyWndTag;
  
  // Структура для создания окна Client Window
  CLIENTCREATESTRUCT clcs;

  // Указатель на структуру для хранения 
  // состояния дочернего окна
  LPCHILD_WINDOW_TAG lpTag;

  switch (msg)
  {
    // При создании окна Frame Window создаем
    // окно Client Window, внутри которого будут создаваться
    // дочерние окна Document Window
    case WM_CREATE:
    {
      clcs.hWindowMenu = GetSubMenu(GetMenu(hwnd), 2);
      clcs.idFirstChild = 500;
      
      // Создаем окно Client Window 
      hwndClient = CreateWindow(
        "MDICLIENT",    // имя класса окна
        NULL,           // заголовок окна
        WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | 
          WS_HSCROLL | WS_VSCROLL,  
        0, 0, 0, 0,
        hwnd,         // идентификатор родительского окна
        (HMENU)1,     // идентификатор меню
        hInst,        // идентификатор приложения
        (LPSTR)&clcs);// указатель на дополнительные параметры

      break;
    }

    // Обработка сообщений от главного меню приложения
    case WM_COMMAND:
    {
      switch (wParam)
      {
        // Создание нового окна Document Window
        case CM_FILENEW:
        {
          hwndChild = CreateMDIWindow(
            (LPSTR)szChildClassName,    // класс окна
            "MDI Child Window",         // заголовок окна
            0,                      // дополнительные стили
            CW_USEDEFAULT, CW_USEDEFAULT, // размеры окна 
            CW_USEDEFAULT, CW_USEDEFAULT, //  Document Window
            hwndClient,
            hInst,           // идентификатор приложения
            0);              // произвольное значение

          // Получаем память для структуры, в которой будет
          // хранится состояние окна
          lpTag = malloc(sizeof(CHILD_WINDOW_TAG));
  
          // Устанавливаем признак активности
          lpTag->fActive = 1;

          // Инициализируем критическую секцию
          InitializeCriticalSection(
             &(lpTag->csChildWindowPaint));
          
          // Устанавливаем адрес структуры состояния в
          // памяти окна
          SetWindowLong(hwndChild, GWL_USERDATA, (LONG)lpTag);

          // Создаем задачу для дочернего окна
          hThread = CreateThread(NULL, 0, 
            (LPTHREAD_START_ROUTINE)ThreadRoutine,
            (LPVOID)hwndChild, 0,(LPDWORD)&dwIDThread);

          if(hThread == NULL)
          {
            MessageBox(hwnd,"Ошибка при создании задачи",
	           szWindowTitle, MB_OK | MB_ICONEXCLAMATION);
          }

          // Сохраняем идентификатор созданной задачи
          lpTag->hThread = hThread;
          
          // Отображаем идентификатор задачи в заголовке
          // дочернего окна
          sprintf(szBuf, "Thread ID = %lX", dwIDThread);
          SetWindowText(hwndChild, szBuf);
   
          break;
        }

        // Увеличиваем содержимое счетчика семафора
        // на единицу
        case ID_SEMAPHORE_INCREMENT:
        {
          ReleaseSemaphore(hSemaphore, 1, NULL);
          break;
        }
        
        // Размещение окон Document Window рядом друг с другом
        case CM_WINDOWTILE:
        {
          SendMessage(hwndClient, WM_MDITILE, 0, 0);
          break;
        }

        // Размещение окон Document Window с перекрытием
        case CM_WINDOWCASCADE:
        {
          SendMessage(hwndClient, WM_MDICASCADE, 0, 0);
          break;
        }

        // Размещение пиктограм минимизированых окон 
        // Document Window в нижней части окна Client Window
        case CM_WINDOWICONS:
        {
          SendMessage(hwndClient, WM_MDIICONARRANGE, 0, 0);
          break;
        }

        // Уничтожение всех окон Document Window
        case CM_WINDOWCLOSEALL:
        {
          HWND hwndTemp;

          ShowWindow(hwndClient, SW_HIDE);

          while(TRUE) 
          {
            // Получаем идентификатор дочернего окна 
            // для окна Client Window
            hwndTemp = GetWindow(hwndClient, GW_CHILD);
            // Если дочерних окон больше нет, выходим из цикла
            if(!hwndTemp)
              break;

            // Пропускаем окна-заголовки
            while(hwndTemp && GetWindow(hwndTemp, GW_OWNER))
              hwndTemp = GetWindow(hwndTemp, GW_HWNDNEXT);

            // Удаляем дочернее окно Document Window
            if(hwndTemp)
            {
              // Завершаем задачу, запущенную для окна
              lpMyWndTag = 
                 (LPCHILD_WINDOW_TAG)GetWindowLong(
                 hwndTemp, GWL_USERDATA);
              lpMyWndTag->fActive = 0;  

              // Посылаем сообщение, в ответ на которое
              // окно будет удалено
              SendMessage(hwndClient, WM_MDIDESTROY, 
                (WPARAM)hwndTemp, 0);
            }
            else
              break;
          }

          // Отображаем окно Client Window
          ShowWindow(hwndClient, SW_SHOW);

          break;
        }

        // Устанавливаем классы приоритета процесса
        case ID_PRIORITYCLASS_REALTIME:
        {
          SetPriorityClass(GetCurrentProcess(),
            REALTIME_PRIORITY_CLASS);
          break;
        }
        case ID_PRIORITYCLASS_HIGH:
        {
          SetPriorityClass(GetCurrentProcess(),
            HIGH_PRIORITY_CLASS);
          break;
        }
        case ID_PRIORITYCLASS_NORMAL:
        {
          SetPriorityClass(GetCurrentProcess(),
            NORMAL_PRIORITY_CLASS);
          break;
        }
        case ID_PRIORITYCLASS_IDLE:
        {
          SetPriorityClass(GetCurrentProcess(),
            IDLE_PRIORITY_CLASS);
          break;
        }

        case CM_HELPABOUT:
        {
          MessageBox(hwnd,
            "Демонстрация использования мультизадачности\n"
            "в MDI-приложениях\n"
            "(C) Alexandr Frolov, 1996\n"
            "Email: frolov@glas.apc.org",
            szWindowTitle, MB_OK | MB_ICONINFORMATION);
          break;
        }

        // Завершаем работу приложения
        case CM_FILEEXIT:
        {
          DestroyWindow(hwnd);
          break;
        }

        default:
          break;
      }

      // Определяем идентификатор активного окна 
      // Document Window
      hwndChild =
        (HWND)LOWORD(SendMessage(hwndClient, 
        WM_MDIGETACTIVE, 0, 0l));

      // Если это окно, посылаем ему сообщение WM_COMMAND
      if(IsWindow(hwndChild))
        SendMessage(hwndChild, WM_COMMAND, wParam, lParam);

      return DefFrameProc(
        hwnd, hwndClient, msg, wParam, lParam);
    }

    case WM_DESTROY:
    {
      PostQuitMessage(0);
      break;
    }

    default:
      break;
  }
  return DefFrameProc(hwnd, hwndClient, msg, wParam, lParam);
}

// =====================================
// Функция ChildWndProc
// =====================================

LRESULT CALLBACK
ChildWndProc(HWND hwnd, UINT msg, 
             WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  RECT rc;
  LPCHILD_WINDOW_TAG lpMyWndTag;
  HMENU hmenuPopup;
  POINT pt;
  CHAR szBuf[256];

  switch (msg)
  {
    case WM_PAINT:
    {
      // Получаем адрес структуры состояния окна
      lpMyWndTag = 
        (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);

      // Входим в критическую секцию
      EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));

      // Перерисовываем внутреннюю область дочернего окна
      hdc = BeginPaint(hwnd, &ps);
      GetClientRect(hwnd, &rc);

      if(lpMyWndTag->fWaiting)
        DrawText(hdc, "Waiting...", -1, &rc,
          DT_SINGLELINE | DT_CENTER | DT_VCENTER);
      else
        DrawText(hdc, "Child Window. Running", -1, &rc,
          DT_SINGLELINE | DT_CENTER | DT_VCENTER);

      EndPaint(hwnd, &ps);

      // Выходим из критической секции
      LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));
      break;
    }

    case WM_CLOSE:
    {
      // Сбрасываем признак активности задачи
      lpMyWndTag = 
        (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);

      // Если задача находится в состоянии ожидания 
      // семафора, запрещаем удаление окна
      if(lpMyWndTag->fWaiting)
        return 0;
      
      // Если задача не ожидает семафор, завершаем
      // задачу и разрешаем удаление окна
      else
        lpMyWndTag->fActive = 0;  
      
      break;
    }

    // Когда пользователь нажимает правую кнопку мыши 
    // в дочернем окне, отображаем плавающее меню
    case WM_RBUTTONDOWN:
    {
      pt.x = LOWORD(lParam);
      pt.y = HIWORD(lParam);
      ClientToScreen(hwnd, &pt);

      hmenuPopup = GetSubMenu(
        LoadMenu(hInst, "IDR_POPUPMENU"), 0);
      TrackPopupMenu(hmenuPopup, 
        TPM_CENTERALIGN | TPM_LEFTBUTTON,
        pt.x, pt.y, 0, hwnd, NULL);
      DestroyMenu(hmenuPopup);
      break;
    }

    // Обрабатываем команды, поступающие от плавающего меню
    case WM_COMMAND:
    {
      switch (wParam)
      {
        // Приостановка выполнения задачи
        case ID_THREADCONTROL_SUSPEND:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          
          // Входим в критическую секцию
          EnterCriticalSection(
             &(lpMyWndTag->csChildWindowPaint));

          SuspendThread(lpMyWndTag->hThread);

          // Выходим из критической секции
          LeaveCriticalSection(
            &(lpMyWndTag->csChildWindowPaint));

          break;
        }
        
        // Возобновление выполнения задачи
        case ID_THREADCONTROL_RESUME:
        {
   
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          ResumeThread(lpMyWndTag->hThread);
          break;
        }

        // Изменение относительного приоритета
        case ID_THREADCONTROL_PRIORITYLOWEST:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          SetThreadPriority(lpMyWndTag->hThread, 
            THREAD_PRIORITY_LOWEST);
          break;
        }
        case ID_THREADCONTROL_PRIORITYNORMAL:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          SetThreadPriority(lpMyWndTag->hThread, 
            THREAD_PRIORITY_NORMAL);
          break;
        }
        case ID_THREADCONTROL_PRIORITYHIGHEST:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          SetThreadPriority(lpMyWndTag->hThread, 
            THREAD_PRIORITY_HIGHEST);
          break;
        }

        // Определение и отображение относительного приоритета
        case ID_THREADCONTROL_GETPRIORITY:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          
          strcpy(szBuf, "Thread priority: ");
          switch (GetThreadPriority(lpMyWndTag->hThread))
          {
            case THREAD_PRIORITY_LOWEST:
            {
              strcat(szBuf, "THREAD_PRIORITY_LOWEST");
              break;
            }
            case THREAD_PRIORITY_BELOW_NORMAL:
            {
              strcat(szBuf, "THREAD_PRIORITY_BELOW_NORMAL");
              break;
            }
            case THREAD_PRIORITY_NORMAL:
            {
              strcat(szBuf, "THREAD_PRIORITY_NORMAL");
              break;
            }
            case THREAD_PRIORITY_ABOVE_NORMAL:
            {
              strcat(szBuf, "THREAD_PRIORITY_ABOVE_NORMAL");
              break;
            }
            case THREAD_PRIORITY_HIGHEST:
            {
              strcat(szBuf, "THREAD_PRIORITY_HIGHEST");
              break;
            }
          }

	   MessageBox(hwnd, szBuf,
            szWindowTitle, MB_OK | MB_ICONINFORMATION);
          break;
        }

        // Удаление задачи
        case ID_THREADCONTROL_KILLTHREAD:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
             hwnd, GWL_USERDATA);
          TerminateThread(lpMyWndTag->hThread, 5);
          break;
        }
        
        // Уничтожение дочернего окна
        case ID_THREADCONTROL_CLOSEWINDOW:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);

          // Если задача не находится в состоянии ожидания
          // семафора, удаляем окно и завершаем работу 
          // задачи, запущенной для него
          if(!lpMyWndTag->fWaiting)
          {
            SendMessage(hwndClient, WM_MDIDESTROY, 
                (WPARAM)hwnd, 0);

            lpMyWndTag->fActive = 0;  
          }
          break;
        }
        default:
          break;
      }
      break;
    }
    default:
      break;
  }
  return DefMDIChildProc(hwnd, msg, wParam, lParam);
}

// =====================================
// Функция ThreadRoutine
// Задача, которая выполняется для каждого
// дочернего окна
// =====================================

DWORD ThreadRoutine(HWND hwnd) 
{
  LONG lThreadWorking = 1L;
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;
  HBRUSH hBrush, hOldBrush;
  LPCHILD_WINDOW_TAG lpMyWndTag;

  // Получаем указатель на структуру параметров окна 
  lpMyWndTag = 
    (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);

  // Если произошла ошибка, завершаем работу задачи
  if(lpMyWndTag == NULL)
  {
    return 0;
  }

  // Устанавливаем флаг ожидания семафора
  lpMyWndTag->fWaiting = TRUE;

  // Выполняем ожидание семафора. Если оно завершилось
  // с ошибкой, завершаем работу задачи
  if(WAIT_FAILED == 
    WaitForSingleObject(hSemaphore, INFINITE))
  {
    return 0;
  }

  // Сбрасываем флаг ожидания семафора
  lpMyWndTag->fWaiting = FALSE;

  srand((unsigned int)hwnd);
  
  while(TRUE) 
  {
    if(!lpMyWndTag->fActive)
      break;

    EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));

    hDC = GetDC(hwnd);
    nRed   = rand() % 255;
    nGreen = rand() % 255;
    nBlue  = rand() % 255;
    
    GetWindowRect(hwnd, &rect);
    xLeft   = rand() % (rect.left   + 1);
    xRight  = rand() % (rect.right  + 1);
    yTop    = rand() % (rect.top    + 1);
    yBottom = rand() % (rect.bottom + 1);

    hBrush = CreateSolidBrush(RGB(nRed, nGreen, nBlue));
    hOldBrush = SelectObject(hDC, hBrush);

    Ellipse(hDC, min(xLeft, xRight), min(yTop, yBottom),
                 max(xLeft, xRight), max(yTop, yBottom));

    SelectObject(hDC, hOldBrush);
    DeleteObject(hBrush);
    ReleaseDC(hwnd, hDC);
    
    LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));
    Sleep(1);
  }

  DeleteCriticalSection(&(lpMyWndTag->csChildWindowPaint));
  free(lpMyWndTag);
  
  // Перед завершением работы задачи увеличиваем
  // на единицу счетчик семафора, разрешая работу
  // других задач, находящихся в состоянии ожидания
  if(hSemaphore != NULL)
    ReleaseSemaphore(hSemaphore, 1, NULL);

  return 0;
}

Файл semmdi.h (листинг 4.8) содержит прототипы функций, определенных в приложении SEMMDI.

Листинг 4.8. Файл semmdi/semmdi.h


// Прототипы функций
BOOL InitApp(HINSTANCE);
LRESULT CALLBACK FrameWndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK ChildWndProc(HWND, UINT, WPARAM, LPARAM);
DWORD ThreadRoutine(HWND hwnd);
VOID AddThreadToList(HANDLE hThread);

Файл resource.h (листинг 4.9), создаваемый автоматически, содержит определение констант для ресурсов приложения SEMMDI.

Листинг 4.9. Файл semmdi/resource.h


//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by Mdiapp.rc
//
#define CM_HELPABOUT                    100
#define CM_FILEEXIT                     101
#define CM_FILENEW                      102
#define CM_WINDOWTILE                   103
#define CM_WINDOWCASCADE                104
#define CM_WINDOWICONS                  105
#define CM_WINDOWCLOSEALL               106
#define ID_THREADCONTROL_SUSPEND        40001
#define ID_THREADCONTROL_RESUME         40002
#define ID_THREADCONTROL_PRIORITYLOWEST 40003
#define ID_THREADCONTROL_PRIORITYNORMAL 40004
#define ID_THREADCONTROL_PRIORITYHIGHEST 40005
#define ID_THREADCONTROL_GETPRIORITY    40006
#define ID_THREADCONTROL_KILLTHREAD     40007
#define ID_THREADCONTROL_CLOSEWINDOW    40008
#define ID_PRIORITYCLASS_REALTIME       40009
#define ID_PRIORITYCLASS_NORMAL         40010
#define ID_PRIORITYCLASS_HIGH           40011
#define ID_PRIORITYCLASS_IDLE           40012
#define ID_SEMAPHORE_INCREMENT          40013

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC                     1
#define _APS_NEXT_RESOURCE_VALUE        102
#define _APS_NEXT_COMMAND_VALUE         40015
#define _APS_NEXT_CONTROL_VALUE         1000
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

Ресурсы приложения SEMMDI определены в файле mdiapp.rc, который приведен в листинге 4.10.

Листинг 4.10. Файл semmdi/mdiapp.rc


//Microsoft Developer Studio generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
//////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

//////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

//////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

//////////////////////////////////////////////////////////////
//
// Menu
//

APP_MENU MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&New",            CM_FILENEW
        MENUITEM SEPARATOR
        MENUITEM "E&xit",           CM_FILEEXIT
    END
    POPUP "&Semaphore"
    BEGIN
        MENUITEM "&Increment",      ID_SEMAPHORE_INCREMENT
    END
    POPUP "Priority class"
    BEGIN
        MENUITEM "&Realtime",       ID_PRIORITYCLASS_REALTIME
        MENUITEM "&High",           ID_PRIORITYCLASS_HIGH
        MENUITEM "&Normal",         ID_PRIORITYCLASS_NORMAL
        MENUITEM "&Idle",           ID_PRIORITYCLASS_IDLE
    END
    POPUP "&Window"
    BEGIN
        MENUITEM "&Tile",           CM_WINDOWTILE
        MENUITEM "&Cascade",        CM_WINDOWCASCADE
        MENUITEM "Arrange &Icons",  CM_WINDOWICONS
        MENUITEM "Close &All",                  CM_WINDOWCLOSEALL
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&About...",       CM_HELPABOUT
    END
END

IDR_POPUPMENU MENU DISCARDABLE 
BEGIN
    POPUP "&Thread Control"
    BEGIN
        MENUITEM "&Suspend",       ID_THREADCONTROL_SUSPEND
        MENUITEM "&Resume",        ID_THREADCONTROL_RESUME
        MENUITEM SEPARATOR
        MENUITEM "Priority &Lowest", ID_THREADCONTROL_PRIORITYLOWEST

        MENUITEM "Priority &Normal", ID_THREADCONTROL_PRIORITYNORMAL

        MENUITEM "Priority &Highest", ID_THREADCONTROL_PRIORITYHIGHEST

        MENUITEM SEPARATOR
        MENUITEM "&Get Priority", ID_THREADCONTROL_GETPRIORITY
        MENUITEM SEPARATOR
        MENUITEM "&Kill Thread", ID_THREADCONTROL_KILLTHREAD
        MENUITEM "&Close Window", ID_THREADCONTROL_CLOSEWINDOW
    END
END

//////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure 
// application icon
// remains consistent on all systems.
APP_ICON                ICON    DISCARDABLE     "multimdi.ico"
APPCLIENT_ICON          ICON    DISCARDABLE     "mdicl.ico"

#ifdef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED

#endif    // English (U.S.) resources
//////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
//////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

Определения и глобальные переменные

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

Для каждого дочернего MDI-окна в приложении MultiMDI мы создавали структуру типа CHILD_WINDOW_TAG, в которой хранилась такая информация, как признак активности задачи, запущенной для этого окна, критическая секция для рисования в окне, а также идентификатор задачи, запущенной для окна. Создавая приложение SEMMDI, мы добавили в эту структуру поле fWaiting:


typedef struct _CHILD_WINDOW_TAG
{
  BOOL fActive;
  BOOL fWaiting;
  CRITICAL_SECTION csChildWindowPaint; 
  HANDLE hThread;
} CHILD_WINDOW_TAG;
typedef CHILD_WINDOW_TAG *LPCHILD_WINDOW_TAG;

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

Описание функций приложения

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

Функция WinMain

Дополнительно к действиям, выполняемым этой функцией в приложении MultiMDI, новый вариант функции WinMain создает семафор с именем lpSemaphoreName:


hSemaphore = CreateSemaphore(NULL, 2, 2, lpSemaphoreName);
if(hSemaphore == NULL)
{
  MessageBox(NULL, "Ошибка при создании семафора",
    szWindowTitle, MB_OK | MB_ICONEXCLAMATION);
  return FALSE;
}

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

После завершения цикла обработки сообщений наше приложение освобождает идентификатор созданного ранее семафора, вызывая для этого функцию CloseHandle XE "CloseHandle" :


CloseHandle(hSemaphore);

Функция FrameWndProc

Эта функция обрабатывает сообщения для главного окна приложения Frame Window, в частности, сообщения, которые поступают от главного меню приложения.

Обработчик сообщения WM_COMMAND XE "WM_COMMAND" получает управление, когда пользователь выбирает строки главного меню приложения.

Обработка сообщений от меню Semaphore

Если из меню Semaphore выбрать строку Increment, содержимое счетчика семафора будет увеличено на единицу:


case ID_SEMAPHORE_INCREMENT:
{
  ReleaseSemaphore(hSemaphore, 1, NULL);
  break;
}

Для этого используется функция ReleaseSemaphore XE "ReleaseSemaphore".

Обработка сообщений от меню Window

Меню Window в приложении SEMMDI, как и в приложении MultiMDI, предназначено для управления дочерними MDI-окнами. Выбирая строки этого меню, пользователь может упорядочить расположение окон одним из двух способов, упорядочить расположение пиктограмм минимизированных окон, а также закрыть все дочерние MDI-окна.

Функция ChildWndProc

Функция ChildWndProc обрабатывает сообщения, поступающие в дочерние MDI-окна.

Обработка сообщения WM_PAINT

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

Если задача, запущенная для дочернего MDI-окна, находится в состоянии ожидания, в окне отображается строка Waiting, а если эта задача работает - строка Child Window. Running. Для выбора нужной строки обработчик сообщения WM_PAINT XE "WM_PAINT" проверяет содержимое поля fWaiting структуры параметров окна типа CHILD_WINDOW_TAG:


if(lpMyWndTag->fWaiting)
  DrawText(hdc, "Waiting...", -1, &rc,
    DT_SINGLELINE | DT_CENTER | DT_VCENTER);
else
  DrawText(hdc, "Child Window. Running", -1, &rc,
    DT_SINGLELINE | DT_CENTER | DT_VCENTER);

Это сообщение поступает в функцию дочернего окна при уничтожении последнего.

Обработчик сообщения WM_CLOSE проверяет флаг ожидания в поле fWaiting структуры параметров окна типа CHILD_WINDOW_TAG. Если задача не находится в состоянии ожидания, обработчик сбрасывает признак активности задачи, в результате чего задача завершает свою работу:


if(lpMyWndTag->fWaiting)
  return 0;
else
  lpMyWndTag->fActive = 0;  

В том случае, когда задача находится в состоянии ожидания, обработчик сообщения WM_CLOSE XE "WM_CLOSE" возвращает нулевое значение, запрещая удаление окна.

Обработка сообщения WM_COMMAND

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

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


case ID_THREADCONTROL_CLOSEWINDOW:
{
  lpMyWndTag = (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
  if(!lpMyWndTag->fWaiting)
  {
    SendMessage(hwndClient, WM_MDIDESTROY, (WPARAM)hwnd, 0);
    lpMyWndTag->fActive = 0;  
  }
  break;
}

Как видно из приведенного выше фрагмента исходного текста, для определения возможности удаления окна обработчик сообщения WM_COMMAND XE "WM_COMMAND" проверяет флаг ожидания в поле fWaiting структуры параметров окна типа CHILD_WINDOW_TAG.

Функция задачи ThreadRoutine

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

Перед началом своей работы функция задачи ThreadRoutine устанавливает флаг ожидания в поле fWaiting структуры типа CHILD_WINDOW_TAG:


lpMyWndTag->fWaiting = TRUE;

Затем она выполняет ожидание семафора, вызывая для этого функцию WaitForSingleObject XE "WaitForSingleObject" :


if(WAIT_FAILED == WaitForSingleObject(hSemaphore, INFINITE))
{
  return 0;
}

Если пользователь создал не более двух окон, вызов функции WaitForSingleObject XE "WaitForSingleObject" не приведет к задержке в выполнении задачи. Если же окон создано больше, задача перейдет в состояние ожидания.

Когда задача вновь возобновит свою работу (в результате того, что пользователь закроет одно из активных окон), флаг ожидания сбрасывается:


lpMyWndTag->fWaiting = FALSE;