Материалы, рассматриваемые в этом разделе, могут быть разделены на две категории - синхронизация потоков и работа с процессами. Они сгруппированы в силу того, что большая часть методов
При реализации
Рассмотрим простейшую программу, в которой несколько потоков увеличивают на 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 предусмотрены ожидающие таймеры, которые могут использоваться или
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;
}
Еще несколько функций предназначены для работы с односвязными InitializeSListHead подготавливает начальный указатель на 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 (или ее клон) ожидает сразу все объекты группы, то до освобождения всех ожидаемых объектов одновременно никаких мер по занятию ранее освободившихся объектов функция не предпринимает.
В тех случаях, когда поток переходит в состояние ожидания, его исполнение блокируется до конца ожидания. Для реализации
Для перехода в ожидание оповещения предусмотрены функции SleepEx, WaitForMultipleObjectsEx, WaitForSingleObjectEx и SignalObjectAndWait.
Еще несколько функций предназначены для разработки GUI-приложений Win32: MsgWaitForMultipleObjects и MsgWaitForMultipleObjectsEx выполняют ожидание указанных объектов и, кроме того, могут контролировать состояние очереди потока, предназначенной для обработки оконных сообщений. Функция WaitForInputIdle ожидает, пока GUI-приложение не закончит свою инициализацию и не перейдет в ожидание в цикле обработки сообщений.
Несколько типов объектов в Win32 предназначены только для взаимной
При проектировании 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 событие все еще занято, поэтому выход из функции осуществляется по таймауту. При втором вызове уже успевает сработать
Если в приведенном примере изменить событие с ручным сбросом на событие с автоматическим сбросом (второй параметр функции 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-подобных систем системного вызова типа в Windows не предусмотрено - новый процесс создается заново, с выделением для него адресного пространства, проецирования на него компонент системы, образа исполняемого файла, необходимых динамических библиотек и других компонент. Эта операция требует чуть больше ресурсов, чем в UNIX-подобных системах, однако выполняется достаточно быстро - диспетчер памяти позволяет просто проецировать на адресное пространство компоненты из других процессов с режимом "копирование при записи".
Основные механизмы взаимодействия процессов могут быть разделены на несколько групп:
WM_COPYDATA и др.). В своей основе используют механизм проецирования файлов для передачи данных между адресными пространствами процессов. В данном курсе эти механизмы не затрагиваются.Для CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW и CreateProcessWithTokenW. Функция CreateProcess создает новый процесс, который будет исполняться от имени текущего пользователя потока, вызвавшего эту функцию. Функция CreateProcessAsUser позволяет запустить процесс от имени другого пользователя, который идентифицируется его маркером безопасности (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,
Важная особенность средств управления адресным пространством и проецированием файлов - они используют так называемую GetSystemInfo ); в современных версиях Win32
Для управления адресным пространством предназначены функции 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 предусмотрены ожидающие таймеры, которые могут использоваться или
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;
}
Еще несколько функций предназначены для работы с односвязными InitializeSListHead подготавливает начальный указатель на 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 (или ее клон) ожидает сразу все объекты группы, то до освобождения всех ожидаемых объектов одновременно никаких мер по занятию ранее освободившихся объектов функция не предпринимает.
В тех случаях, когда поток переходит в состояние ожидания, его исполнение блокируется до конца ожидания. Для реализации
Для перехода в ожидание оповещения предусмотрены функции SleepEx, WaitForMultipleObjectsEx, WaitForSingleObjectEx и SignalObjectAndWait.
Еще несколько функций предназначены для разработки GUI-приложений Win32: MsgWaitForMultipleObjects и MsgWaitForMultipleObjectsEx выполняют ожидание указанных объектов и, кроме того, могут контролировать состояние очереди потока, предназначенной для обработки оконных сообщений. Функция WaitForInputIdle ожидает, пока GUI-приложение не закончит свою инициализацию и не перейдет в ожидание в цикле обработки сообщений.
Несколько типов объектов в Win32 предназначены только для взаимной
При проектировании 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 событие все еще занято, поэтому выход из функции осуществляется по таймауту. При втором вызове уже успевает сработать
Если в приведенном примере изменить событие с ручным сбросом на событие с автоматическим сбросом (второй параметр функции 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-подобных систем системного вызова типа в Windows не предусмотрено - новый процесс создается заново, с выделением для него адресного пространства, проецирования на него компонент системы, образа исполняемого файла, необходимых динамических библиотек и других компонент. Эта операция требует чуть больше ресурсов, чем в UNIX-подобных системах, однако выполняется достаточно быстро - диспетчер памяти позволяет просто проецировать на адресное пространство компоненты из других процессов с режимом "копирование при записи".
Основные механизмы взаимодействия процессов могут быть разделены на несколько групп:
WM_COPYDATA и др.). В своей основе используют механизм проецирования файлов для передачи данных между адресными пространствами процессов. В данном курсе эти механизмы не затрагиваются.Для CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW и CreateProcessWithTokenW. Функция CreateProcess создает новый процесс, который будет исполняться от имени текущего пользователя потока, вызвавшего эту функцию. Функция CreateProcessAsUser позволяет запустить процесс от имени другого пользователя, который идентифицируется его маркером безопасности (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,
Важная особенность средств управления адресным пространством и проецированием файлов - они используют так называемую GetSystemInfo ); в современных версиях Win32
Для управления адресным пространством предназначены функции 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;
}
Некоторое неудобство, связанное с необходимостью использовать одно и то же приложение, можно легко обойти, если разделяемый сегмент поместить в разделяемую библиотеку - тогда один и тот же образ библиотеки может быть подключен к разным приложениям, что позволит им обмениваться данными. В этом варианте надо только учитывать, что адреса, в которые будет помещена библиотека, в разных процессах могут отличаться.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.