Common Intermediate Language и системное программирование в Microsoft .NET

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

Разбить на страницы
Показывать лекцию целиком

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

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

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

Рассмотрим простейшую программу, в которой несколько потоков увеличивают на 1 значение элементов общего массива. Так как начальные значения элементов массива 0, то в результате весь массив должен быть заполнен числами, соответствующими числу потоков:

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

#define THREADS 10
#define ASIZE  10000000
static LONG   array[ASIZE];
unsigned __stdcall ThreadProc( void *param )
{
  int  i;
  for ( i = 0; i < ASIZE; i++ ) array[i]++;
  return 0;
}

int main( void )
{
  HANDLE  	hThread[THREADS];
  unsigned 	dwThread;
  int 			i, errs;
  for ( i = 0; i < THREADS; i++ )
    hThread[i] = (HANDLE)_beginthreadex(
      NULL, 0, ThreadProc, NULL, 0, dwThread
    );
  WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
  for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
  for ( errs=i=0; i<ASIZE; i++ )
    if ( array[i] != THREADS ) errs++;
  if ( errs ) printf("Detected %d errors!\n", errs );
  return 0;
}

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

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

несколько разных способов решения подобных проблем:

  • Атомарные операции.

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

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

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

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

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

  • Ожидающие таймеры.

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

  • Атомарные операции

    Атомарные операции обычно имеют более-менее близкое соответствие с командами процессора. Так, например, они могут сводиться к операциям с блокировкой шины (префикс lock ) и специальным командам (типа cmpxchg ) процессора. ОС Windows предоставляет функции для увеличения ( InterlockedIncrement, InterlockedIncrement64 ) или уменьшения ( InterlockedDecrement, InterlockedDecrement64 ) значения целочисленных переменных и изменения их значений ( InterlockedExchange, InterlockedExchange64, InterlockedExchangeAdd, InterlockedExchangePointer ), в том числе со сравнением ( InterlockedCompareExchange, InterlockedCompareExchangePointer ).

    В приведенном выше примере было бы достаточно заменить оператор array[i]++ на вызов функции InterlockedIncrement:

    unsigned _ _stdcall ThreadProc( void *param )
    {
      int  i;
      for ( i = 0; i < ASIZE; i++ ) InterlockedIncrement(array+i);
      return 0;
    }

    Еще несколько функций предназначены для работы с односвязными LIFO списками. Функция InitializeSListHead подготавливает начальный указатель на LIFO список, функция InterlockedPushEntrySList добавляет новую запись в список, функция InterlockedPopEntrySList извлекает из списка последнюю добавленную запись и функция InterlockedFlushSList очищает список. Операции изменения указателей в списке являются атомарными.

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

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

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

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

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

    #include <stdio.h>
    #include <process.h>
    #define _WIN32_WINNT 0x0403
    #include <windows.h>
    
    #define THREADS 		10
    #define ASIZE 			10000000
    static  LONG		        array[ASIZE];
    static  CRITICAL_SECTION 	CS;
    
    unsigned _ _stdcall ThreadProc( void *param )
    {
      int  i;
      for ( i = 0; i < ASIZE; i++ ) {
        EnterCriticalSection( CS );
        array[i]++;
        LeaveCriticalSection( CS );
      }
      return 0;
    }
    
    int main( void )
    {
      HANDLE  	hThread[THREADS];
      unsigned 	dwThread;
      int    		i, errs;
      InitializeCriticalSectionAndSpinCount( CS, 100 );
      for ( i = 0; i < THREADS; i++ )
        hThread[i] = (HANDLE)_beginthreadex(
          NULL, 0, ThreadProc, (void*)i, 0, dwThread );
      WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
      for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
      for ( errs=i=0; i<ASIZE; i++ )
        if ( array[i] != THREADS ) errs++;
      if ( errs ) printf("Detected %d errors!\n", errs );
      DeleteCriticalSection( CS );
      return 0;
    }

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

    Кроме того, эффективность критических секций на многопроцессорных машинах может быть повышена, если перед началом ожидания занятой секции, вместо перехода к ожиданию в режиме ядра, выполнить предварительный кратковременный цикл с опросом состояния секции. Если секция занимается другим потоком на небольшое время, то такой цикл позволяет дождаться ее освобождения, не переходя в режим ядра. Для этого предназначены функции: InitializeCriticalSectionAndSpinCount и SetCriticalSectionSpinCount. Они позволяют задавать число опросов состояния занятой критической секции перед переходом в режим ядра для ожидания. На однопроцессорных машинах опросы не выполняются и функция InitializeCriticalSectionAndSpinCount ничем не отличается от обычной функции InitializeCriticalSection.

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

    В 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 нет.

    Ожидающие таймеры

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

    #include <windows.h>
    #include <stdio.h>
    
    int main()
    {
      HANDLE    		hTimer = NULL;
      LARGE_INTEGER 	liDueTime;
      hTimer = CreateWaitableTimer(NULL, TRUE, "WaitableTimer");
      /* задать срабатывание через 5 секунд */
      liDueTime.QuadPart=-50000000;
      SetWaitableTimer( hTimer, liDueTime, 0, NULL, NULL, 0 );
      WaitForSingleObject( hTimer, INFINITE );
      return 0;
    }

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

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

    Процессы

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

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

  • Использование объектов ядра для взаимной синхронизации. Рассмотрено при обсуждении взаимной синхронизации потоков. При использовании именованных объектов или передаче описателей объектов ядра другим процессам рассмотренные средства могут использоваться для межпроцессной синхронизации.
  • Проецирование файлов в адресное пространство процесса (File Mapping). Один из базовых механизмов, рассматривается ниже.
  • Использование файловых объектов. Каналы (Pipes), почтовые ящики (Mailslots) и сокеты (Sockets). Еще один базовый механизм; чаще применяется для организации межузлового взаимодействия, за исключением анонимных каналов (unnamed pipes, anonymous pipes), которые используются для межпроцессного взаимодействия в рамках одного узла. В данном курсе эти механизмы не затрагиваются.
  • Механизмы, ориентированные на обмен оконными сообщениями (буфер обмена, DDE, сообщение WM_COPYDATA и др.). В своей основе используют механизм проецирования файлов для передачи данных между адресными пространствами процессов. В данном курсе эти механизмы не затрагиваются.
  • Вызов удаленных процедур (Remote Procedure Call, RPC). Является надстройкой, использующей проецирование для реальной передачи данных. Позволяет описать процедуры, реализованные в других процессах, и обращаться к ним как к обычным процедурам, локальным для данного процесса. RPC инкапсулирует вопросы нахождения реальной процедуры, выполняющей необходимую работу, передачу данных в эту процедуру и получение от нее ответа. RPC позволяет организовать не только межпроцессное взаимодействие, но также межузловое с передачей данных по сети. В данном курсе не рассматривается.
  • COM. Является еще более высокоуровневой абстракцией, в данном курсе также не рассматривается.
  • Создание процессов

    Для создания процессов используются функции CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW и CreateProcessWithTokenW. Функция CreateProcess создает новый процесс, который будет исполняться от имени текущего пользователя потока, вызвавшего эту функцию. Функция CreateProcessAsUser позволяет запустить процесс от имени другого пользователя, который идентифицируется его маркером безопасности (security token); однако вызвавший эту функцию поток должен принять меры к правильному использованию реестра, так как профиль нового пользователя не будет загружен. Функции CreateProcessWithTokenW и CreateProcessWithLogonW позволяют при необходимости загрузить профиль пользователя и, кроме того, функция CreateProcessWithLogonW сама получает маркер пользователя по известному учетному имени, домену и паролю.

    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    
    int main( void )
    {
      STARTUPINFO         si;
      PROCESS_INFORMATION  pi;
      memset( si, 0, sizeof(si) );
      memset( pi, 0, sizeof(pi) );
      si.cb = sizeof(si);
      CreateProcess(
        NULL, "cmd.exe", DEFAULT_SECURITY, DEFAULT_SECURITY,
        FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, si, pi
      );
      CloseHandle( pi.hThread );
      WaitForSingleObject( pi.hProcess, INFINITE );
      CloseHandle( pi.hProcess );
      return 0;
    }

    При создании процесса ему можно передать описатели каналов ( CreatePipe ), предназначенные для перенаправления stdin, stdout и stderr. Описатели каналов должны быть наследуемыми.

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

    Адресное пространство процесса и проецирование файлов

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

    Доступная пользователю часть адресного пространства процесса выделяется реально не в физической оперативной памяти, а в файлах, данные которых проецируются на соответствующие фрагменты адресного пространства. Выделение памяти без явного указания проецируемого файла приведет к тому, что область для проецирования будет автоматически выделяться в файле подкачки страниц. Физическая оперативная память в значительной степени является кэшем для данных файлов. Выделение физической оперативной памяти применяется очень редко, например, средствами расширения адресного пространства (Address Windowing Extension, AWE), позволяющими использовать более чем 4Г адресного пространства в 32-х разрядном приложении Win32.

    Важная особенность средств управления адресным пространством и проецированием файлов - они используют так называемую гранулярность выделения ресурсов (ее можно узнать с помощью функции GetSystemInfo ); в современных версиях Win32 гранулярность составляет 64К. Это означает, что если вы попробуете спроецировать в память файл размером 100 байт, то в адресном пространстве будет занят фрагмент в 65536 байт длиной, из которого реально будут заняты только первые 100 байт.

    Для управления адресным пространством предназначены функции VirtualAlloc, VirtualFree, VirtualAllocEx, VirtualFreeEx, VirtualLock и VirtualUnlock. С их помощью можно резервировать пространство в адресном пространстве процесса (без передачи физической памяти из файла) и управлять передачей памяти из файла подкачки страниц указанному диапазону адресов.

    Механизмы проецирования файлов в память различаются для обычных и для исполняемых файлов. Функции создания процессов ( CreateProcess...) и загрузки библиотек ( LoadLibrary, LoadLibraryEx ), помимо специфичных действий, выполняют проецирование исполняемых файлов в адресное пространство процесса; при этом учитывается их деление на сегменты, наличие секций импорта, экспорта и релокаций и др. Обычные файлы проецируются как непрерывный блок данных на непрерывный диапазон адресов.

    Для явного проецирования файлов используется специальный объект ядра проекция файла (file mapping object). Этот объект предназначен для описания файла, который может быть спроецирован в память, но реального отображения файла или его части в память при создании проекции не происходит. Описатель объекта "проекция файла" можно получить с помощью функций CreateFileMapping и OpenFileMapping. Для проецирования файла или его части в память предназначены функции MapViewOfFile, MapViewOfFileEx и UnmapViewOfFile:

    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    int main( void )
    {
      HANDLE  hFile, hMapping;
      LPVOID  pMapping;
      LPSTR   p;
      int     i;
      hFile = CreateFile(
        "abc.dat", GENERIC_WRITE|GENERIC_READ,
        FILE_SHARE_WRITE, DEFAULT_SECURITY,
        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL
      );
      hMapping = CreateFileMapping(
        hFile, DEFAULT_SECURITY, PAGE_READWRITE, 0, 256, NULL
      );
      pMapping = MapViewOfFile( hMapping, FILE_MAP_WRITE, 0,0, 0 );
      for ( p = (LPSTR)pMapping, i=0; i<256; i++ ) *p++ = (char)i;
      UnmapViewOfFile( hMapping );
      CloseHandle( hMapping );
      CloseHandle( hFile );
      return 0;
    }

    Межпроцессное взаимодействие с использованием проецирования файлов

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

    В примере ниже приводится текст двух приложений (first.cpp и second.cpp), которые обмениваются между собой данными через общий объект "проекция файла":

    /* FIRST.CPP */
    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    int main( void )
    {
      HANDLE                hMapping;
      LPVOID                pMapping;
      STARTUPINFO  	        si;
      PROCESS_INFORMATION  	pi;
      int           	*p;
      int           	i;
      memset( si, 0, sizeof(si) );
      memset( pi, 0, sizeof(pi) );
      si.cb = sizeof(si);
      hMapping = CreateFileMapping(
        INVALID_HANDLE_VALUE, DEFAULT_SECURITY, PAGE_READWRITE,
        0, 1024, "FileMap-AB-874436342"
      );
      pMapping = MapViewOfFile( hMapping, FILE_MAP_WRITE, 0,0, 0 );
      for (p=(int*)pMapping,i=0; i<256; i++) *p++=i;
      CreateProcess(
        NULL, "second.exe", DEFAULT_SECURITY,
        DEFAULT_SECURITY, FALSE, NORMAL_PRIORITY_CLASS,
        NULL, NULL, si, pi);
      CloseHandle( pi.hThread );
      WaitForSingleObject( pi.hProcess, INFINITE );
      CloseHandle( pi.hProcess );
      UnmapViewOfFile( hMapping );
      CloseHandle( hMapping );
      return 0;
    }
    
    /* SECOND.CPP */
    #include <windows.h>
    int main( int ac, char **av )
    {
      HANDLE   	 hMapping;
      LPVOID  	 pMapping;
      int     	 *p;
      int     	 i;
      hMapping = OpenFileMapping(
        FILE_MAP_READ, FALSE, "FileMap-AB-874436342"
      );
      pMapping = MapViewOfFile( hMapping, FILE_MAP_READ, 0,0, 0 );
      for ( p = (int*)pMapping, i=0; i<256; i++ )
        if ( *p++ != i ) break;
      if ( i != 256 ) { /* ОШИБКА! */ }
      UnmapViewOfFile( hMapping );
      CloseHandle( hMapping );
      return 0;
    }

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

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

    Межпроцессное взаимодействие с использованием общих секций

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

    В Microsoft Visual C++ для объявления разделяемого сегмента и помещаемых в него данных используются директивы #pragma и _ _declspec, как показано в примере ниже:

    /* FSEC.CPP */
    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    
    #pragma section("SHRD_DATA",read,write,shared)
    _ _declspec(allocate("SHRD_DATA")) int shared[ 256 ];
    
    int main( int ac, char **av )
    {
      STARTUPINFO         	si;
      PROCESS_INFORMATION 	pi;
      int          		i;
      memset( si, 0, sizeof(si) );
      memset( pi, 0, sizeof(pi) );
      si.cb = sizeof(si);
      if ( ac != 2 || strcmp( av[1], "slave" ) ) {    
        for ( i = 0; i < 256; i++ ) shared[i] = 256-i;
        /* первый экземляр с общими данными */
        CreateProcess(
          NULL, "fsec.exe slave", DEFAULT_SECURITY,
          DEFAULT_SECURITY, FALSE, NORMAL_PRIORITY_CLASS,
          NULL, NULL, si, pi
        );
        CloseHandle( pi.hThread );
        
        WaitForSingleObject( pi.hProcess, INFINITE );
        CloseHandle( pi.hProcess );
      } else {
        /* второй экземляр с общими данными */
        for ( i = 0; i < 256; i++ )
          if ( shared[i] != 256-i ) break;
        if ( i != 256 ) { /* ОШИБКА! */ }
      }
      return 0;
    }

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

    Страницы:

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

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

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

    Рассмотрим простейшую программу, в которой несколько потоков увеличивают на 1 значение элементов общего массива. Так как начальные значения элементов массива 0, то в результате весь массив должен быть заполнен числами, соответствующими числу потоков:

    #include <stdio.h>
    #include <process.h>
    #include <windows.h>
    
    #define THREADS 10
    #define ASIZE  10000000
    static LONG   array[ASIZE];
    unsigned __stdcall ThreadProc( void *param )
    {
      int  i;
      for ( i = 0; i < ASIZE; i++ ) array[i]++;
      return 0;
    }
    
    int main( void )
    {
      HANDLE  	hThread[THREADS];
      unsigned 	dwThread;
      int 			i, errs;
      for ( i = 0; i < THREADS; i++ )
        hThread[i] = (HANDLE)_beginthreadex(
          NULL, 0, ThreadProc, NULL, 0, dwThread
        );
      WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
      for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
      for ( errs=i=0; i<ASIZE; i++ )
        if ( array[i] != THREADS ) errs++;
      if ( errs ) printf("Detected %d errors!\n", errs );
      return 0;
    }

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

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

    несколько разных способов решения подобных проблем:

  • Атомарные операции.

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

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

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

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

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

  • Ожидающие таймеры.

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

  • Атомарные операции

    Атомарные операции обычно имеют более-менее близкое соответствие с командами процессора. Так, например, они могут сводиться к операциям с блокировкой шины (префикс lock ) и специальным командам (типа cmpxchg ) процессора. ОС Windows предоставляет функции для увеличения ( InterlockedIncrement, InterlockedIncrement64 ) или уменьшения ( InterlockedDecrement, InterlockedDecrement64 ) значения целочисленных переменных и изменения их значений ( InterlockedExchange, InterlockedExchange64, InterlockedExchangeAdd, InterlockedExchangePointer ), в том числе со сравнением ( InterlockedCompareExchange, InterlockedCompareExchangePointer ).

    В приведенном выше примере было бы достаточно заменить оператор array[i]++ на вызов функции InterlockedIncrement:

    unsigned _ _stdcall ThreadProc( void *param )
    {
      int  i;
      for ( i = 0; i < ASIZE; i++ ) InterlockedIncrement(array+i);
      return 0;
    }

    Еще несколько функций предназначены для работы с односвязными LIFO списками. Функция InitializeSListHead подготавливает начальный указатель на LIFO список, функция InterlockedPushEntrySList добавляет новую запись в список, функция InterlockedPopEntrySList извлекает из списка последнюю добавленную запись и функция InterlockedFlushSList очищает список. Операции изменения указателей в списке являются атомарными.

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

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

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

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

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

    #include <stdio.h>
    #include <process.h>
    #define _WIN32_WINNT 0x0403
    #include <windows.h>
    
    #define THREADS 		10
    #define ASIZE 			10000000
    static  LONG		        array[ASIZE];
    static  CRITICAL_SECTION 	CS;
    
    unsigned _ _stdcall ThreadProc( void *param )
    {
      int  i;
      for ( i = 0; i < ASIZE; i++ ) {
        EnterCriticalSection( CS );
        array[i]++;
        LeaveCriticalSection( CS );
      }
      return 0;
    }
    
    int main( void )
    {
      HANDLE  	hThread[THREADS];
      unsigned 	dwThread;
      int    		i, errs;
      InitializeCriticalSectionAndSpinCount( CS, 100 );
      for ( i = 0; i < THREADS; i++ )
        hThread[i] = (HANDLE)_beginthreadex(
          NULL, 0, ThreadProc, (void*)i, 0, dwThread );
      WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
      for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
      for ( errs=i=0; i<ASIZE; i++ )
        if ( array[i] != THREADS ) errs++;
      if ( errs ) printf("Detected %d errors!\n", errs );
      DeleteCriticalSection( CS );
      return 0;
    }

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

    Кроме того, эффективность критических секций на многопроцессорных машинах может быть повышена, если перед началом ожидания занятой секции, вместо перехода к ожиданию в режиме ядра, выполнить предварительный кратковременный цикл с опросом состояния секции. Если секция занимается другим потоком на небольшое время, то такой цикл позволяет дождаться ее освобождения, не переходя в режим ядра. Для этого предназначены функции: InitializeCriticalSectionAndSpinCount и SetCriticalSectionSpinCount. Они позволяют задавать число опросов состояния занятой критической секции перед переходом в режим ядра для ожидания. На однопроцессорных машинах опросы не выполняются и функция InitializeCriticalSectionAndSpinCount ничем не отличается от обычной функции InitializeCriticalSection.

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

    В 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 нет.

    Ожидающие таймеры

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

    #include <windows.h>
    #include <stdio.h>
    
    int main()
    {
      HANDLE    		hTimer = NULL;
      LARGE_INTEGER 	liDueTime;
      hTimer = CreateWaitableTimer(NULL, TRUE, "WaitableTimer");
      /* задать срабатывание через 5 секунд */
      liDueTime.QuadPart=-50000000;
      SetWaitableTimer( hTimer, liDueTime, 0, NULL, NULL, 0 );
      WaitForSingleObject( hTimer, INFINITE );
      return 0;
    }

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

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

    Процессы

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

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

  • Использование объектов ядра для взаимной синхронизации. Рассмотрено при обсуждении взаимной синхронизации потоков. При использовании именованных объектов или передаче описателей объектов ядра другим процессам рассмотренные средства могут использоваться для межпроцессной синхронизации.
  • Проецирование файлов в адресное пространство процесса (File Mapping). Один из базовых механизмов, рассматривается ниже.
  • Использование файловых объектов. Каналы (Pipes), почтовые ящики (Mailslots) и сокеты (Sockets). Еще один базовый механизм; чаще применяется для организации межузлового взаимодействия, за исключением анонимных каналов (unnamed pipes, anonymous pipes), которые используются для межпроцессного взаимодействия в рамках одного узла. В данном курсе эти механизмы не затрагиваются.
  • Механизмы, ориентированные на обмен оконными сообщениями (буфер обмена, DDE, сообщение WM_COPYDATA и др.). В своей основе используют механизм проецирования файлов для передачи данных между адресными пространствами процессов. В данном курсе эти механизмы не затрагиваются.
  • Вызов удаленных процедур (Remote Procedure Call, RPC). Является надстройкой, использующей проецирование для реальной передачи данных. Позволяет описать процедуры, реализованные в других процессах, и обращаться к ним как к обычным процедурам, локальным для данного процесса. RPC инкапсулирует вопросы нахождения реальной процедуры, выполняющей необходимую работу, передачу данных в эту процедуру и получение от нее ответа. RPC позволяет организовать не только межпроцессное взаимодействие, но также межузловое с передачей данных по сети. В данном курсе не рассматривается.
  • COM. Является еще более высокоуровневой абстракцией, в данном курсе также не рассматривается.
  • Создание процессов

    Для создания процессов используются функции CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW и CreateProcessWithTokenW. Функция CreateProcess создает новый процесс, который будет исполняться от имени текущего пользователя потока, вызвавшего эту функцию. Функция CreateProcessAsUser позволяет запустить процесс от имени другого пользователя, который идентифицируется его маркером безопасности (security token); однако вызвавший эту функцию поток должен принять меры к правильному использованию реестра, так как профиль нового пользователя не будет загружен. Функции CreateProcessWithTokenW и CreateProcessWithLogonW позволяют при необходимости загрузить профиль пользователя и, кроме того, функция CreateProcessWithLogonW сама получает маркер пользователя по известному учетному имени, домену и паролю.

    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    
    int main( void )
    {
      STARTUPINFO         si;
      PROCESS_INFORMATION  pi;
      memset( si, 0, sizeof(si) );
      memset( pi, 0, sizeof(pi) );
      si.cb = sizeof(si);
      CreateProcess(
        NULL, "cmd.exe", DEFAULT_SECURITY, DEFAULT_SECURITY,
        FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, si, pi
      );
      CloseHandle( pi.hThread );
      WaitForSingleObject( pi.hProcess, INFINITE );
      CloseHandle( pi.hProcess );
      return 0;
    }

    При создании процесса ему можно передать описатели каналов ( CreatePipe ), предназначенные для перенаправления stdin, stdout и stderr. Описатели каналов должны быть наследуемыми.

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

    Адресное пространство процесса и проецирование файлов

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

    Доступная пользователю часть адресного пространства процесса выделяется реально не в физической оперативной памяти, а в файлах, данные которых проецируются на соответствующие фрагменты адресного пространства. Выделение памяти без явного указания проецируемого файла приведет к тому, что область для проецирования будет автоматически выделяться в файле подкачки страниц. Физическая оперативная память в значительной степени является кэшем для данных файлов. Выделение физической оперативной памяти применяется очень редко, например, средствами расширения адресного пространства (Address Windowing Extension, AWE), позволяющими использовать более чем 4Г адресного пространства в 32-х разрядном приложении Win32.

    Важная особенность средств управления адресным пространством и проецированием файлов - они используют так называемую гранулярность выделения ресурсов (ее можно узнать с помощью функции GetSystemInfo ); в современных версиях Win32 гранулярность составляет 64К. Это означает, что если вы попробуете спроецировать в память файл размером 100 байт, то в адресном пространстве будет занят фрагмент в 65536 байт длиной, из которого реально будут заняты только первые 100 байт.

    Для управления адресным пространством предназначены функции VirtualAlloc, VirtualFree, VirtualAllocEx, VirtualFreeEx, VirtualLock и VirtualUnlock. С их помощью можно резервировать пространство в адресном пространстве процесса (без передачи физической памяти из файла) и управлять передачей памяти из файла подкачки страниц указанному диапазону адресов.

    Механизмы проецирования файлов в память различаются для обычных и для исполняемых файлов. Функции создания процессов ( CreateProcess...) и загрузки библиотек ( LoadLibrary, LoadLibraryEx ), помимо специфичных действий, выполняют проецирование исполняемых файлов в адресное пространство процесса; при этом учитывается их деление на сегменты, наличие секций импорта, экспорта и релокаций и др. Обычные файлы проецируются как непрерывный блок данных на непрерывный диапазон адресов.

    Для явного проецирования файлов используется специальный объект ядра проекция файла (file mapping object). Этот объект предназначен для описания файла, который может быть спроецирован в память, но реального отображения файла или его части в память при создании проекции не происходит. Описатель объекта "проекция файла" можно получить с помощью функций CreateFileMapping и OpenFileMapping. Для проецирования файла или его части в память предназначены функции MapViewOfFile, MapViewOfFileEx и UnmapViewOfFile:

    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    int main( void )
    {
      HANDLE  hFile, hMapping;
      LPVOID  pMapping;
      LPSTR   p;
      int     i;
      hFile = CreateFile(
        "abc.dat", GENERIC_WRITE|GENERIC_READ,
        FILE_SHARE_WRITE, DEFAULT_SECURITY,
        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL
      );
      hMapping = CreateFileMapping(
        hFile, DEFAULT_SECURITY, PAGE_READWRITE, 0, 256, NULL
      );
      pMapping = MapViewOfFile( hMapping, FILE_MAP_WRITE, 0,0, 0 );
      for ( p = (LPSTR)pMapping, i=0; i<256; i++ ) *p++ = (char)i;
      UnmapViewOfFile( hMapping );
      CloseHandle( hMapping );
      CloseHandle( hFile );
      return 0;
    }

    Межпроцессное взаимодействие с использованием проецирования файлов

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

    В примере ниже приводится текст двух приложений (first.cpp и second.cpp), которые обмениваются между собой данными через общий объект "проекция файла":

    /* FIRST.CPP */
    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    int main( void )
    {
      HANDLE                hMapping;
      LPVOID                pMapping;
      STARTUPINFO  	        si;
      PROCESS_INFORMATION  	pi;
      int           	*p;
      int           	i;
      memset( si, 0, sizeof(si) );
      memset( pi, 0, sizeof(pi) );
      si.cb = sizeof(si);
      hMapping = CreateFileMapping(
        INVALID_HANDLE_VALUE, DEFAULT_SECURITY, PAGE_READWRITE,
        0, 1024, "FileMap-AB-874436342"
      );
      pMapping = MapViewOfFile( hMapping, FILE_MAP_WRITE, 0,0, 0 );
      for (p=(int*)pMapping,i=0; i<256; i++) *p++=i;
      CreateProcess(
        NULL, "second.exe", DEFAULT_SECURITY,
        DEFAULT_SECURITY, FALSE, NORMAL_PRIORITY_CLASS,
        NULL, NULL, si, pi);
      CloseHandle( pi.hThread );
      WaitForSingleObject( pi.hProcess, INFINITE );
      CloseHandle( pi.hProcess );
      UnmapViewOfFile( hMapping );
      CloseHandle( hMapping );
      return 0;
    }
    
    /* SECOND.CPP */
    #include <windows.h>
    int main( int ac, char **av )
    {
      HANDLE   	 hMapping;
      LPVOID  	 pMapping;
      int     	 *p;
      int     	 i;
      hMapping = OpenFileMapping(
        FILE_MAP_READ, FALSE, "FileMap-AB-874436342"
      );
      pMapping = MapViewOfFile( hMapping, FILE_MAP_READ, 0,0, 0 );
      for ( p = (int*)pMapping, i=0; i<256; i++ )
        if ( *p++ != i ) break;
      if ( i != 256 ) { /* ОШИБКА! */ }
      UnmapViewOfFile( hMapping );
      CloseHandle( hMapping );
      return 0;
    }

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

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

    Межпроцессное взаимодействие с использованием общих секций

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

    В Microsoft Visual C++ для объявления разделяемого сегмента и помещаемых в него данных используются директивы #pragma и _ _declspec, как показано в примере ниже:

    /* FSEC.CPP */
    #include <windows.h>
    #define DEFAULT_SECURITY  (LPSECURITY_ATTRIBUTES)NULL
    
    #pragma section("SHRD_DATA",read,write,shared)
    _ _declspec(allocate("SHRD_DATA")) int shared[ 256 ];
    
    int main( int ac, char **av )
    {
      STARTUPINFO         	si;
      PROCESS_INFORMATION 	pi;
      int          		i;
      memset( si, 0, sizeof(si) );
      memset( pi, 0, sizeof(pi) );
      si.cb = sizeof(si);
      if ( ac != 2 || strcmp( av[1], "slave" ) ) {    
        for ( i = 0; i < 256; i++ ) shared[i] = 256-i;
        /* первый экземляр с общими данными */
        CreateProcess(
          NULL, "fsec.exe slave", DEFAULT_SECURITY,
          DEFAULT_SECURITY, FALSE, NORMAL_PRIORITY_CLASS,
          NULL, NULL, si, pi
        );
        CloseHandle( pi.hThread );
        
        WaitForSingleObject( pi.hProcess, INFINITE );
        CloseHandle( pi.hProcess );
      } else {
        /* второй экземляр с общими данными */
        for ( i = 0; i < 256; i++ )
          if ( shared[i] != 256-i ) break;
        if ( i != 256 ) { /* ОШИБКА! */ }
      }
      return 0;
    }

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

    Вернуться к учебному плану