Окт 28 2010

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

Как только вы разобьете приложение на несколько потоков, вам придется решать проблемы, о существовании которых вы даже не подозревали, занимаясь программированием в однопоточном режиме.
Например, простая операция считывания и записи в общую глобальную переменную из нескольких потоков без должной синхронизации, может быть источником ошибок в работе программы.
Первичный поток создает с помощью CreateThread дочерний поток, имеющий входную функцию ThreadFunc. В теле функции ThreadFunc вызывается функция IncCounter, которая выполняет в цикле N раз операцию инкремента для глобального счетчика g_counter.
Запустив дочерний поток и не дожидаясь окончания его выполнения, первичный поток вызывает функцию DecCounter. Функция DecCounter выполняет в цикле N раз операцию декремента для глобального счетчика g_counter.
После возврата из DecCounter первичный поток ждет окончания работы дочернего потока с помощью функции WaitForSingleObject. Вызов функции
WaitForSingleObject(hThread, INFINITE):
обеспечивает приостановку выполнения потока на неограниченное время (INFINITE), пока не освободится объект ядра hThread.
После этого, вызывая функцию InvalidateRect, первичный поток заставляет Windows сформировать сообщение WM_PAINT. Обрабатывая это сообщение, функция WndProc выводит значение переменной g_counter в главное окно приложения. Нетрудно догадаться, что правильным результатом выполнения этого приложения должно быть значение g_counter = 0.
При маленьких значениях N так и происходит. Но вот для значения 50 000 000 эта программа дает самые разные результаты на разных компьютерах. Эксперименты проводились в основном на компьютерах, имеющих процессор Intel Celeron и Intel Pentium. Заметим, что при тактовой частоте 2 ГГц функция IncCounter так же, как и функция DecCounter, требует для своего выполнения примерно 150 мс. При такой продолжительности выполнения каждый поток будет прерываться не менее пяти раз, отдавая процессор другому потоку.
На всех компьютерах, имеющих процессор с технологией HyperThreating, был получен ненулевой результат счетчика g_counter. Для компьютеров с процессором без поддержки HyperThreating результат был иногда ненулевой, иногда нулевой.
Чем же вызвано искажение результата работы программы? — Прерыванием потока в тот момент, когда процессор не полностью завершил очередную операцию! Рассмотрим это чуть подробнее.
Допустим, что функция DecCounter первичного потока приступила к очередному вычитанию единицы из счетчика. Процессор, реализуя эту операцию, скопировал значение глобальной переменной g_counter в свой регистр. Предположим, что это значение равно 100 000. Далее процессор успел вычесть единицу, но не успел переписать новое значение 99 999 обратно в глобальную переменную. В этот момент система отбирает процессор у первичного потока, сохранив контекст потока, и передает его на использование дочернему потоку. Предположим, что функция IncCounter дочернего потока успела 50 000 раз добавить в счетчик единицу, так что переменная g_counter стала равна 150 000. В этот момент квант дочернего потока истек, и после окончания очередной операции система возвращает процессор первичному потоку. При этом система восстанавливает контекст первичного потока с незавершенной операцией. Завершая эту операцию, процессор переписывает значение 99 999 из своего регистра в глобальную переменную g_counter. В описанной ситуации квант работы дочернего потока пошел насмарку — его результаты утеряны!
Итак, мы показали, что одновременное использование несколькими потоками общей глобальной переменной без должной синхронизации может быть источником ошибок.


Окт 19 2010

Синхронизация

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


Окт 01 2010

Атомарный доступ и семейство Interlocked-функций

Большая часть синхронизации потоков связана с атомарным доступом (atomic access) — монопольным захватом ресурса обращающимся к нему потоком. Win32 API предоставляет несколько функций для реализации взаимно блокированных операций. Все Interlocked-функций работают корректно только при условии, что их аргументы выровнены по границе двойного слова (DWORD).
Функция Interlockedlncrement, имеющая прототип
LONG InterlockedIncrement(LPLONG IpAddend); инкрементирует 32-разрядную переменную, адрес которой задается параметром IpAddend. Функция возвращает новое значение указанной переменной.
Функция Interlocked Decrement определена аналогично функции Interlockedlncrement, но она декрементирует 32-разрядную переменную.
Пара функций
LONG Inter!ockedExchange(LPLONG IpTarget. LONG Value);
PVOID InterlockedExchangePointer(PVOID ppvTarget. PVOID pvValue);
монопольно заменяет текущее значение переменной типа LONG, адрес которой передается в первом параметре, значением, передаваемым во втором параметре. В 32-разрядном приложении обе функции работают с 32-разрядными значениями. В 64-разрядной программе первая функция оперирует 32-разрядными значениями, а вторая — 64-разрядными. Обе функции возвращают исходное значение пе-ременной.
Следующая функция добавляет к значению переменной, адрес которой передается в первом параметре, значение, передаваемое во втором параметре:
LONG InterlockedExchangeAdd(LPLONG IpAddend. LONG Increment);
Еще две функции выполняют операцию сравнения и присваивания по результату сравнения:
LONG InterlockedCompareExchangetLPLONG IpDestination, LONG Exchange.
LONG Comparand); PVOID InterlockedCompareExchangePointeKPVOID ppvDestination.
PVOID pvExchange. PVOID pvComparand);
Если значение переменной, адрес которой передается в первом параметре, совпадает со значением, передаваемым в третьем параметре, то оно заменяется значением, передаваемым во втором параметре. В 32-разрядном приложении обе функции работают с 32-разрядными значениями. В 64-разрядной программе первая функция оперирует 32-разрядными значениями, а вторая — 64-разрядными. Обе функции возвращают исходное значение переменной, заданной первым параметром.
Вернемся к нашему приложению BadCount (см. листинг 9.3), работающему некорректно из-за одновременного доступа к общей глобальной переменной g_counter из разных потоков. Имея на вооружении функции с атомарным доступом, совсем несложно заставить приложение работать правильно. Для этого нужно в реализации функции IncCounter заменить инструкцию
++g_counter: вызовом следующей функции:
InterlockedIncrement(&g_counter);
Аналогично, в реализации функции DecCounter необходимо операцию
--g_counter; заменить следующим вызовом:
InterlockedDecrement(&g_counter);
Проверьте, как будет работать на вашем компьютере программа BadCount после указанных изменений в ее исходном тексте. Модифицированное приложение, по-видимому, достойно и нового имени GoodCount!


Сен 27 2010

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

Критическая секция (critical section) — это небольшой участок кода, который должен использоваться только одним потоком одновременно. Если в одно время несколько потоков попытаются получить доступ к критическому участку, то контроль над ним будет предоставлен только одному из потоков, а все остальные будут переведены в состояние ожидания до тех пор, пока участок не освободится.
Для использования критической секции необходимо определить переменную типа CRITICAL_SECTION:
CRITICAL_SECTION cs;
Поскольку эта переменная должна находиться в области видимости для каждого использующего ее потока, обычно она объявляется как глобальная. Эту переменную следует инициализировать до ее первого применения с помощью функции InitializeCriticalSection:
InitializeCriticalSection(Scs);
Чтобы завладеть критическим участком, поток должен вызвать функцию EnterCriticalSection:
EnterCri ti calSecti on(&cs);
Если критический участок не используется в данный момент другим потоком, он обозначается системой как занятый, и поток немедленно продолжает выполнение. Если критический участок уже используется, то поток блокируется до тех пор, пока участок не будет освобожден.
После вызова EnterCriticalSection следуют инструкции, принадлежащие критическому участку.
Конец критического участка обозначается вызовом функции LeaveCriticalSection:
LeaveCriticalSection(&cs);
Как только поток получает контроль над критическим участком, доступ других потоков к этому участку блокируется. При этом очень важно, чтобы время выполнения критического участка было минимальным. Это позволит добиться наилучших результатов работы приложения.
Если критический участок больше не нужен, используемые им ресурсы освобождаются вызовом функции DeleteCriticalSection.


Сен 05 2010

Побочные эффекты успешного ожидания

Успешный вызов функции WaitForSingleObject или WaitForMuttipleObjects на самом деле меняет состояние некоторых объектов ядра. Успешным считается такой вызов, который завершается освобождением соответствующего объекта или объектов. При этом функция возвращает значение WAIT_OBJECT_0 или значение, являющееся смещением относительно WAIT_OBJECT_0. Вызов считается неудачным, если возвращается значение WAIT_TIMEOUT или WAIT_FAILED. В этом случае состояние каких-либо объектов не меняется.
Пусть, например, поток вызвал функцию WaitForSingleObject и ждет освобождения объекта «событие с автосбросом» (объекты-события рассматриваются в следующем разделе). Когда объект переходит в свободное состояние, функция обнаруживает это и возвращает значение WAIT_OBJECT_0. Но перед самым возвратом из функции объект-событие будет переведен в занятое состояние. Это и есть побочный эффект успешного ожидания.
Объекты ядра «событие с автосбросом» ведут себя подобным образом, потому что таково одно из правил, определенных Microsoft для объектов этого типа. Другие объекты дают иные побочные эффекты, а некоторые — вообще никаких. К последним относятся объекты ядра «процесс» и «поток».


Авг 19 2010

Wait-функции

Многие объекты ядра могут находиться либо в свободном {signaled state), либо
в занятом состоянии (nonsignaled state). К таким объектам относятся:
процессы;
потоки;
задания;
файлы;
консольный ввод;
уведомления об изменении файлов;
события;
ожидаемые таймеры;
семафоры;
мьютексы.
Wait-функции позволяют потоку в любой момент приостановиться и ждать освобождения какого-либо объекта ядра. Важным свойством функций этого семейства является то, что они не тратят процессорное время, пока ждут освобождения объекта или наступления тайм-аута.


Авг 07 2010

Обмен данными между процессами

Потоки одного процесса не имеют доступа к адресному пространству другого процесса. Однако существуют специализированные механизмы для передачи данных между процессами. Вообще обмен данными между процессами (Interprocess Communication, IPC) в свое время начинался в виде процедуры перемещения перфокарт из выходного отверстия одного вычислительного устройства в устройство чтения другого вычислительного устройства. Позже, с развитием операционных систем, обмен данными между процессами проделал определенный путь в сторону высоких технологий. Наиболее впечатляющим примером обмена информацией является создание Всемирной паутины (World Wide Web). Приведем краткий список механизмов IPC, встроенных в Windows:
Буфер обмена (clipboard) — одна из самых примитивных и хорошо известных форм IPC. Основная его задача состоит в поддержке обмена данными между программами по желанию и под контролем пользователя.
Библиотеки динамической компоновки. Когда в рамках DLL объявляется переменная, то ее можно сделать разделяемой (shared). Все процессы из DLL, обращающиеся к такой переменной, будут использовать одно и то же место в физической памяти.
Сообщение WM_COPYDATA, которое применяется для передачи участка памяти другому процессу.
Разделяемая память (shared memory) реализуется при помощи объектов ядра «проекция файла», которые осуществляют отображение файлов на оперативную память.
Протокол динамического обмена данными (Dynamic Data Exchange, DDE), который определяет все основные функции для обмена данными между приложениями. DDE широко использовался до тех пор, пока Microsoft не приняла решение использовать OLE, которую затем переименовали в ActiveX, в качестве основной технологии взаимодействия программ.
Удаленный вызов процедур (Remote Procedure Call, RPC), строго говоря, не является механизмом IPC. Это скорее, технологическая оболочка, расширяющая возможности традиционных механизмов IPC. Благодаря RPC сеть становится совершенно прозрачной как для сервера, так и для клиента.
ActiveX является универсальной технологией, и одним из ее применений является обмен данными между процессами. Специально для этой цели Microsoft определила стандартный интерфейс IDataObject А для обмена данными по сети используется Distibuted Component Object Model (DCOM), которую можно рассматривать как объединение ActiveX и RPC.
Каналы (pipes) — мощная технология обмена данными. В общем случае канал можно представить в виде трубы, соединяющей два процесса, через которую идет непрерывный поток данных. Каналы делятся на анонимные (anonymous pipes) и именованные (named pipes). Анонимные каналы используются довольно редко. Именованные каналы, которые поддерживаются только в системах WinNT/2000, передают произвольные данные и могут работать через сеть. В последнее время вместо именованных каналов все чаще используют сокеты.
Сокеты (sockets) — очень важная технология, так как именно она отвечает за обмен данными в сети Интернет. Сокеты также часто используются в крупных локальных сетях. Сокеты можно рассматривать как «разъемы», представляющие собой абстракцию конечных точек коммуникационной линии, которая соединяет два приложения. Windows содержит достаточно мощный API для работы с сокетами.
Почтовые слоты (mailslots) — это механизм однонаправленного IPC. Если приложению известно имя слота, то оно может помещать туда сообщения, а приложение, которое является владельцем этого слота, может их оттуда извлекать и обрабатывать. Основное преимущество этого способа заключается в возможности передавать сообщения по локальной сети сразу нескольким компьютерам за одну операцию, если, конечно, несколько приемников имеют почтовые слоты с одним и тем же именем.
Microsoft Message Queue (MSMQ) — обеспечивает посылку сообщений между приложениями с помощью очереди сообщений. В отличие от других форм IPC, эта технология позволяет посылать сообщения процессу, который в данное время недоступен, например, если приложение не запущено, сервер вышел из строя или сетевой канал связи перегружен. Механизм MSMQ ставит сообщение в очередь до тех пор, пока не появится возможность переслать его адресату.
В данной книге будут рассмотрены только два механизма обмена данными между процессами: обмен с помощью разделяемой памяти (или объекта ядра «проекция файла») и обмен при помощи сообщения WM_COPYDATA.
Но сначала — несколько слов об архитектуре памяти Win32.


июля 30 2010

Физическая память и страничный файл

Так как на современных компьютерах оперативная память RAM имеет размеры, по крайней мере, на порядок меньше 4 Гбайт, то система имитирует задекларированную память за счет дискового пространства. При этом на диске создается страничный файл (page file), который вместе с физической памятью RAM образует виртуальную память, доступную всем процессам. Другое название страничного файла — файл подкачки (swap file).
Процессор сам управляет отображением виртуальных адресов из машинных команд в эквивалентные физические адреса в ходе выполнения команды. Осуществляя это отображение, процессор оперирует страницами памяти размером 4 Кбайта. Этот же размер страниц используется Windows для управления виртуальной памятью.
Теперь посмотрим, что происходит, когда поток пытается получить доступ к блоку данных в адресном пространстве своего процесса. В этом случае возможны три ситуации:
Данные, к которым обращается поток, находятся в оперативной памяти. Тогда процессор проецирует виртуальный адрес данных на физический, и поток получает доступ к этим данным.
Данные отсутствуют в оперативной памяти, но размещены где-то в страничном файле. Попытка доступа к данным генерирует прерывание, называемое Page Fault (Ошибка страницы). Тогда система начинает искать свободную страницу в оперативной памяти. Если такой страницы нет, то система вынуждена освободить одну из занятых страниц. Если занятая страница не модифицировалась, она просто освобождается. В ином случае она сначала копируется из оперативной памяти в страничный файл. После этого система отыскивает в страничном файле запрошенный блок данных, загружает этот блок на свободную страницу оперативной памяти и, наконец, проецирует виртуальный адрес данных на соответствующий адрес в физической памяти.
Иногда из-за программной ошибки или сбоя в аппаратной части требуемая страница отсутствует и в оперативной памяти, и в страничном файле. Тогда система генерирует ошибку Invalid Page Fault, и работающее приложение закрывается.


июля 19 2010

Архитектура интерфейсов (API) управления памятью

Диспетчер виртуальной памяти (Virtual Memory Manager — VMM) является составной частью ядра операционной системы. Он отображает виртуальные адреса на физические, используя механизм подкачки страниц памяти (page swapping). Кроме того, он предоставляет прикладным программам различные интерфейсы (API) для работы с виртуальной памятью:
Virtual Memory API — набор функций, позволяющих приложению работать с виртуальным адресным пространством. Например, функции VirtualAlloc и VirtualFree позволяют процессу получать страницы из памяти или возвращать их системе.
Memory Mapped File API — набор функций, позволяющих работать с файлами, проецируемыми в память. Это новый механизм, предоставляемый Windows для работы с файлами и взаимодействия процессов.
Heap Memory API — набор функций, позволяющих работать с динамически распределяемыми областями памяти (кучами).


июля 04 2010

Файлы данных, проецируемые в память

Проецирование файла данных в адресное пространство процесса предоставляет разработчику мощный механизм работы с файлами. Спроецировав файл на адресное пространство процесса, программа получает возможность работать с ним, как с массивом. Это очень удобно при манипуляциях с большими потоками данных. Для проецирования файла в память необходимо выполнить три операции:
1. Создать объект ядра «файл», идентифицирующий дисковый файл, который вы хотите использовать как проецируемый в память (функция CreateFile).
2. Создать объект ядра «проекция файла» при помощи функции CreateFileMapping. При этом используется дескриптор файла, возвращенный функцией CreateFile.
3. Указать отображение объекта «проекция файла» или его части на адресное пространство процесса (функция MapViewOfFile).
Закончив работу с проецируемым в память файлом, тоже следует выполнить три операции:
1. Отменить отображение на адресное пространство процесса объекта «проекция файла» (функция UnmapViewOfFile).
2. Закрыть объект ядра «проекция файла».
3. Закрыть объект ядра «файл».
Описанную технологию можно проиллюстрировать следующим фрагментом кода:
HANDLE hFile. hFileMapping:
PVOID pArray;
hFile = CreateFileC'File Name", ... );
hFileMapping = CreateFileMapping(hFile, ... ):
CloseHandle(hFile) :
pArray - MapViewOfFile(hFileMapping. ... );
CloseHandle(hFileMapping) ;
//
/ Работаем с файлом, как с массивом рАггау /
//
UnmapVi ewOfFi1е(pArray);
В этом примере «закрывающие» операции выполняются сразу после использования соответствующего дескриптора объекта. Это уменьшает вероятность утечки ресурсов.


Следующая страница »