- •8.1. Общие сведения о потоках
- •8.2. Синхронизация потоков
- •При работе с критическими секциями всегда необходимо следить за порядком вхождения в критические секции, чтобы избегать ситуации взаимной блокировки.
- •8.2.1. Синхронизация потоков объектами ядра
- •8.2.2. Функции ожидания объектов ядра (wait - функции)
При работе с критическими секциями всегда необходимо следить за порядком вхождения в критические секции, чтобы избегать ситуации взаимной блокировки.
При работе с критическими секциями не рекомендуется использовать функции типа SendMessage в области критической секции, т.к. она может не возвращать управление неопределённо долго, и ресурс останется заблокированным, увеличивая конкуренцию потоков, ожидающих доступа.
8.2.1. Синхронизация потоков объектами ядра
Механизмы синхронизации в пользовательском режиме (рассмотренные выше) обеспечивают высокое быстродействие, но эти механизмы имеют много ограничений, и в большинстве приложений их недостаточно. Механизмы синхронизации объектами ядра предоставляют большую функциональность, единственный их недостаток – это меньшее быстродействие.
Некоторые объекты ядра, как синхронизируемые, так и синхронизирующие могут находиться в двух состояниях: свободном (signaled state) и занятом (nonsignaled state). Переход из одного состояния в другое осуществляется по правилам, определённым для каждого из объектов ядра отдельно. Внутри объекта ядра находится булева переменная, которая хранит состояние объекта ядра (FALSE – занят, TRUE – свободен). Объект ядра «процесс»/«поток» пребывает в занятом (FALSE) состоянии, пока выполняется сопоставленный с ним процесс/поток, и переходит в свободное (TRUE) состояние, когда процесс/поток завершается. Кроме того, потоки, в отличие от процессов, могут существовать в свободном (TRUE) состоянии не завершаясь, а засыпая. Для введения потока в свободное (TRUE) состояние существуют Wait-функции. Они переводят потоки из режима пользователя в режим ядра. В двух состояниях могут находиться такие объекты ядра как: процессы, потоки, задания, события, семафоры, мьютексы, ожидаемые таймеры, файлы, консольный ввод, уведомления об изменении файлов.
К объектам ядра, которые можно использовать для синхронизации относятся:
Мъютексы (mutex) — это глобальные объекты, при помощи которых возможно организовать последовательный доступ к ресурсам. Сначала устанавливается мьютекс, затем происходит некоторое действие и после этого освобождается мьютекс. Если мьютекс установлен, любой другой поток (или процесс), который попытается установить тот же мьютекс, будет остановлен до того момента, когда мьютекс будет освобожден предыдущим потоком (или процессом). Мьютекс может совместно использоваться различными приложениями.
Мьютексы применяются для синхронизации доступа к ресурсу нескольких потоков из разных процессов, - при этом можно задавать максимальное время ожидания доступа к ресурсу. Вообще, мьютексы ведут себя подобно критическим секциям, и если имеется возможность заменить их критическими секциями, то рекомендуется это сделать, т.к. последние имеют значительно более высоко быстродействие. Создать мьютекс можно функцией CreateMutex, а открыть его функцией - OpenMutex.
При работе с мьютексами необходимо следовать следующим правилам:
Если его идентификатор потока равен 0 (поток не может иметь такой идентификатор), мьютекс не захвачен ни одним из потоков и находится в свободном состоянии.
Если его идентификатор не равен 0, мьютекс захвачен одним из потоков и находится в занятом состоянии.
Захват мьютекса осуществляется Wait-функцией, при этом значение идентификатор потока принимает значение захватившего его потока.
Освободить мьютекс можно функцией ReleaseMutex.
В отличие от других объектов ядра, мьютекс можно захватить несколько раз одним и тем же потоком, при этом будет увеличиваться счётчик рекурсии.
Функция ReleaseMutex - это функция освобождения мьютекса.
Семафоры (semaphor) — аналогичны мьютексам, но при их работе подсчитывается число обращений. Семафор – синхронизирующий объект ядра, который содержит счётчик числа пользователей и два 32-разрядных целых числа со знаком: счётчик текущего числа ресурсов от 0 и до значения максимального числа ресурсов (контролируемого семафором). Посредством семафора можно разрешить доступ к ресурсу, например, не более чем трем потокам одновременно. Мьютекс это тот же семафор с максимальным значением счетчика, равным 1. Создать семафор можно функцией CreateSemaphore, а открыть его - функцией OpenSemaphore. При работе с семафорами необходимо следовать таким правилам:
Если счётчик числа ресурсов равен нулю – семафор занят.
Если счётчик числа ресурсов больше, чем нуль – семафор свободен.
Счётчик числа ресурсов не может быть меньше нуля или больше, чем максимальное значение счётчика ресурсов.
Каждое успешное ожидание семафора Wait-функцией уменьшает значение счётчика числа ресурсов на 1, т. е. поток захватывает ресурс.
Функция ReleaseSemaphore увеличивает значение счётчика числа пользователей на 1, может вызываться потоком, закончившим работать с ресурсом, или при поступлении клиентского запроса.
Потоки, ожидающие доступ через семафор, получают его по принципу работы стека FIFO (First Input First Output) – первый поток, ставший на очередь ожидания получит доступ первым.
Семафор может использоваться в двух случаях. Во первых, когда необходимо предоставить доступ к ресурсу ограниченному числу потоков или точнее, когда на ресурс претендует большее количество потоков, чем может себе это позволить ресурс. Во вторых, когда организуется буфер хранения клиентских запросов, и когда клиентские запросы обрабатываются серверными потоками, В обоих случаях это называют очередью. Увеличить значение счётчика текущего числа ресурсов можно функцией ReleaseSemaphore.
События (event) — могут применяться как средство синхронизации потока с системными событиями, такими как пользовательские операции с файлами. Метод WaitFor класса TThread (Delphi, C++ Builder) использует событие. События могут также использоваться для того, чтобы активизировать одновременно несколько потоков. Событие – самая простая разновидность синхронизирующего объекта ядра, используется для уведомления об окончании какой-либо операции. Событие содержит счётчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сообщает тип данного объекта ядра «событие» (с авто-сбросом или без), другая – его состояние (свободен или занят). Объект ядра «событие» бывает двух типов:
со сбросом вручную (manual-reset events) – вызов Wait-функции никак не влияет на состояние объект ядра «событие», что позволяет возобновить сразу несколько ждущих потоков и предоставить им доступ «только для чтения».
с авто-сбросом (auto-reset events) – при успешном ожидании Wait-функция автоматически переводит объект ядра «событие» в занятое состояние, что позволяет возобновить только один ждущий поток, он получит доступ, как для чтения, так и для записи.
Создать объект ядра «событие» можно при помощи функции CreateEvent. В числе параметров этой функции есть два булевых параметра. Один параметр bManualReset определяет тип объекта ядра «событие». Если он равен TRUE, то объект ядра «событие» – со сбросом вручную, если FALSE – с авто-сбросом. Параметр bInitialState определяет начальное состояние объекта ядра «событие». Если он равен TRUE, то объект ядра «событие» имеет «свободное» состояние, а если он равен FALSE – «занятое» состояние.
При помощи функции OpenEvent можно открыть объект ядра «событие». Для управления состоянием объекта ядра «событие» можно использовать такие функци: SetEvent - переводит объект ядра «событие» в свободное состояние, ResetEvent - переводит объект ядра «событие» в занятое состояние. Далее в примере рассматривается использование объекта ядра «событие» с авто-сбросом
Пример 8.2. Использование объекта ядра «событие» с авто-сбросом
HANDLE hMyEvent; // глобальный дескриптор события
int WINAPI WinMain(...)
{
hMyEvent = CreateEvent( NULL, FALSE, FALSE, NULL);
HANDLE hMyThread[2];
DWORD dwThreadId;
hMyThread[0] = _beginthreadex( NULL, ColorCorrector, NULL, 0, &dwThreadId);
hMyThread[1] = _beginthreadex( NULL, ContrastCorrector, NULL, 0, &dwThreadId);
LoadImage(...);
// теперь один из потоков получит доступ к ресурсу (изображению)
SetEvent( hEvent );
...
}
//*************************************************************************
DWORD WINAPI ColorCorrection(LPVOID lpParam)
{
WaitForSingleObject( hMyEvent, INFINITE );
// событие автоматически переводится в занятое состояние
// получен доступ к изображению и выполняем коррекцию цвета изображения
SetEvent( hEvent ); // освобождаем объект ядра «событие»
return(0);
}
//*************************************************************************
DWORD WINAPI ContrastCorrector(LPVOID lpParam)
{
WaitForSingleObject( hMyEvent, INFINITE );
// событие автоматически переводится в занятое состояние
// получен доступ к изображению и выполняем коррекцию контраста изображения
SetEvent( hEvent ); // освобождаем объект ядра «событие»
return(0);
}
Некоторые замечания по примеру. В данном примере создается два дочерних потока, которые осуществляют обработку одного изображения. Цель данного примера - показать, каким образом использовать объект ядра «событие» для синхронизации доступа к одному объекту. После вызова первичным потоком функции SetEvent система возобновит выполнение только одного потока из двух. Закончив работу с данными, поток вызывает SetEvent, которая разрешает системе возобновить выполнение следующего из ждущих потоков. Использование события с авто-сбросом снимает проблему с доступом вторичных потоков к памяти, как для чтения, так и для записи.