Московский государственный технический университет им. Н.Э. Баумана
Опубликован: 28.06.2006 | Доступ: свободный | Студентов: 12463 / 343 | Оценка: 4.54 / 3.83 | Длительность: 22:03:00
ISBN: 978-5-9556-0055-0
Лекция 14:

Взаимодействие процессов и потоков

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

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

DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMsecs );
DWORD WaitForMultipleObjects(
  DWORD nCount, const HANDLE* lpHandles,
  BOOL bWaitAll, DWORD dwMsecs
);

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

Функция WaitForSingleObject осуществляет ожидание одного объекта, а функция WaitForMultipleObjects может ожидать как освобождения любого из указанных объектов ( bWaitAll = FALSE ), так и всех сразу ( bWaitAll = TRUE ). Ожидание завершается либо по освобождении объекта(ов), либо по истечении указанного интервала времени ( dwMsecs ) в миллисекундах (бесконечное при dwMsecs = INFINITE ). Код возврата функции позволяет определить причину - таймаут, освобождение конкретного объекта либо ошибка.

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

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

Для перехода в ожидание оповещения предусмотрены функции SleepEx, WaitForMultipleObjectsEx, WaitForSingleObjectEx и SignalObjectAndWait.

Еще несколько функций предназначены для разработки GUI-приложений Win32: MsgWaitForMultipleObjects и MsgWaitForMultipleObjectsEx выполняют ожидание указанных объектов и, кроме того, могут контролировать состояние очереди потока, предназначенной для обработки оконных сообщений. Функция WaitForInputIdle ожидает, пока GUI-приложение не закончит свою инициализацию и не перейдет в ожидание в цикле обработки сообщений.

Несколько типов объектов в Win32 предназначены только для взаимной синхронизации потоков: события (event), семафоры (semaphore), объекты исключительного владения (мьютексы, mutex, mutual exclusion) и ожидающие таймеры (waitable timer). Для изменения их состояния предусмотрены специальные функции, так что потоки могут явным образом управлять состоянием таких объектов, обеспечивая взаимную синхронизацию. Эти объекты являются базовыми примитивами, на которых часто строятся более сложные синхронизирующие объекты.

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

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

События

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

В Windows различают события с ручным и с автоматическим сбросом (тип и начальное состояние задаются при создании события функцией CreateEvent ). События с ручным сбросом ведут себя обычным образом: функция SetEvent переводит событие в свободное (сигнальное) состояние, а функция ResetEvent - в занятое (не сигнальное). События с автоматическим сбросом переводятся в занятое состояние либо явным вызовом функции ResetEvent, либо ожидающей функцией WaitFor....

#define DEFAULT_SECURITY (LPSECURITY_ATTRIBUTES)NULL

VOID CALLBACK ApcProc( ULONG_PTR dwData )
{
  SetEvent( (HANDLE)dwData );
}
int main( void )
{
  HANDLE  hEvent;
  hEvent = CreateEvent( DEFAULT_SECURITY, TRUE, FALSE, NULL );
  QueueUserAPC(ApcProc, GetCurrentThread(), (ULONG_PTR)hEvent);
  WaitForSingleObject(hEvent,100);
    /* код завершения WAIT_TIMEOUT */
  SleepEx( 100, TRUE ); 
    /* APC процедура освобождает событие */
  WaitForSingleObject(hEvent,100);
    /* код завершения WAIT_OBJECT_0 */
  WaitForSingleObject(hEvent,100);
    /* код завершения WAIT_OBJECT_0 */
  CloseHandle( hEvent );
  return 0;
}

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

Если в приведенном примере изменить событие с ручным сбросом на событие с автоматическим сбросом (второй параметр функции CreateEvent должен быть FALSE ), то при втором обращении к WaitForSingleObject функция завершится также с кодом WAIT_OBJECT_0, но при этом событие автоматически будет переведено в занятое состояние. В результате третий вызов функции WaitForSingleObject завершится по таймауту.

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

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

Семафоры

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

#define DEFAULT_SECURITY (LPSECURITY_ATTRIBUTES)NULL

int main( void )
{
  HANDLE 	 hSem;
  LONG 	 lPrev;
  hSem = CreateSemaphore( DEFAULT_SECURITY, 0, 5, NULL );
  WaitForSingleObject( hSem, 100 );
    /* код завершения WAIT_TIMEOUT */
  ReleaseSemaphore( (HANDLE)dwData, 1, &lPrev );
  WaitForSingleObject( hSem, 100 ); 
    /* код завершения WAIT_OBJECT_0 */
  WaitForSingleObject( hSem, 100 ); 
    /* код завершения WAIT_TIMEOUT */
  ReleaseSemaphore( (HANDLE)dwData, 2, &lPrev );
  WaitForSingleObject( hSem, 100 ); 
    /* код завершения WAIT_OBJECT_0 */
  WaitForSingleObject( hSem, 100 ); 
    /* код завершения WAIT_OBJECT_0 */
  WaitForSingleObject( hSem, 100 ); 
    /* код завершения WAIT_TIMEOUT */
  CloseHandle( hSem );
  return 0;
}

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

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

LONG  lCounter;
lCounter = 0;
if ( WaitForSingleObject( hSem, 0 ) == WAIT_OBJECT_0 )
  ReleaseSemaphore( hSem, 1, &lCounter );
/* теперь переменная lCounter содержит значение счетчика */

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

Мьютексы

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

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

#include <process.h>
#include <windows.h>
#define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL

unsigned __stdcall TProc( void *pdata )
{
  WaitForSingleObject( (HANDLE)pdata, 2000 );
  WaitForSingleObject( (HANDLE)pdata, 2000 );
  Sleep( 1000 );
  ReleaseMutex( (HANDLE)pdata );
  ReleaseMutex( (HANDLE)pdata );
  return 0;
}
int main( void )
{
  unsigned  id;
  HANDLE    hMutex, hThread;
  hMutex = CreateMutex( DEFAULT_SECURITY, TRUE, NULL );
  hThread = (HANDLE)_beginthreadex(
    (void*)0, 0, TProc, (void*)hMutex, 0, &id
  );
  Sleep( 1000 );
  ReleaseMutex( hMutex );
  WaitForSingleObject( hThread, INFINITE );
  CloseHandle( hThread );
  CloseHandle( hMutex );
  return 0;
}

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

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

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

Стандартных типов объектов, решающих такие задачи, в Windows нет.

Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было