Объектами ядра ос windows специально предназначенными для синхронизации потоков являются

Работа по теме: Основы многопоточного и параллельного программирования by Карепова Е.Д. (z-lib.org). Глава: 3.5. Синхронизация потоков с помощью объектов ядра. Предмет: Параллельное программирование. ВУЗ: СФУ.

Г л а в а 3. Управление потоками с помощью функций WinAPI

Следует отметить, что поскольку язык Си изначально не снабжен средствами многопоточности, то все стандартные функции (ввода-вывода, строковые и символьные, математические, времени, даты и локализации, динамического распределения памяти, работы с очередями, служебные) неатомарны.

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

Наиболее общий механизм синхронизации потоков в WinAPI основан на использовании специальных объектов ядра: мьютексов (mutexes), семафоров (semaphores) и событий (events). В отличие от синхронизации в пользовательском режиме с помощью объектов ядра можно синхронизировать потоки, принадлежащие разным процессам.

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

Wait-функции

Почти каждый объект ядра Windows может пребывать в одном из двух состояний – свободном (signaled state) или занятом (unsignaled state).

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

Переход из одного состояния в другое осуществляется по особым правилам, определенным разработчиками для каждого типа объектов ядра. Например, объект типа «процесс» или «поток» сразу после создания переходит в занятое состояние и становится свободным, когда сопоставленное с ним исполнение команд завершается. Таким образом, по переходу объекта «поток» или «процесс» в свободное состояние можно определить, что поток или процесс завершен.

Потоки могут «засыпать» и в таком состоянии ждать освобождения какого-либо объекта. Для этого используется семейство Wait-функций.

Wait-функции позволяют потоку в любой момент приостановиться и ожидать освобождения какого-либо объекта ядра.

DWORD WaitForSingleObject(HANDLE hObject,DWORD dwMilliseconds);

120

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

Функция ожидает освобождения одного объекта ядра, который идентифицирован описателем, передаваемым в первом параметре. Второй параметр указывает время в миллисекундах, которое будет ждать поток. Вместо второго параметра можно передать слово INFINITE, что будет означать, что поток готов ждать «вечно». Например, по команде

WaitForSingleObject(hProc, INFINITE);

поток будет «вечно» ждать, пока не завершится процесс, на который указывает описатель hProc.

Wait-функция возвращает значение типа DWORD, проанализировав которое, можно определить, чем конкретно закончилось ожидание.

Код примера 3.6 позволяет выяснить результат срабатывания Wait— функции. Используемая Wait-функция сообщает системе, что вызывающий поток не должен получать процессорное время до тех пор, пока не завершится указанный процесс или не пройдет 5000 мс. Возвращаемое значение функции указывает, почему поток снова стал планируемым. Если функция возвращает WAIT_OBJECT_0, ожидаемый объект свободен, если WAIT_TIMEOUT – заданное время ожидания истекло. Относительно другого объекта ядра Wait-функция может возвращать и другие значения.

DWORD WaitForMultipleObjects(DWORD dwCount, HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds);

Пример 3.6. Фрагмент кода обработки результата срабатывания Wait-функции

DWORD dw = WaitForSingleObject(hProc, 5000); switch(dw){

case WAIT_OBJECT_0: //процесс успешно завершился

… //код обработки break;

case WAIT_TIMEOUT: //процесс не завершился за 5000 мс, //ожидание прервано

… //код обработки break;

case WAIT_FAILED: //неправильный вызов функции

… // код обработки break;

}

Функция позволяет ожидать освобождения сразу нескольких или одного из списка объектов. Первый параметр dwCount определяет количество интересующих объектов ядра. Параметр phObjects – это указатель на массив описателей ядра. Параметр fWaitAll указывает, следует ли ждать освобождения всех объектов (TRUE) или хотя бы одного (FALSE).

121

Г л а в а 3. Управление потоками с помощью функций WinAPI

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

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

События

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

Как правило, события используются для уведомления об окончании какой-либо операции. Объекты ядра «событие» бывают двух видов: со сбросом вручную (manual-reset event) и с автосбросом (auto-reset event).

Первые позволяют возобновить выполнение сразу нескольких ожидающих потоков, вторые – только одного.

Рассмотрим функции WinAPI, работающие с объектом ядра «собы-

тие».

HANDLE CreateEvent(PSECURITY_ATTRIBUTES psa,BOOL fManualReset, BOOL fInitialState, PCTSTR pszName);

Функция создает объект ядра «событие».

Параметр psa указывает на настройки безопасности (как правило, здесь передается NULL, выше эти настройки обсуждались более подробно).

Параметр fManualReset определяет, будет ли событие со сбросом вручную (TRUE) или с автосбросом (FALSE).

Параметр fInitialState определяет начальное состояние события – свободное (TRUE) или занятое (FALSE).

Параметр pszName позволяет задать событию какое-нибудь имя. Имя позволяет другим процессам получить описатель данного события. Для этого они могут либо использовать CreateEvent() с таким же параметром pszName, либо открыть событие функцией OpenEvent().

HANDLE OpenEvent(DWORD fdwAccess, BOOL fInherit,PCTSTR pszName);

Функция открывает объект ядра «событие». Последний параметр, pszName, определяет имя объекта ядра. Он всегда должен содержать адрес строки с нулевым символом в конце (передавать NULL нельзя). Функции OpenHandle() просматривают единое пространство имен объектов ядра,

122

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

пытаясь найти совпадение. Если объекта ядра «событие» с указанным именем нет или он другого типа, Open-функции возвращают NULL. Но если объект ядра «событие» с заданным именем существует, система проверяет, разрешен ли к данному объекту доступ запрошенного (через параметр fdwAccess) вида. Если доступ разрешен, таблица описателей в вызывающем процессе обновляется, и счетчик числа пользователей объекта возрастает на единицу. Если присвоить параметру fInherit значение TRUE, то будет получен наследуемый описатель.

BOOL CloseHandle(HANDLE hObj);

Функция закрывает описатель hObj объекта ядра «событие».

BOOL SetEvent(HANDLE hEvent);

Функция переводит событие hEvent в свободное состояние. Если событие уже было свободно, то никаких действий функция не производит.

BOOL ResetEvent(HANDLE hEvent);

Функция переводит событие hEvent в занятое состояние.

Важным отличием событий с автосбросом и ручным сбросом является их реакция на вызванную по отношению к ним Wait-функцию.

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

ResetEvent() реализуют функцию P(), а SetEvent() – функцию V().

Для событий с ручным сбросом никаких побочных эффектов не предусмотрено, поэтому только ResetEvent() реализуют функцию P(). Крупномодульное представление Wait-функций можно представить в на-

шей нотации следующим оператором ожидания:

<await (Event>0)>;

Поскольку Wait-функция не занимает событие, освобождение события с ручным сбросом могут дождаться сразу несколько потоков.

Рис. 3.2 демонстрирует аналогию между нотацией семафоров, введенной в гл. 2, и объектом ядра «событие».

Ниже приведены два примера использования события – с ручным сбросом и автосбросом соответственно.

В примере 3.7 поток ThreadOne() готовит набор данных. Пока происходит подготовка, потоки ThreadSecond() и ThreadThird() ожидают ее завершения. Как только первый поток завершает формирование данных, он освобождает событие hEvent. Поскольку событие инициализировано

123

Г л а в а 3. Управление потоками с помощью функций WinAPI

с ручным автосбросом, то после освобождения события оба ожидавших его потока могут начать обработку данных.

СОБЫТИЕ

с автосбросом

с ручным сбросом

нотация

HANDLE hEvent;

HANDLE hEvent;

hEvent = CreateE-

sem

hEvent = CreateEvent(NULL,

vent(NULL,FALSE,

Event=0;

FALSE,«Event»);

TRUE,FALSE,«Event»);

установить

СОБЫТИЕ в значение «свободно»

SetEvent(Event);

V(Event):

<Event=0;>

установить СОБЫТИЕ в значение «занято»

ResetEvent(Event);

P(Event):

<await

WaitForSingleObject

ResetEvent(Event);

(Event>0)

(Event,INFINITE);

Event—;>

дождаться освобождения СОБЫТИЯ, не занимая его

нет

WaitForSingleObject(Event,

<await

INFINITE);

(Event>0);>

Рис. 3.2. Объект ядра «событие» и двоичный семафор

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

HANDLE hEvent;

DWORD WINAPI ThreadOne(PVOID pvParam){

… //блок подготовки данных

SetEvent(hEvent); return 0;

}

DWORD WINAPI ThreadSecond(PVOID pvParam){ WaitForSingleObject(hEvent,INFINITE);

… //блок обработки данных, подготовленных потоком ThreadOne

return 0;

}

DWORD WINAPI ThreadThird(PVOID pvParam){ WaitForSingleObject(hEvent, INFINITE);

… //блок обработки данных, подготовленных потоком ThreadOne

return 0;

}

int main(int argc, char** argv){

hEvent = CreateEvent(NULL,TRUE,FALSE,”ЕventBeginExecute”); HANDLE hThread[3];

… //создание потоков ThreadOne, ThreadTwo и ThreadThird

… //ожидание завершения работы потоков

CloseHandle(hEvent); return 0;

}

124

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

Впримере 3.8 подобным образом используется событие с автосбросом. Обратите внимание на выделенные изменения в коде. Поскольку первый поток освобождает событие с автосбросом, то блок данных, им

подготовленный, сначала обработает один из потоков ThreadSecond() иThreadThird(), азатемдругой(приэтомпорядокобработкинеопределен).

Пример 3.8. Фрагмент кода, демонстрирующий использование события с автосбросом

HANDLE hEvent;

DWORD WINAPI ThreadOne(PVOID pvParam){

… //блок подготовки данных

SetEvent(hEvent); return 0;

}

DWORD WINAPI ThreadSecond(PVOID pvParam){ WaitForSingleObject(hEvent, INFINITE);

…//блок обработки данных, подготовленных потоком ThreadOne SetEvent(hEvent);

return 0;

}

DWORD WINAPI ThreadThird(PVOID pvParam){ WaitForSingleObject(hEvent, INFINITE);

… //блок обработки данных, подготовленных потоком ThreadOne SetEvent(hEvent);

return 0;

}

int main(int argc, char** argv){

hEvent = CreateEvent(NULL,FALSE,FALSE,”EventBeginExecute”); HANDLE hThread[3];

//создание потоков ThreadOne, ThreadTwo и ThreadThird

… //ожидание завершения работы потоков

CloseHandle(hEvent); return 0;

}

Семафоры

Объект ядра «семафор» используется для учета ресурсов. Семафор представлен счетчиком числа пользователей и двумя значениями: максимальным числом ресурсов, которое может захватить семафор, и счетчиком текущего числа свободных ресурсов.

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

125

Г л а в а 3. Управление потоками с помощью функций WinAPI

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

Рассмотрим функции, определенные для семафоров.

HANDLE CreateSemaphore(PSECURITY_ATTRIBUTES psa, LONG lInitialCount,BOOL lMaximumCount, PCTSTR pszName);

Функция создает объект ядра «семафор». Параметры psa и pszName аналогичны описанным для событий. Параметр lMaximumCount показывает максимально возможное число ресурсов, обрабатываемых семафором. Параметр lInitialCount устанавливает начальное значение счетчика текущего числа свободных ресурсов.

Например, при вызове

hSem = CreateSemaphore(NULL, 2, 5, «MySemaphore»);

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

Любой процесс может получить свой процессозависимый описатель существующего объекта «семафор», вызвав функцию

HANDLE OpenSemaphore(DWORD fdwAccess,BOOL fInherit, PCTSTR pszName);

Функция открывает существующий объект ядра «семафор». Параметры функции аналогичны соответствующей функции для события.

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

BOOL ReleaseSemaphore(HANDLE hSem, LONG lReleaseCount, LONG* plPreviousCount);

Функция освобождает определенное число ресурсов. Первый параметр hSem указывает на освобождаемый семафор, второй lReleaseCount содержит число освобождаемых ресурсов, а в третий *plPreviousCount функция возвращает предыдущее (до вызова функции) значение счетчика текущего числа свободных ресурсов. Таким образом, функция

126

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

ReleaseSemaphore() реализует несколько расширенную V()-функцию семафора.

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

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

HANDLE hSem;

DWORD WINAPI ThreadFunc(PVOID pvParam){ while(TRUE){

WaitForSingleObject(hSem, INFINITE); //P()

… //обработка защищенного блока данных

ReleaseSemaphore(hSem, 1, NULL); //V()

}

return 0;

}

int main(int argc, char** argv){

hSem = CreateSemaphore(NULL, 1, 1, «ForCS»);

…//создание нескольких потоков, исполняющих код ThreadFunc CloseHandle(hSem);

return 0;

}

Заметим, что пример 3.9 реализует классическое решение задачи критической секции, но если потоки относятся к одному процессу, то использование структуры CRITICAL_SECTION значительно снизит накладные расходы на такую защиту. Если же потоки принадлежат различным процессам, то более логично использование другого объекта ядра – мьютекса.

Мьютексы

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

Рассмотрим функции, определенные для мьютексов.

HANDLE CreateMutex(PSECURITY_ATTRIBUTES Ipsa,

BOOL fInitialOwner, LPTSTR IpszMutexName);

Функция создает объект ядра «мьютекс».

Параметр Ipsa указывает на структуру SECURITY_ATTRIBUTES.

127

Г л а в а 3. Управление потоками с помощью функций WinAPI

Параметр fInitialOwner определяет, должен ли поток, создающий мьютекс, быть первоначальным владельцем этого объекта. Если он равен TRUE, данный поток становится владельцем созданного мьютекса, а объ- ект-мьютекс оказывается в занятом состоянии. Любой другой поток, ожидающий данный мьютекс, будет приостановлен, пока поток, создавший этот объект, не освободит его. Передача FALSE в параметре flnitialOwner подразумевает, что объект-мьютекс не принадлежит ни одному из потоков и поэтому «рождается свободным». Любой поток, ожидающий освобождения этого объекта, может занять мьютекс, тем самым продолжить свое выполнение.

Параметр IpszMutexName содержит либо NULL, либо адрес строки (с нулевым символом в конце), идентифицирующей мьютекс. Когда приложение вызывает CreateMutex(), система создает объект ядра «мьютекс» и присваивает ему имя, на которое указывает параметр IpszMutexName. Это имя используется при совместном доступе к нему нескольких процессов.

CreateMutex() возвращает процессозависимый описатель, определяющий созданный объект-мьютекс.

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

HANDLE OpenMutex (DWORD fdwAccess,BOOL fInherit,PCTSTR IpszName);

Функция открывает существующий объект ядра «мьютекс». Параметр fdwAccess может быть равен либо SYNCHRONIZE, либо

MUTEX_ALL_ACCESS. Параметр fInherit определяет, унаследует ли дочерний процесс описатель данного объекта-мьютекса. Параметр IpszName – это имя объекта-мьютекса в виде строки с нулевым символом в конце.

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

Поток занимает объект-мьютекс в результате успешного завершения ожидания одной из Wait-функций (например, WaitForSingleObject()).

BOOL ReleaseMutex(HANDLE hMutex);

Функция возвращает мьютекс в свободное состояние. Параметр hMutex – это указатель того мьютекса, который освобождается.

128

Critical Sections

В составе API ОС Windows имеются специальные и эффективные функции для организации входа в критическую секцию и выхода из нее потоков одного процесса в режиме пользователя. Они называются EnterCriticalSection и LeaveCriticalSection и имеют в качестве параметра предварительно проинициализированную структуру типа CRITICAL_SECTION.

Примерная схема программы может выглядеть следующим образом.

CRITICAL_SECTION cs; 
DWORD WINAPI SecondThread() 
{ 
 InitializeCriticalSection(&cs);

EnterCriticalSection(&cs); 
… критический участок кода
LeaveCriticalSection(&cs); 
} 

main () 
{
InitializeCriticalSection(&cs);
CreateThread(NULL, 0, SecondThread,…);


EnterCriticalSection(&cs); 
… критический участок кода
LeaveCriticalSecLion(&cs); 
DeleteCriticalSection(&cs);
}

Функции EnterCriticalSection и LeaveCriticalSection реализованы на основе Interlocked-функций, выполняются атомарным образом и работают очень быстро. Существенным является то, что в случае невозможности входа в критический участок поток переходит в состояние ожидания. Впоследствии, когда такая возможность появится, поток будет «разбужен» и сможет сделать попытку входа в критическую секцию. Механизм пробуждения потока реализован с помощью объекта ядра «событие» (event), которое создается только в случае возникновения конфликтной ситуации.

Уже говорилось, что иногда, перед блокированием потока, имеет смысл некоторое время удерживать его в состоянии активного ожидания. Чтобы функция EnterCriticalSection выполняла заданное число циклов спин-блокировки, критическую секцию целесообразно проинициализировать с помощью функции InitalizeCriticalSectionAndSpinCount.

Прогон программы

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

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

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

Почти все объекты ядра, рассмотренные ранее, в том числе, процессы, потоки и файлы, пригодны для решения задач синхронизации. В контексте задач синхронизации о каждом из объектов можно сказать, находится ли он в свободном (сигнальном, signaled state) или занятом (nonsignaled state) состоянии. Правила перехода объекта из одного состояния в другое зависят от объекта. Например, если поток выполняется, то он находится в занятом состоянии, а если поток успешно завершил ожидание семафора, то семафор находится в занятом состоянии.

Потоки находятся в состоянии ожидания, пока ожидаемые ими объекты заняты. Как только объект освобождается, ОС будит поток и позволяет продолжить выполнение. Для приостановки потока и перевода его в состояние ожидания освобождения объекта используется функция

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);

где hObject — описатель ожидаемого объекта ядра, а второй параметр — максимальное время ожидания объекта.

Поток создает объект ядра при помощи семейства функций Create ( CreateSemaphore, CreateThread и т.д.), после чего объект посредством описателя становится доступным всем потокам данного процесса. Копия описателя может быть получена при помощи функции DuplicateHandle и передана другому процессу, после чего потоки смогут воспользоваться этим объектом для синхронизации.

Другим, более распространенным способом получения описателя является открытие существующего объекта по имени, поскольку многие объекты имеют имена в пространстве имен объектов.
Имя объекта — один из параметров Create -функций. Зная имя объекта, поток, обладающий нужными правами доступа, получает его описатель с помощью Open -функций. Напомним, что в структуре, описывающей объект, имеется счетчик ссылок на него, который увеличивается на 1 при открытии объекта и уменьшается на 1 при его закрытии.

Несколько подробнее рассмотрим те объекты ядра, которые предназначены непосредственно для решения проблем синхронизации.

Семафоры

Известно, что семафоры, предложенные Дейкстрой в 1965 г., представляет собой целую переменную в пространстве ядра, доступ к которой, после ее инициализации, может осуществляться через две атомарные операции: wait и signal (в ОС Windows это функции WaitForSingleObject и ReleaseSemaphore соответственно).

wait(S):  если S <= 0 процесс блокируется 
   (переводится в состояние ожидания);    
              в противном случае S = S - 1;                               
signal(S):  S = S + 1

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

Ниже приведен пример синхронизации программы async с помощью семафоров.

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

int Sum = 0, iNumber=5, jNumber=300000;
HANDLE hFirstSemaphore, hSecondSemaphore;

DWORD WINAPI SecondThread(LPVOID)
{
  int i,j;
  double a,b=1.;
    for (i = 0; i < iNumber; i++)
  {
    WaitForSingleObject(hSecondSemaphore, INFINITE); 
    for (j = 0; j < jNumber; j++)
    {
      Sum = Sum + 1;  a=sin(b); 
     }
    
     ReleaseSemaphore(hFirstSemaphore, 1, NULL);
    }
  return 0;
}

void main()
{
  int i,j;
  HANDLE  hThread;
  DWORD  IDThread;
  double a,b=1.;

  hFirstSemaphore = CreateSemaphore(NULL, 0, 1, "MyFirstSemaphore");
  hSecondSemaphore = CreateSemaphore(NULL, 1, 1, "MySecondSemaphore1");
  hThread=CreateThread(NULL, 0, SecondThread, NULL, 0, &IDThread);
  if (hThread == NULL) return;    
  
  for (i = 0; i < iNumber; i++)
  {
	WaitForSingleObject(hFirstSemaphore, INFINITE);
        for (j = 0; j < jNumber; j++)
    {
      Sum = Sum - 1;  a=sin(b);
	}
    	printf(" %d ",Sum);	
	ReleaseSemaphore(hSecondSemaphore, 1, NULL);  
  }	  

WaitForSingleObject(hThread, INFINITE); // ожидание окончания потока SecondThread
CloseHandle(hFirstSemaphore);
CloseHandle(hSecondSemaphore);
printf(" %d ",Sum);	
return;
}

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

Мьютексы

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

Если поток завершается, не освободив мьютекс, последний переходит в свободное состояние.
Отличие от семафоров в том, что поток, занявший мьютекс, получает права на владение им. Только этот поток может освободить мьютекс. Поэтому мнение о мьютексе как о семафоре с максимальным значением 1 не вполне соответствует действительности.

События

Объекты «события» — наиболее примитивные объекты ядра. Они предназначены для информирования одного потока другим об окончании какой-либо операции. События создаются функцией CreateEvent. Простейший вариант синхронизации: переводить событие в занятое состояние функцией WaitForSingleObject и в свободное — функцией SetEvent.

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

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

Суммарные сведения об объектах ядра

В руководствах по программированию, см., например,
[
Рихтер
]
, и в MSDN содержатся сведения и о других объектах ядра применительно к синхронизации потоков.

В частности, существуют следующие свойства объектов:

  • процесс и поток находятся в занятом состоянии, когда активны, и в свободном состоянии, когда завершаются;
  • файл находится в занятом состоянии, когда выдан запрос на ввод-вывод, и в свободном состоянии, когда операция ввода-вывода завершена;
  • уведомление об изменении файла находится в занятом состоянии, когда в файловой системе нет изменений, и в свободном — когда изменения обнаружены;
  • и т.д.

Синхронизация в ядре

Решение проблемы взаимоисключения особенно актуально для такой сложной системы, как ядро ОС Windows.

Одна из проблем связана с тем, что код ядра зачастую работает на приоритетных IRQL (уровни IRQL рассмотрены в
«Базовые понятия ОС Windows»
) уровнях «DPC/dispatch» или «выше», известных как «высокий IRQL». Это означает, что традиционные средства синхронизации, связанные с приостановкой потока, не могут быть использованы, поскольку процедура планирования и запуска другого потока имеет более низкий приоритет.
Вместе с тем существует опасность возникновения события, чей IRQL выше, чем IRQL критического участка, который будет в этом случае вытеснен. Поэтому в подобных ситуациях прибегают к приему, который называется «запрет прерываний»
[
Карпов
]
,
[
Таненбаум
]
. В случае Windows этого добиваются, искусственно повышая IRQL критического участка до самого высокого уровня, используемого любым возможным источником прерываний. В результате критический участок может беспрепятственно выполнить свою работу.

К сожалению, для мультипроцессорных систем подобная стратегия не годится. Запрет прерываний на одном из процессоров не исключает прерываний на другом процессоре, который может продолжить свою работу и получить доступ к критическим данным. В этом случае нужен специальный протокол установки взаимоисключения. Основой этого протокола является установка блокирующей переменной (переменой-замка), сопоставленной с каждой глобальной структурой данных, с помощью TSL команды. Поскольку установка замка происходит в результате активного ожидания, то говорят, что код ядра устанавливает (захватывает) спин-блокировку. Установка спин-блокировки происходит при высоких IRQL уровнях, поэтому код ядра, захватывающего спин-блокировку и удерживающего ее для выполнения критической секции кода, никогда не вытесняется. Установка и освобождение спин-блокировок осуществляется функциями ядра KeAcquireSpinlock и KeReleaseSpinlock, которые активно используются в ядре и драйверах устройств.
На однопроцессорных системах установка и снятие спин-блокировок реализуется простым повышением и понижением IRQL.

Наконец, имея набор глобальных ресурсов, в данном случае — спин-блокировок, необходимо решить проблему возникновения потенциальных тупиков
[
Сорокина
]
. Например, поток 1 захватывает блокировку 1, а поток 2 захватывает блокировку 2. Затем поток 1 пытается захватить блокировку 2, а поток 2 — блокировку 1. В результате оба потока ядра виснут. Одним из решений данной проблемы является нумерация всех ресурсов и выделение их только в порядке возрастания номеров
[
Карпов
]
. В случае Windows имеется иерархия спин-блокировок: все они помещаются в список в порядке убывания частоты использования и должны захватываться в том порядке, в каком они указаны в списке.

В случае низких IRQL синхронизация осуществляется традиционным образом — при помощи объектов ядра.

Заключение

Проблема недетерминизма является одной из ключевых в параллельных вычислительных средах. Традиционное решение — организация взаимоисключения. Для синхронизации с применением переменной-замка используются Interlocked-функции, поддерживающие атомарность некоторой последовательности операций. Взаимоисключение потоков одного процесса легче всего организовать с помощью примитива Crytical Section. Для более сложных сценариев рекомендуется применять объекты ядра, в частности, семафоры, мьютексы и события. Рассмотрена проблема синхронизации в ядре, основным решением которой можно считать установку и освобождение спин-блокировок.

Объекты синхронизации потоков

Объекты синхронизации потоков

До сих пор нами были обсуждены только два механизма, обеспечивающие синхронизацию процессов и потоков друг с другом:

1. Поток, выполняющийся в контексте одного процесса, может дожидаться завершения другого процесса с использованием функции ExitProcess путем применения к дескриптору процесса функций ожидания WaitForSingleObject или WaitForMultipleObject. Тем же способом поток может организовать ожидание завершения (с помощью функции ExitThread или выполнения оператора return) другого потока.

2. Блокировки файлов, предназначенные для частного случая синхронизации доступа к файлам.

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

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

В то же время, при этом возникают некоторые проблемы, связанные с производительностью, о чем говорится в главе 9.

Предостережение

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

Рассмотрение двух других объектов синхронизации — таймеров ожидания и портов завершения ввода/вывода — отложено до главы 14. Эти типы объектов требуют использования методик асинхронного ввода/вывода Windows, которые описываются в указанной главе.

Читайте также

Необходимость в синхронизации потоков

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

Обзор: объекты синхронизации Windows

Обзор: объекты синхронизации Windows
Наиболее важные свойства объектов синхронизации Windows перечислены в табл. 8.2.Таблица 8.2. Сравнительные характеристики объектов синхронизации Windows

CRITICAL_SECTION
Мьютекс
Семафор
Событие

Именованный защищаемый объект

Влияние синхронизации на производительность

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

ГЛАВА 10 Усовершенствованные методы синхронизации потоков

ГЛАВА 10
Усовершенствованные методы синхронизации потоков
В предыдущей главе были описаны проблемы производительности, возникающие в Windows, и способы их преодоления в реалистичных ситуациях. В главе 8 обсуждался ряд простых задач, требующих привлечения объектов

Стеки потоков и допустимые количества потоков

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

Резюмирование по синхронизации

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

Дополнительно о синхронизации

Дополнительно о синхронизации
Мы уже обсудили:• мутексы;• семафоры;• барьеры.Давайте теперь завершим нашу дискуссию о синхронизации, обсудив следующее:• блокировки чтения/записи (reader/writer locks);• ждущие блокировки (sleepons);• условные переменные (condition

4. Примитивы синхронизации

4. Примитивы синхронизации
ОС QNX Neutrino предоставляет широкий набор элементов синхронизации выполнения потоков, как в рамках одного процесса, так и разных. Это практически полный спектр примитивов, описываемых как базовым стандартом POSIX, так и всеми его расширениями

13.9.1 Сигнал синхронизации

13.9.1 Сигнал синхронизации
Для некоторых функций (например, Interrupt Process) включение команды в общий поток данных не приводит к нужным результатам. Когда реальный терминал посылает сигнал прерывания, хост операционной системы получает этот сигнал сразу и быстро останавливает

Центр синхронизации

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

Результаты синхронизации потоков

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

Результаты синхронизации процессов

Результаты синхронизации процессов
 В табл. А.4 и А.5 и на соответствующих рисунках были приведены результаты синхронизации потоков одного процесса. Интересно посмотреть, как взаимодействуют разные процессы. В табл. А.6 и на рис. А.5 приведены результаты измерения времени

Проблема конкуренции и роль синхронизации потоков

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

Объекты DataSet с множеством таблиц и объекты DataRelation

Объекты DataSet с множеством таблиц и объекты DataRelation
До этого момента во всех примерах данной главы объекты DataSet содержали по одному объекту DataTable. Однако вся мощь несвязного уровня ADO.NET проявляется тогда, когда DataSet содержит множество объектов DataTable. В этом случае вы можете

СРЕДНЕЕ ЗВЕНО: Проблемы синхронизации

СРЕДНЕЕ ЗВЕНО: Проблемы синхронизации
Алексей Дмитриев, операционный менеджер группы WiMAX в Motorola, начинал работу в этой компании с должности
инженера в отделе качества. Через год перешел в группу CDMA, где несколько лет проработал инженером по разработке и
поддержке

Функции синхронизации

   Функции, ожидающие единственный объект

   Функции, ожидающие несколько объектов

   Прерывание ожидания по запросу на завершение операции ввода-вывода
или APC

Объекты синхронизации

   Event (событие)

   Mutex (Mutually Exclusive)

   Semaphore (семафор)

   Waitable timer (таймер ожидания)

   Дополнительные объекты синхронизации

      Сообщение об изменении
папки (change notification)

      Устройство стандартного
ввода с консоли (console input)

      Задание (Job)

      Процесс (Process)

      Поток (thread)

Дополнительные механизмы синхронизации

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

   Защищенный доступ к переменным (Interlocked Variable Access)

Резюме

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

Главной идеей, заложенной в основе
синхронизации потоков в Win32,
является использование объектов
синхронизации и функций ожидания. Объекты
могут находиться в одном из двух состояний —
Signaled
или Not
Signaled. Функции
ожидания блокируют выполнение потока до
тех пор, пока заданный объект находится в
состоянии Not Signaled. Таким
образом, поток, которому необходим
эксклюзивный доступ к ресурсу, должен
выставить какой-либо объект синхронизации
в несигнальное состояние, а по окончании —
сбросить его в сигнальное. Остальные
потоки должны перед доступом к этому
ресурсу вызвать функцию ожидания, которая
позволит им дождаться освобождения ресурса.

Рассмотрим,
какие объекты и функции синхронизации
предоставляет нам Win32 API.

Функции синхронизации

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

Функции, ожидающие единственный объект

Простейшей функцией ожидания является
функция WaitForSingleObject:

function WaitForSingleObject(
     hHandle: THandle;       // идентификатор    объекта
     dwMilliseconds: DWORD   // период ожидания
   ): DWORD; stdcall; 

Функция ожидает перехода объекта hHandle в сигнальное состояние в течение dwMilliseconds
миллисекунд. Если в качестве параметра dwMilliseconds передать значение INFINITE,
функция будет ждать в течение неограниченного времени. Если dwMilliseconds равен
0, то функция проверяет состояние объекта и немедленно возвращает управление.

Функция
возвращает одно из следующих значений:

WAIT_ABANDONED 
Поток, владевший объектом, завершился, не переведя объект в сигнальное
состояние
WAIT_OBJECT_0 
Объект перешел в сигнальное состояние
WAIT_TIMEOUT 
Истек срок ожидания. Обычно в этом случае генерируется ошибка либо функция
вызывается в цикле до получения другого результата
WAIT_FAILED 
Произошла ошибка (например, получено неверное значение hHandle). Более
подробную информацию можно получить, вызвав GetLastError

Следующий фрагмент кода запрещает доступ
к Action1
до перехода объекта ObjectHandle
в сигнальное состояние (например, таким
образом можно дожидаться завершения
процесса, передав в качестве ObjectHandle
его идентификатор, полученный функцией CreateProcess):

var
     Reason: DWORD;
     ErrorCode: DWORD;
   
   Action1.Enabled := FALSE;
   try
     repeat
       Application.ProcessMessages;
       Reason := WailForSingleObject(ObjectHandle, 10);
       if Reason = WAIT_FAILED then begin
         ErrorCode := GetLastError;
         raise Exception.CreateFmt(
           ‘Wait for object failed with error:    %d’, [ErrorCode]);
       end;
     until Reason <> WAIT_TIMEOUT;
   finally
     Actionl.Enabled := TRUE;
   end; 

В случае когда одновременно с ожиданием объекта требуется перевести в сигнальное
состояние другой объект, может использоваться функция SignalObjectAndWait:

function SignalObjectAndWait(
     hObjectToSignal: THandle;  // объект, который будет переведен в
                                   // сигнальное состояние
     hObjectToWaitOn: THandle;  // объект, который     ожидает    функция
     dwMilliseconds: DWORD;     // период ожидания
     bAlertable: BOOL              // задает, должна ли функция возвращать
                                //    управление в случае запроса на
                                   // завершение операции ввода-вывода
   ): DWORD; stdcall; 

Возвращаемые значения аналогичны функции WaitForSingleObject.

! В модуле Windows.pas эта функция ошибочно объявлена как возвращающая
значение BOOL. Если вы намерены ее использовать – объявите ее корректно
или используйте приведение типа возвращаемого значения к DWORD.

Объект hObjectToSignal может быть семафором, событием (event) либо мьютексом.
Параметр bAlertable определяет, будет ли прерываться ожидание объекта в случае,
если операционная система запросит у потока окончание операции асинхронного
ввода-вывода либо асинхронный вызов процедуры. Более подробно это будет рассматриваться
ниже.

Функции, ожидающие несколько объектов

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

type
     TWOHandleArray = array[0..MAXIMUM_WAIT_OBJECTS - 1] of THandle;
     PWOHandleArray = ^TWOHandleArray;
   
   function WaitForMultipleObjects(
     nCount: DWORD;                 // Задает количество объектов
     lpHandles: PWOHandleArray;  // Адрес массива объектов
     bWaitAll: BOOL;                // Задает, требуется ли ожидание всех
                                    // объектов или любого
     dwMilliseconds: DWORD       // Период ожидания
   ): DWORD; stdcall; 

Функция возвращает одно из следующих значений:

Число в диапазоне от

       WAIT_OBJECT_0 до
       WAIT_OBJECT_0 + nCount – 1              
Если bWaitAll равно TRUE, то это число означает, что все объекты перешли
в сигнальное состояние. Если FALSE — то, вычтя из возвращенного значения
WAIT_OBJECT_0, мы получим индекс объекта в массиве lpHandles

Число в диапазоне от

       WAIT_ABANDONED_0 до
       WAIT_ABANDONED_0 + nCount – 1              
Если bWaitAll равно TRUE,  это означает, что все объекты перешли
в сигнальное состояние, но хотя бы один из владевших ими потоков завершился,
не сделав объект сигнальным Если FALSE — то, вычтя из возвращенного значения
WAIT_ABANDONED_0,  мы получим в массиве lpHandles индекс объекта,
при этом поток, владевший этим объектом,
завершился,  не сделав его сигнальным
WAIT_TIMEOUT 
Истек период ожидания
WAIT_FAILED 
Произошла ошибка

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

var
     Handles: array[0..1] of THandle;
     Reason: DWORD;
     RestIndex: Integer;
   
   ...
   
   Handles[0] := OpenMutex(SYNCHRONIZE, FALSE, ‘FirstResource’);
   Handles[1] := OpenMutex(SYNCHRONIZE, FALSE, ‘SecondResource’);
   // Ждем первый из объектов
   Reason := WaitForMultipleObjects(2, @Handles, FALSE, INFINITE);
   case Reason of
     WAIT_FAILED: RaiseLastWin32Error;
     WAIT_OBJECT_0, WAIT_ABANDONED_0:
       begin
         ModifyFirstResource;
         RestIndex := 1;
       end;
     WAIT_OBJECT_0 + 1, WAIT_ABANDONED_0 + 1:
       begin
         ModifySecondResource;
         RestIndex := 0;
       end;
     // WAIT_TIMEOUT возникнуть не может
   end;
   // Теперь ожидаем освобождения следующего объекта
   if WailForSingleObject(Handles[RestIndex],
        INFINITE) = WAIT_FAILED then
          RaiseLastWin32Error;
   // Дождались, модифицируем оставшийся ресурс
   if RestIndex = 0 then
     ModifyFirstResource
   else
    ModifySecondResource; 

Описанную выше технику можно применять,
если вы точно знаете, что задержка ожидания
объекта будет незначительной. В противном
случае ваша программа окажется «замороженной»
и не сможет даже перерисовать свое окно. Если
период задержки может оказаться
значительным, то необходимо дать программе
возможность реагировать на сообщения Windows. Выходом
может стать использование функций с
ограниченным периодом ожидания (и
повторный вызов — в случае возврата WAIT_TIMEOUT)
либо функции MsgWaitForMultipleObjects:

function MsgWaitForMultipleObjects(
   nCount: DWORD;     // количество объектов синхронизации
   var pHandles;      // адрес массива объектов
   fWaitAll: BOOL;    // Задает, требуется ли ожидание всех
                      // объектов или любого
   dwMilliseconds,    // Период ожидания
   dwWakeMask: DWORD  // Тип события, прерывающего ожидание
 ): DWORD; stdcall;  

Главное отличие этой функции от
предыдущей — параметр dwWakeMask,
который является комбинацией битовых
флагов QS_XXX
и задает типы сообщений, прерывающих
ожидание функции независимо от состояния
ожидаемых объектов. Например, маска QS_KEY
позволяет прервать ожидание при появлении
в очереди сообщений WM_KEYUP,
WM_KEYDOWN,
WM_SYSKEYUP
или WM_SYSKEYDOWN,
а маска QS_PAINT
— сообщения WM_PAINT.
Полный список значений, допустимых для dwWakeMask,
имеется в документации по Windows
SDK. При
появлении в очереди потока, вызвавшего
функцию, сообщений, соответствующих
заданной маске, функция возвращает
значение WAIT_OBJECT_0
+ nCount.
Получив это значение, ваша программа может
обработать его и снова вызвать функцию
ожидания. Рассмотрим пример с запуском
внешнего приложения (необходимо, чтобы на время его работы
вызывающая программа не реагировала на
ввод пользователя, однако ее окно должно
продолжать перерисовываться):

procedure TForm1.Button1Click(Sender: TObject);
 var
   PI: TProcessInformation;
   SI: TStartupInfo;
   Reason: DWORD;
   Msg: TMsg;
 begin
   // Инициализируем структуру TStartupInfo
   FillChar(SI, SizeOf(SI), 0);
   SI.cb := SizeOf(SI);
   // Запускаем внешнюю программу
   Win32Check(CreateProcess(NIL, 'COMMAND.COM', NIL,
     NIL, FALSE, 0, NIL, NIL, SI, PI));
 //**************************************************
 // Попробуйте заменить нижеприведенный код на строку
 // WaitForSingleObject(PI.hProcess, INFINITE);
 // и посмотреть, как будет реагировать программа на
 // перемещение других окон над ее окном
 //**************************************************
   repeat
     // Ожидаем завершения дочернего процесса или сообщения
     // перерисовки WM_PAINT
     Reason := MsgWaitForMultipleObjects(1, PI.hProcess, FALSE,
       INFINITE, QS_PAINT);
     if Reason = WAIT_OBJECT_0 + 1 then begin
       // В очереди сообщений появился WM_PAINT – Windows
       // требует обновить окно программы.
       // Удаляем сообщение из очереди
       PeekMessage(Msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE);
       // И перерисовываем наше окно
       Update;
     end;
     // Повторяем цикл, пока не завершится дочерний процесс
   until Reason = WAIT_OBJECT_0;
   // Удаляем из очереди накопившиеся там сообщения
   while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do;
   CloseHandle(PI.hProcess);
   CloseHandle(PI.hThread)
 end;  

Если в потоке, вызывающем функции
ожидания, явно (функцией CreateWindow)
или неявно (используя TForm, DDE, COM)
создаются окна Windows
— поток должен
обрабатывать сообщения. Поскольку
широковещательные сообщения посылаются
всем окнам в системе, то поток, не
обрабатывающий сообщения, может вызвать
взаимоблокировку (система ждет, когда поток
обработает сообщение, поток — когда
система или другие потоки освободят объект)
и привести к зависанию Windows.
Если в вашей программе имеются подобные
фрагменты, необходимо использовать MsgWaitForMultipleObjects
или MsgWaitForMultipleObjectsEx
и позволять прервать ожидание для
обработки сообщений. Алгоритм
аналогичен вышеприведенному примеру.

Прерывание ожидания по запросу на завершение операции ввода-вывода
или APC

Windows
поддерживает асинхронные вызовы процедур.
При создании каждого потока (thread) с ним
ассоциируется очередь асинхронных вызовов
процедур (APC queue). Операционная
система (или приложение пользователя — при
помощи функции QueueUserAPC)
может помещать в нее запросы на выполнение
функций в контексте данного потока. Эти
функции не могут быть выполнены немедленно,
поскольку поток может быть занят. Поэтому
операционная система вызывает их, когда
поток вызывает одну из следующих функций
ожидания:

function SleepEx(
   dwMilliseconds: DWORD;   // Период ожидания
   bAlertable: BOOL         // задает, должна ли функция возвращать
                            // управление в случае запроса на
                            // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function WaitForSingleObjectEx(
   hHandle: THandle;      // Идентификатор объекта
   dwMilliseconds: DWORD; // Период ожидания
   bAlertable: BOOL       // задает, должна ли функция возвращать
                          // управление в случае запроса на
                          // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function WaitForMultipleObjectsEx(
   nCount: DWORD;            // количество объектов
   lpHandles: PWOHandleArray;// адрес массива идентификаторов объектов
   bWaitAll: BOOL;           // Задает, требуется ли ожидание всех
                             // объектов или любого
   dwMilliseconds: DWORD;    // Период ожидания
   bAlertable: BOOL          // задает, должна ли функция возвращать
                             // управление в случае запроса на
                             // асинхронный вызов процедуры
 ): DWORD; stdcall;
 
   
function SignalObjectAndWait(
   hObjectToSignal: THandle;  // объект, который будет переведен в
                              // сигнальное состояние
   hObjectToWaitOn: THandle;  // объект, которого ожидает функция
   dwMilliseconds: DWORD;     // период ожидания
   bAlertable: BOOL           // задает, должна ли функция возвращать
                              // управление в случае запроса на
                              // асинхронный вызов процедуры
 ): DWORD; stdcall;  
   
function MsgWaitForMultipleObjectsEx(
 nCount: DWORD;     // количество объектов синхронизации
   var pHandles;      // адрес массива объектов
   fWaitAll: BOOL;    // Задает, требуется ли ожидание всех
                      // объектов или любого
   dwMilliseconds,    // Период ожидания
   dwWakeMask: DWORD  // Тип события, прерывающего ожидание
   dwFlags: DWORD     // Дополнительные флаги
 ): DWORD; stdcall;  

Если параметр bAlertable
равен TRUE
(либо если dwFlags
в функции MsgWaitForMultipleObjectsEx
содержит MWMO_ALERTABLE),
то при появлении в очереди APC
запроса на
асинхронный вызов процедуры операционная
система выполняет вызовы всех имеющихся в
очереди процедур, после чего функция
возвращает значение WAIT_IO_COMPLETION.

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

Объекты синхронизации

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

Event (событие)

Event
позволяет
известить один или несколько ожидающих
потоков о наступлении события. Event
бывает:

Отключаемый
вручную

Будучи
установленным в сигнальное состояние,
остается в нем до тех пор, пока не будет
переключен явным вызовом функции ResetEvent

Автоматически
отключаемый

Автоматически
переключается в несигнальное состояние
операционной системой, когда один из
ожидающих его потоков завершается

Для создания объекта используется функция CreateEvent:

function CreateEvent(
   lpEventAttributes: PSecurityAttributes;  // Адрес структуры
                                            // TSecurityAttributes
    bManualReset,         // Задает, будет Event переключаемым
                          // вручную (TRUE) или автоматически (FALSE)
    bInitialState: BOOL;  // Задает начальное состояние. Если TRUE -
                          // объект в сигнальном состоянии
    lpName: PChar         // Имя или NIL, если имя не требуется
 ): THandle; stdcall;     // Возвращает идентификатор созданного
                          // объекта  
Структура TSecurityAttributes описана, как:  
TSecurityAttributes = record
   nLength: DWORD;                // Размер структуры, должен
                                  // инициализироваться как
                                  // SizeOf(TSecurityAttributes)
   lpSecurityDescriptor: Pointer; // Адрес дескриптора защиты. В
                                  // Windows 95 и 98 игнорируется
                                  // Обычно можно указывать NIL
   bInheritHandle: BOOL;          // Задает, могут ли дочерние
                                  // процессы наследовать объект
 end;  

Если не требуется задание особых прав
доступа под Windows
NT или
возможности наследования объекта
дочерними процессами, в качестве параметра lpEventAttributes
можно передавать NIL.
В этом случае объект не может
наследоваться дочерними процессами и ему
задается дескриптор защиты «по умолчанию».

Параметр
lpName позволяет разделять объекты между
процессами. Если lpName совпадает с именем уже существующего
объекта типа Event,
созданного текущим или любым другим
процессом, то функция не
создает нового объекта, а возвращает
идентификатор уже существующего. При этом
игнорируются параметры bManualReset, bInitialState и lpSecurityDescriptor.
Проверить, был ли объект создан или
используется уже существующий, можно
следующим образом:

hEvent := CreateEvent(NIL, TRUE, FALSE, ‘EventName’);
 if hEvent = 0 then
   RaiseLastWin32Error;
 if GetLastError = ERROR_ALREADY_EXISTS then begin
   // Используем ранее созданный объект
 end;  

Если объект используется для
синхронизации внутри одного процесса, его
можно объявить как глобальную переменную и
создавать без имени.

Имя
объекта не должно совпадать с именем любого
из существующих объектов типов Semaphore, Mutex, Job,
Waitable Timer или FileMapping. В случае совпадения имен функция
возвращает ошибку.

Если известно, что Event
уже создан, для получения доступа к нему
можно вместо CreateEvent воспользоваться функцией OpenEvent:

function OpenEvent(
   dwDesiredAccess: DWORD;  // Задает права доступа к объекту
   bInheritHandle: BOOL;    // Задает, может ли объект наследоваться
                            // дочерними процессами
   lpName: PChar            // Имя объекта
 ): THandle; stdcall;  

Функция возвращает идентификатор
объекта либо 0 — в случае ошибки. Параметр
dwDesiredAccess может принимать одно из следующих
значений:

EVENT_ALL_ACCESS              

Приложение
получает полный доступ к объекту

EVENT_MODIFY_STATE              

Приложение
может изменять состояние объекта
функциями SetEvent и ResetEvent

SYNCHRONIZE              

Только для Windows
NT
— приложение может использовать объект
только в функциях ожидания

После получения идентификатора можно
приступать к его использованию. Для
этого имеются следующие функции:

function SetEvent(hEvent: THandle): BOOL; stdcall;  

— устанавливает объект в сигнальное состояние

function ResetEvent(hEvent: THandle): BOOL; stdcall;  

— сбрасывает объект, устанавливая его в несигнальное состояние

function PulseEvent(hEvent: THandle): BOOL; stdcall  

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

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

var
   Events: array[0..1] of THandle;  // Массив объектов синхронизации
   Overlapped: array[0..1] of TOverlapped;
 
 ...
 
 // Создаем объекты синхронизации
 Events[0] := CreateEvent(NIL, TRUE, FALSE, NIL);
 Events[1] := CreateEvent(NIL, TRUE, FALSE, NIL);
 
 // Инициализируем структуры TOverlapped
 FillChar(Overlapped, SizeOf(Overlapped), 0);
 Overlapped[0].hEvent := Events[0];
 Overlapped[1].hEvent := Events[1];
 
 // Начинаем асинхронную запись в файлы
 WriteFile(hFirstFile, FirstBuffer, SizeOf(FirstBuffer),
   FirstFileWritten, @Overlapped[0]);
 WriteFile(hSecondFile, SecondBuffer, SizeOf(SecondBuffer),
   SecondFileWritten, @Overlapped[1]);
 
 // Ожидаем завершения записи в оба файла
 WaitForMultipleObjects(2, @Events, TRUE, INFINITE);
 
 // Уничтожаем объекты синхронизации
 CloseHandle(Events[0]);
 CloseHandle(Events[1]) 

По завершении работы с объектом он должен
быть уничтожен функцией CloseHandle.

Delphi
предоставляет класс TEvent,
инкапсулирующий функциональность объекта Event.
Класс расположен в модуле
SyncObjs.pas и объявлен следующим образом:

type
   TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError);
 
   TEvent = class(THandleObject)
   public
     constructor Create(EventAttributes: PSecurityAttributes;
       ManualReset, InitialState: Boolean; const Name: string);
     function WaitFor(Timeout: DWORD): TWaitResult;
     procedure SetEvent;
     procedure ResetEvent;
   end;  

Назначение методов очевидно следует из
их названий. Использование
этого класса позволяет не вдаваться в
тонкости реализации вызываемых функций
Windows API. Для простейших случаев объявлен еще один
класс с упрощенным конструктором:

type
   TSimpleEvent = class(TEvent)
   public
     constructor Create;
   end;
 
 …
 
 constructor TSimpleEvent.Create;
 begin
   FHandle := CreateEvent(nil, True, False, nil);
 end;  

Mutex (Mutually Exclusive)

Мьютекс — это объект синхронизации,
который находится в сигнальном состоянии
только тогда, когда не принадлежит ни
одному из процессов. Как только хотя бы один
процесс запрашивает владение мьютексом, он
переходит в несигнальное состояние и
остается таким до тех пор, пока не будет
освобожден владельцем. Такое поведение
позволяет использовать мьютексы для
синхронизации совместного доступа
нескольких процессов к разделяемому
ресурсу. Для создания мьютекса
используется функция:

function CreateMutex(
   lpMutexAttributes: PSecurityAttributes;  // Адрес структуры
                                            // TSecurityAttributes
   bInitialOwner: BOOL;  // Задает, будет ли процесс владеть
                         // мьютексом сразу после создания
   lpName: PChar         // Имя мьютекса
 ): THandle; stdcall;  

Функция возвращает идентификатор
созданного объекта либо 0. Если мьютекс с
заданным именем уже был создан,
возвращается его идентификатор. В
этом случае функция GetLastError вернет код
ошибки ERROR_ALREDY_EXISTS. Имя
не должно совпадать с именем уже
существующего объекта типов Semaphore,
Event, Job,
Waitable
Timer или
FileMapping.

Если неизвестно, существует ли уже
мьютекс с таким именем, программа не должна
запрашивать владение объектом при создании
(то есть должна передать в качестве bInitialOwner значение FALSE).

Если мьютекс уже существует, приложение
может получить его идентификатор функцией OpenMutex:

function OpenMutex(
   dwDesiredAccess: DWORD;  // Задает права доступа к объекту
   bInheritHandle: BOOL;    // Задает, может ли объект наследоваться
                            // дочерними процессами
   lpName: PChar            // Имя объекта
 ): THandle; stdcall;  

Параметр dwDesiredAccess
может принимать одно из следующих значений:

MUTEX_ALL_ACCESS 
Приложение получает полный доступ к объекту
SYNCHRONIZE
Только для Windows NT — приложение может использовать объект только в
функциях ожидания и функции ReleaseMutex

Функция возвращает идентификатор
открытого мьютекса либо 0 — в случае ошибки.
Мьютекс переходит в сигнальное состояние
после срабатывания функции ожидания, в
которую был передан его идентификатор. Для
возврата в несигнальное состояние служит
функция ReleaseMutex:

function ReleaseMutex(hMutex: THandle): BOOL; stdcall;  

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

var
   Mutex: THandle;
 
 // При инициализации программы
 Mutex := CreateMutex(NIL, FALSE, ‘UniqueMutexName’);
 if Mutex = 0 then
   RaiseLastWin32Error;
 
 ...
 // Доступ к ресурсу
 WaitForSingleObject(Mutex, INFINITE);
 try
   // Доступ к ресурсу, захват мьютекса гарантирует,
   // что остальные процессы, пытающиеся получить доступ,
   // будут остановлены на функции WaitForSingleObject
   ...
 finally
   // Работа с ресурсом окончена, освобождаем его
   // для остальных процессов
   ReleaseMutex(Mutex);
 end;
 
 ...
 // При завершении программы
 CloseHandle(Mutex);  

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

Разумеется,
если работа с ресурсом может потребовать
значительного времени, то необходимо либо
использовать функцию MsgWaitForSingleObject, либо
вызывать WaitForSingleObject в цикле с нулевым
периодом ожидания, проверяя код возврата. В
противном случае ваше
приложение окажется замороженным. Всегда
защищайте захват-освобождение объекта
синхронизации при помощи блока try … finally,
иначе ошибка во время работы с ресурсом
приведет к блокированию работы всех
процессов, ожидающих его освобождения.

Semaphore (семафор)

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

Для создания семафора служит функция CreateSemaphore:

function CreateSemaphore(
   lpSemaphoreAttributes: PSecurityAttributes; // Адрес структуры
                                               // TSecurityAttributes
   lInitialCount,           // Начальное значение счетчика
   lMaximumCount: Longint;  // Максимальное значение счетчика
   lpName: PChar            // Имя объекта
 ): THandle; stdcall;  

Функция возвращает идентификатор
созданного семафора либо 0, если создать
объект не удалось.

Параметр
lMaximumCount задает максимальное значение
счетчика семафора, lInitialCount задает начальное
значение счетчика и должен быть в диапазоне
от 0 до lMaximumCount. lpName задает имя семафора. Если
в системе уже есть семафор с таким именем,
то новый не создается, а возвращается
идентификатор существующего семафора. В
случае если семафор используется внутри
одного процесса, можно создать его без
имени, передав в качестве lpName значение NIL. Имя
семафора не должно совпадать с именем уже
существующего объекта типов event,
mutex, waitable
timer, job
или file-mapping.

Идентификатор ранее созданного семафора
может быть также получен функцией OpenSemaphore:

function OpenSemaphore(
     dwDesiredAccess: DWORD;  // Задает права доступа к объекту
     bInheritHandle: BOOL;    // Задает, может ли объект наследоваться
                                 // дочерними процессами
     lpName: PChar            //    Имя объекта
   ): THandle; stdcall; 

Параметр dwDesiredAccess
может принимать одно из следующих значений:

SEMAPHORE_ALL_ACCESS              

Поток
получает все права на семафор

SEMAPHORE_MODIFY_STATE              

Поток
может увеличивать счетчик семафора
функцией ReleaseSemaphore

SYNCHRONIZE              

Только для Windows
NT
— поток может использовать семафор в
функциях ожидания

Для увеличения счетчика семафора
используется функция ReleaseSemaphore:

function ReleaseSemaphore(
   hSemaphore: THandle;      // Идентификатор семафора
   lReleaseCount: Longint;   // Счетчик будет увеличен на эту величину
   lpPreviousCount: Pointer  // Адрес 32-битной переменной,
                             // принимающей предыдущее значение
                             // счетчика
 ): BOOL; stdcall;  

Если значение счетчика после выполнения
функции превысит заданный для него
функцией CreateSemaphore максимум, то ReleaseSemaphore
возвращает FALSE
и значение семафора не изменяется. В
качестве параметра lpPreviousCount можно передать
NIL, если это значение нам не нужно.

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

unit LimitedThread;
 
 interface
 
 uses Classes;
 
 type
   TLimitedThread = class(TThread)
     procedure Execute; override;
   end;
  
 implementation
 
 uses Windows;
 
 const
   MAX_THREAD_COUNT = 10;
 
 var
   Semaphore: THandle;
 
 procedure TLimitedThread.Execute;
 begin
   // Уменьшаем счетчик семафора. Если к этому моменту уже запущено
   // MAX_THREAD_COUNT потоков — счетчик равен 0 и семафор в
   // несигнальном состоянии. Поток будет заморожен до завершения
   // одного из запущенных ранее.
   WaitForSingleObject(Semaphore, INFINITE);
  
   // Здесь располагается код, отвечающий за функциональность потока,
   // например загрузка файла
   ...
 
   // Поток завершил работу, увеличиваем счетчик семафора и позволяем
   // начать обработку другим потокам.
   ReleaseSemaphore(Semaphore, 1, NIL);
 end;
 
 initialization
   // Создаем семафор при старте программы
   Semaphore := CreateSemaphore(NIL, MAX_THREAD_COUNT,
     MAX_THREAD_COUNT, NIL);
 
 finalization
   // Уничтожаем семафор по завершении программы
   CloseHandle(Semaphore);
 end;  

Цель работы: получение практических навыков по использованию Win32 API для синхронизации потоков

Дескрипторы и идентификаторы потоков

Тема о дескрипторах, псевдодескрипторах и идентификаторах потоков аналогична теме об аналогичных характеристиках процессов.

Функция CreateProcess возвращает идентификатор и дескриптор первого (и только первого) потока, выполняющегося во вновь созданном процессе. Функция Create Thread тоже возвращает идентификатор потока, но область действия которого — вся система.

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

Согласно документации Win32, Win32 API не предлагает способа для получения дескриптора потока по его идентификатору. Если бы дескрипторы можно было находить таким образом, то процесс, которому принадлежат потоки, завершался бы неудачей, так как другой процесс смог бы выполнять несанкционированные операции с его потоками, например, приостанавливать поток, возобновлять его действие, изменять приоритет или завершать его работу. Запрашивать дескриптор следует у процесса, создавшего данный поток, или у самого потока.

Наконец, поток может вызывать функцию GetCurrentThread для получения собственного псевдодескриптора. Как и в случае псевдодескрипторов процессов, псевдодескриптор потока может использоваться только для вызова процесса и не может наследоваться. Можно использовать функцию Duplicate Handle для получения настоящего дескриптора потока по его псевдодескриптору так же, как это делается в процессах.

Приоритет потоков

Термин многозадачность (multitasking) или мультипрограммирование (multiprogramming), обозначает возможность управлять несколькими процессами (или несколькими потоками) на базе одного процессора. Многопроцессорной обработкой (multiprocessing) называется управление некоторым числом процессов или потоков на нескольких процессорах.

Компьютер может одновременно выполнять команды двух разных процессов только в многопроцессорной среде. Однако даже на одном процессоре с помощью переключения задач (task switching) можно создать впечатление, что одновременно выполняются команды нескольких процессов.

В старой 16-разрядной Windows существовал только один поток. Более того, в данной системе был реализован метод кооперативной (cooperative) многозадачности, который состоит в том, что каждое приложение само отвечает за высвобождение единственного системного потока, после чего могут выполняться другие приложения. Если программа выполняла задачи, требующие значительного времени, такие как форматирование гибкого диска, все другие загруженные приложения должны были ждать. А если программа, написанная с ошибками, входила в бесконечный цикл, то вся система становилась непригодной для использования («зависала»), требовала перезагрузки с выключением питания.

Уровни приоритета потоков

Win32 значительно отличается от Win16. Во-первых, она является многопоточной (multithreaded), что определяет ее многозадачность. Во-вторых, в ней реализована модель вытесняющей (preemptive) многозадачности, в которой операционная система решает, когда каждый поток получает процессорное время, выделяемое квантами времени (time slices), и сколько именно времени выделяется. Временной интервал в Windows называется квантом.

Продолжительность кванта времени зависит от аппаратуры и может фактически меняться от потока к потоку. Например, базовое значение в Windows 95 со составляет 20 мс, для Windows NT Workstation (на базе процессора Pentium) -30 мс, для Windows NT Server — 180 мс.

Давайте выясним, каким образом Windows выделяет кванты времени потокам в системе. Эти процедуры в Windows 9x и Windows NT довольно похожи, но не идентичны.

Каждый поток в системе имеет Уровень приоритета (priority level), который представляет собой число в диапазоне от 0 до 31. Ниже перечислено то самое основное, что нужно знать:

· если существуют какие-либо потоки с приоритетом 31, которые требуют процессорного времени, то есть не находятся в состоянии ожидания (not idle), операционная система перебирает эти потоки (независимо от того, каким процессам они принадлежат), выделяя по очереди каждому из них кванты времени. Потокам с более низким приоритетом кванты времени совсем не выделяются, поэтому они не выполняются. Если нет активных потоков с приоритетом 31, операционная система ищет активные потоки с уровнем приоритета 30 и т. д. Однако потоки довольно часто простаивают. Тот факт, что приложение загружено, не означает, что все его потоки активны. Поэтому у потоков с более низким приоритетом все же есть возможность работать. Более того, если пользователь нажимает клавишу, относящуюся к процессу, потоки которого простаивают, операционная система временно выделяет процессорное время соответствующему потоку, чтобы он мог обработать нажатие клавиши;

· приоритет 0 зарезервирован исключительно за специальным системным потоком, который называется потоком нулевой страницы (zero page thread). Он освобождает незадействованные области памяти. Существует также поток idle, который работает с уровнем приоритета 0, опрашивая систему в поисках какой-нибудь работы;

· если поток с произвольным приоритетом выполняется в тот момент, когда потоку с большим приоритетом потребовалось процессорное время (например, он получает сообщение о том, что пользователь щелкнул мышью), операционная система немедленно вытесняет поток с меньшим приоритетом и отдает процессорное время потоку с большим. Таким образом, поток может не успеть завершить выделенный ему квант времени;

· для того чтобы перейти с одного потока на другой, система осуществляет переключение контекста (context switch). Это процедура, сохраняющая состояние процессора (регистров и стека) и загрузки соответствующих значений другого потока.

Назначение приоритета потоку

Назначение потоку приоритета происходит в два этапа. Во-первых, каждому процессу в момент создания присваивается класс приоритета. Узнать класс приоритета можно с помощью функции GetPriorityClass, а изменить — с помощью функции SetPriorityClass. Ниже приведены имена классов приоритета процессов, уровни приоритета и константы, которые используются с этими вышеупомянутыми функциями (как и с функцией CreateProcess).

Имя класса приоритета

Уровень приоритета класса

Символьная константа

Idle

4

IDLE_PRIORITY_CLASS=&H40

Normal

8

NORMAL_PRIORITY_CLASS=&H20

High

13

HIGH_ PRIORITY_CLASS=&H80

Realtime

24

REALTIME_ PRIORITY_CLASS=&H100

Большинство процессов должно получать класс уровня приоритета Normal (обычный). Однако некоторым приложениям, таким как приложения мониторинга системы, возможно, более уместно назначать приоритет Idle (ожидания). Назначения приоритета Realtime (реального времени) обычно следует избегать, потому что в этом случае потоки изначально получают приоритет более высокий, чем системные потоки, такие как потоки ввода от клавиатуры и мыши, очистки кэша и обработки нажатия клавиш Ctrl+Alt+Del. Такой приоритет может быть подходящим для краткосрочных, критичных к времени выполнения процессов, которые относятся к взаимодействию с аппаратурой.

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

BOOL SetThreadPriority (

НANDLЕ hThread, // Дескриптор потока

Int NPriority // Уровень приоритета потока

);

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

_______________________________________________________________________

Константа Уровень приоритета потоков

_______________________________________________________________________

Thread_priority_normal Уровень приоритета класса

Thread_priority_above_normal Уровень приоритета класса + 1

THREAD_PRIORITY_BELOW_N0RMAL Уровень приоритета класса — 1

Thread_priority_highest Уровень приоритета класса + 2

THREAD_PRIORITY_LOWEST Уровень приоритета класса — 2

Thread_Priority_Idle Устанавливает уровень приоритета 1 для всех классов приоритета процессов за исключением Realtime. В этом случае устанавливает уровень приоритета 16.

Thread_priority_time_critical Устанавливает уровень приоритета 15 для всех классов приоритета процессов за исключением Realtime. В этом случае устанавливает уровень приоритета 31.

____________________________________________________________________

Повышение приоритета потока и квант изменений приоритета

Диапазон приоритета от 1 до 15 известен как диапазон динамического приоритета (Dynamic Priority), а диапазон от 16 до 31 — как диапазон приоритета реального времени (Realtime Priority).

В Windows NT приоритет потока, находящийся в динамическом диапазоне, может временно повышаться операционной системой в различные моменты времени Соответственно, нижний уровень приоритета потока (установленный программистом с помощью API функции) называется уровнем его базового приоритетa (Base Priority) API функция Windows NT SetProcessPriorityBoost может использоваться для разрешения или запрещения временных изменений приоритета (priority boosting). Правда, она не поддерживается в Windows 9х.

Бывают также случаи, когда кванты времени, выделяемые потоку, временно увеличиваются.

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

    если поток принадлежит приоритетному процессу (Foreground Process), то есть процессу, окно которого активно и имеет фокус ввода; если поток первым вошел в состояние ожидания; если поток выходит из состояния ожидания; если поток совсем не получает процессорного времени.

Состояния потоков

Потоки могут находиться в одном из нескольких состояний:

    Ready (готов) – находящийся в пуле (pool) потоков, ожидающих выполнения; Running (выполнение) — выполняющийся на процессоре; Waiting (ожидание), также называется idle или suspended, приостановленный — в состоянии ожидания, которое завершается тем, что поток начинает выполняться (состояние Running) или переходит в состояние Ready; Terminated (завершение) — завершено выполнение всех команд потока. Впоследствии его можно удалить. Если поток не удален, система может вновь установить его в исходное состояние для последующего использования.

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

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

Синхронизация потоков (Thread Synchronization) — это обобщенный термин, относящийся к процессу взаимодействия и взаимосвязи потоков. Учтите, что синхронизация потоков требует привлечения в качестве посредника самой операционной системы. Потоки не могут взаимодействовать друг с другом без ее участия.

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

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

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

Критическая секция — это некоторый участок кода, который в каждый момент времени может выполняться только одним из потоков. Если код, используемый для инициализации массива, поместить в критическую секцию, то другие потоки не смогут войти в этот участок кода до тех пор, пока первый поток не завершит его выполнение.

До использования критической секции необходимо инициализировать ее с помощью процедуры Win32 API InitializeCriticalSection(), которая определяется (в Delphi) следующим образом:

Procedure InitializeCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

Параметр IpCriticalSection представляет собой запись типа TRTLCriticalSection, которая передается по ссылке. Точное определение записи TRTLCriticalSection не имеет большого значения, поскольку вам вряд ли понадобится когда-либо заглядывать в ее содержимое. От вас требуется лишь передать неинициализированную запись в параметр IpCtitical Section, и эта запись будет тут же заполнена процедурой.

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

Procedure EnterCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

Procedure LeaveCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

Параметр IpCriticalSection, который передается этим процедурам, является не чем иным, как записью, созданной процедурой InitializeCriticalSection().

Функция EnterCriticalSection проверяет, не выполняет ли уже какой-нибудь другой поток критическую секцию своей программы, связанную с данным объектом критической секции. Если нет, поток получает разрешение на выполнение своего критического кода, точнее, ему не запрещают это делать. Если да, то поток, обратившийся с запросом, переводится в состояние ожидания, а о запросе делается запись. Так как нужно создавать записи, объект «критическая секция» представляет собой структуру данных.

Когда функция LeaveCriticalSection вызывается потоком, который владеет в текущий момент разрешением на выполнение своей критической секции кода, связанной с данным объектом «критическая секция», система может проверить, нет ли в очереди другого потока, ожидающего освобождения этого объекта. Затем система может вынести ждущий поток из состояния ожидания, и он продолжит свою работу (в выделенные ему кванты времени).

По окончании работы с записью TRTLCriticalSection необходимо освободить ее, вызвав процедуру DeleteCriticalSection(), которая определяется следующим образом:

Procedure DeleteCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

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

Многие объекты ядра, включая процесс, поток, файл, мьютекс, семафор, уведомление об изменении файла и событие, могут находиться в одном из двух состояний — «свободно» (Signaled) и «занято» (Nonsignaled). Вероятно, проще представлять себе эти объекты подключенными к лампочке, как на приведенном рисунке. Если свет горит, объект свободен, в обратном случае объект занят.

Например, в момент создания процесса его объект ядра находится в состоянии «занято». Когда процесс завершается, объект переходит в состояние «свободно». Аналогично выполняющиеся потоки (то есть их объекты) пребывают в состоянии «занято», но переходят в состояние «свободно», когда завершают работу. На самом деле некоторые объекты, такие как мыютекс, семафор, событие, уведомление об изменении файла, таймер ожидания, существуют исключительно для того, чтобы вырабатывать сигналы «свободно» и «занято».

Смысл всей этой «сигнализации» в том, чтобы поток мог приостанавливать свою работу до того момента, когда заданный объект перейдет в состояние «свободно». Например, поток одного процесса может временно прекратить работу до завершения другого, просто подождав, когда объект ядра этого другого процесса перейдет в состояние «свободно».

Посредством вызова функций WaitForSingleObject и WaitForMultipleObjects поток приостанавливает свое выполнение до того момента, когда заданный объект (или объекты) перейдет в состояние «свободно». Рассмотрим функции WaitForSingleObject, декларация которой выглядит так:

DWORD WaitForSingleObject(

HANDLE HHandle, // Дескриптор объекта ожидания

DWORD DwMilliseconds // Время ожидания в миллисекундах

);

Параметр HHandle является дескриптором объекта, уведомление о свободном состоянии которого требуется получить, a DwMilliseconds — это время, которое вызывающий поток готов ждать. Если DwMilliseconds равно нулю, функция немедленно вернет текущий статус заданного объекта. Таким образом, можно протестировать состояние объекта. Параметру можно также присваивать значение символьной константы INFINITE (= -1), в этом случае вызывающий поток будет ждать неограниченное время.

Функция WaitForSingleObjEcT переводит вызывающий поток в состояние ожидания до того момента, когда она передаст ему свое возвращаемое значение. Ниже перечислены возможные возвращаемые значения:

· Wait_object_0 — объект находится в состоянии «свободно»;

· WAIT_TIMEOUT — интервал ожидания, заданный DwMilliseconds, истек,

А нужный объект по прежнему находится в состоянии «занято»;

· WAIT_ABANDONED относится только к мьютексу и означает, что объект не

был освобожден потоком, который владел им до своего завершения;

· WAIT_FAILED — при выполнении функции произошла ошибка.

Объекты «мьютекс»

Мьютекс (MUTual Exclusions— взаимоисключения) — это объект ядра, который можно использовать для синхронизации потоков из разных процессов. Он может принадлежать или не принадлежать некоторому потоку. Если мьютекс принадлежит потоку, то он находится в состоянии «занято». Если данный объект не относится ни к одному потоку, то он находится в состоянии «свободно». Другими словами, принадлежать для него означает быть в состоянии «занято».

Если мьютекс не принадлежит ни одному потоку, первый поток, который вызовет функцию WaitForSingleObject, завладевает данным объектом, и тот переходит в состояние «занято». В определенном смысле мьютекс похож на выключатель, которым может пользоваться любой поток по принципу «первым пришел — первым обслужили» (first-come-first,-served).

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

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

Мьютексы создаются с помощью вызова функции CreateMutex:

HANDLE CreateMutexi

LPSECURITY_ATTRIBUTES IpMutexAttributes,

// Указатель на атрибуты защиты.

BOOL BInitialOwner, // флаг первоначального владельца.

LPCTSTR IpName // Указатель на имя мьютекса.

);

Параметр IpMutexAttributes — это указатель на запись типа TSecurityftttributes. Обычно в качестве данного параметра передастся значение nil, и в этом случае используются атрибуты защиты, действующие по умолчанию.

Параметр blnitialOwner определяет, следует ли считать поток, создающий мьютекс, его владельцем. Если этот параметр равен False, значит, мьютекс не имеет владельца.

Параметр IpName представляет имя мыотскса. Если вы не собираетесь присваивать мьютексу имя, установите этот параметр равным nil. Если же значение этого параметра отлично от nil, функция выполнит в системе поиск мьютекса с таким же именем. При успешном завершении поиска функция вернет дескриптор найденного мьютекса, в противном случае возвращается дескриптор нового мьютекса. При наличии имени этот объект может совместно использоваться несколькими процессами. Если каким-то процессом создается мьютекс с именем, то поток другого процесса может вызывать функции CreateMutex или OpenMutex с тем же самым именем. В любом случае система просто передаст вызывающему потоку дескриптор исходного мьютекса. Другой способ совместно использовать мьютекс — вызвать функцию DuplicateHandle.

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

BOOL ReleaseMutex(

HANDLE HMutex // Дескриптор мьютекса.

);

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

По завершении использования мьютекса необходимо закрыть его с помощью функции Win32 API CloseHandle().

События

События используются в качестве сигналов о завершении какой-либо операции. Однако в отличие от мьютексов, они не принадлежат ни одному потоку. Например, поток А создает событие с помощью функции CreateEvent и устанавливает объект в состояние «занято». Поток В получает дескриптор этого объекта, вызвав функцию OpenEvent, затем вызывает функцию WaitForSingleObject, чтобы приостановить работу до того момента, когда поток А завершит конкретную задачу и освободит указанный объект. Когда это произойдет, система выведет из состояния ожидания поток В, который теперь владеет информацией, что поток А завершил выполнение своей задачи.

Объявление функции CreateEvent записывается таким образом:

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES IpEventAttributes,

// Указатель на атрибуты защиты.

BOOL BManualReset, // Флаг интерактивного события.

BOOL BInitialState, // Флаг первоначального состояния.

LPCTSTR IpName // Указатель на имя события.

);

Эта функция возвращает дескриптор создаваемого объекта «событие». Первый параметр определяет, наследуется ли дескриптор порожденными процессами. Если IpEventAttributes имеет значение NULL, дескриптор наследоваться не может.

Если параметр BMaNuAlReseT имеет значение TRUE, то при освобождении объект остается в этом состоянии (в отличие от объекта «мъютекс»). Это значит, что все потоки, ожидающие перехода данного объекта в состояние «свободно», будут выведены системой из состояния ожидания. Такой объект называется событием с ручным сбросом (Manual-reset event), поскольку «разбуженный» (выведенный из состояния ожидания) поток может самостоятельно сбросить состояние объекта «событие» в «занято». Если параметр bManualReset имеет значение FALSE, то система автоматически сбрасывает состояние рассматриваемого объема в «занято» после «пробуждения» первого потока, ожидающего освобождения данного объекта. Только один поток выводится из состояния ожидания, как и в случае с мьютексами. Такое событие называют событием с автоматическим сбросом (Auto-REset event).

Параметр BINitialSTate определяет первоначальное состояние (если TRUE, то «свободно», если FALSE, то «занято») данного события. Параметру IpName может быть присвоено имя события. Имя предоставляет способ совместного использования, например посредством функции OpenEvent.

В качестве дополнительного варианта, если вы не хотите иметь дела с вопросами защиты, можно установить IpEventAttributes в NULL (0&). В таком случае декларация примет следующий вид:

Declare Function CreateEvent Lib «Kernel32″ Alias «CreateEventA» ( _

ByVal IpEventAttributes As Long, _

By Val bManualReset As Long, _

By Val blmtialState As Long, _

By Val IpName As String _

) As Long

Так же, как и другие дескрипторы, дескриптор события должен быть закрыт с использованием функции CloseHandle.

Объявление функции OpenEvent выглядит так:

HANDLE OpenEvent(

DWORD dwDesiredAccess, // Флаг доступа.

BOOL BlnheritHandle, // Флаг наследования.

LPCTSTR IpName // Указатель на имя события.

);

Где параметр DwDesiredAccess может принимать одно из трех значений:

· EVENT_ALL_ACCESS предоставляет полный доступ к событию;

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

· SYNCHRONIZE Разрешает использование дескриптора события в любых функциях ожидания (таких как WaitForSingleObject), ждущих освобождения данного объекта.

Каждая из этих функций принимает дескриптор события в качестве аргумента. Функция SetEvent устанавливает состояние данного события в «свободно», a Reset Event «сбрасывает» событие, то есть присваивает событию статус «занято», функция PulseEvent вызывает SetEvent для освобождения ожидающих потоков, а затем вызывает ResetEvent для перевода данного события в состояние «занято».

Семафоры

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

Function CreateSemaphoreflpSemaphoreAttributes: PSecurityAttributes; llnitialCoimt, iMaximumCount: Longint; IpName: PChar): THandle; stdcall;

Как и в случае функции CreateMutex(), первым параметром, передаваемым функции CreateSemaphore(), является указатель на запись TSecurityAttributes, причем значение Nil соответствует согласию на использование стандартных атрибутов защиты.

Параметр llnitialCount представляет собой начальное значение счетчика семафорного объекта. Это число может находиться в диапазоне от 0 до значения IMaximumCount. Семафор доступен, если значение этого параметра больше нуля. Когда поток вызывает функцию WaitForSingleObject() или любую другую, ей подобную, значение счетчика семафора уменьшается на единицу. И наоборот, при вызове потоком функции ReleaseSemaphore() значение счетчика семафора увеличивается на единицу.

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

Параметр IpName содержит имя семафора. Поведение этого параметра аналогично поведению одноименного параметра функции CreateMutex().

Функция ReleaseSemaphore () используемая для увеличения значения счетчика семафора имеет больше параметров, чем ее «коллега» ReleaseMutex(). Объявление функции ReleaseSemaphore() выглядит следующим образом:

Function ReleaseSemaphore(hSemaphore: THandle; IReleaseCount: Longint; IpPreviousCount: Pointer): BOOL; stdcall;

С помощью параметра IReleaseCount можно задать число, на которое будет уменьшено значение счетчика семафора. При этом старое значение счетчика будет сохранено в переменной типа Longint, на которую указывает параметр IpPreviousCount, если его значение не равно Nil, Скрытый смысл этого средства состоит в том, что семафор никогда не принадлежит ни одному

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

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

Для освобождения дескриптора семафора, выделенного ему с помощью функции CreateSemaphore(), не забудьте вызвать функцию CloseHandle().

Ждущие таймеры

Ждущий таймер (waitable timer) представляет собой новый тип объектов синхронизации, поддерживаемый в Windows NT версии 4.0 и выше. Это полноценный объект синхронизации, который может использоваться для организации задержки в одном или нескольких приложениях.

Ждущий таймер работает в трех режимах. В режиме «ручного сброса» таймер переходит в установленное состояние при истечении заданной задержки и остается установленным до тех пор, пока функция SetWaitableTimer не задаст новую задержку. В режиме «автоматического сброса» таймер переходит в установленное состояние при истечении заданной задержки и остается установленным до первого успешного вызова функции ожидания. В этом режиме он напоминает объект Event в режиме автоматического сброса, поскольку каждый раз при истечении времени задержки разрешается выполнение лишь одной нити. Наконец, ждущий таймер может выполнять функции интервального таймера, который перезапускается с заданной задержкой после каждого срабатывания объекта.

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

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

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

Функция

Описание

CancelWaitableTimer

СгеаteWaitableTimer

OpenWaitableTirner

SetWaitableTimer

Останавливает работу ждущего таймера. Таймер остается в текущем состоянии

Создает объект ждущего таймера. Если таймер с заданным именем уже существует, он открывается

Открывает существующий ждущий таймер

Запускает ждущий таймер с заданной продолжительностью и интервалом срабатывания

СОДЕРЖАНИЕ ОТЧЕТА

11. Наименование лабораторной работы, ее цель.

12. Исследование на конкретном примере следующих методов синхронизации потоков:

1. критические секции

2. мьтексы

3. события

Задачу для синхронизации выбрать на свое усмотрение.

13. Примеры разработанных приложений (описание программ, результаты и тексты программ).

Примечание:

Задачи для каждого метода синхронизации должны быть различными. Задачи должны наглядно демонстрировать выбранный метод синхронизации и учитывать его особенности. Студент, сдающий работу должен АРГУМЕНТИРОВАННО обосновать задачу, выбранную для синхронизации и метод синхронизации.

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

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

—         первый поток прочитал значение глобальной переменной в локальную;

—         ОС прерывает его, так как закончился выделенный ему квант времени процессора, и передаёт управление второму потоку;

—         второй поток также считал значение глобальной переменной в локальную, декрементировал её и записал новое значение обратно;

—         ОС вновь передаёт управление первому потоку, тот, ничего не зная о действиях второго потока, инкрементирует свою локальную переменную и  записывает её значение в глобальную.

Очевидно, что изменения, внесённые вторым потоком, будут утеряны.

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

Средства синхронизации в ОС Windows:

1) критическая секция (Critical Section) – это объект, который принадлежи процессу, а не ядру. А значит, не может синхронизировать потоки из разных процессов.

Существует так же функции инициализации (создания) и удаления, вхождения и выхода из критической секции:

создание – InitializeCriticalSection(…),    удаление – DeleteCriticalSection(…),

вход – EnterCriticalSection(…),                выход – LeaveCriticalSection(…).

Ограничения: поскольку это не объект ядра, то он не виден другим процессам, то есть можно защищать только потоки своего процесса.

Критический раздел анализирует значение специальной перемен­ной процесса, которая используется как флаг, предотвращающий исполнение некоторого участка кода несколькими потоками одновременно.

Среди синхронизирующих объектов критические разделы наиболее просты.

2) mutexmutable exclude. Это объект ядра, у него есть имя, а значит с их помощью можно синхронизировать доступ к общим данным со стороны нескольких процессов, точнее, со стороны потоков разных процессов. Ни один другой поток не может завладеть мьютексом, который уже принадлежит одному из по­токов. Если мьютекс защищает какие-то совместно используемые данные, он сможет выполнить свою функцию только в случае, если перед обращением к этим данным каждый из потоков будет проверять состояние этого мьютекса. Windows расценивает мьютекс как объект общего доступа, который можно пере­вести в сигнальное состояние или сбросить. Сигнальное состояние мьютекса говорит о том, что он занят. Потоки должны самостоятельно ана­лизировать текущее состояние мьютексов. Если требуется, чтобы к мьютексу могли обратиться потоки других процессов, ему надо присвоить имя.

Функции:

CreateMutex(имя) – создание,                  hnd=OpenMutex(имя) – открытие,

WaitForSingleObject(hnd) – ожидание и занятие,

ReleaseMutex(hnd) – освобождение,        CloseHandle(hnd) – закрытие.

Его можно использовать в защите от повторного запуска программ.

3) семафор – semaphore. Объект ядра “семафор” используются для учёта ресурсов и служат для ограничения одновременного доступа к ресурсу нескольких потоков. Используя семафор, можно организовать работу программы таким образом, что к ресурсу одновременно смо­гут получить доступ несколько потоков, однако количество этих потоков будет ограничено. Создавая семафор, указывается максимальное количество пото­ков, которые одновременно смогут работать с ресурсом. Каждый раз, когда программа обращается к семафору, значение счетчика ресурсов семафора уменьша­ется на единицу. Когда значение счетчика ресурсов становится равным нулю, семафор недоступен.

создание CreateSemaphore,                      открытие OpenSemaphore,

занять WaitForSingleObject,                     освобождение ReleaseSemaphore

4событие – event. События обычно просто оповещают об окончании какой-либо операции, они также являются объектами ядра. Можно не просто явным образом освободить, но так же есть операция установки события. События могут быть мануальными (manual) и единичными (single).

Единичное событие (single event) – это скорее общий флаг. Событие находится в сигнальном состоянии, если его установил какой-нибудь поток. Если для работы программы требуется, чтобы в случае возникновения события на него реа­гировал только один из потоков, в то время как все остальные потоки продолжали ждать, то используют единичное событие.

Мануальное событие (manual event) — это не про­сто общий флаг для нескольких потоков. Оно выполняет несколько более сложные функции. Любой поток может установить это событие или сбросить (очистить) его. Если событие установлено, оно останется в этом состоянии сколь угодно долгое время, вне зависимости от того, сколько потоков ожидают установки этого события. Когда все потоки, ожидающие этого события, получат сообщение о том, что событие произошло, оно автоматически сбросится.

Функции: SetEvent, ClearEvent, WaitForEvent.

Типы событий:

1) событие с автоматическим сбросом: WaitForSingleEvent.

2) событие с ручным сбросом (manual), тогда событие необходимо сбрасывать: ReleaseEvent.

Некоторые теоретики выделяют ещё один объект синхронизации:

WaitAbleTimer – объект ядра ОС, который самостоятельно переходит в свободное состояние через заданный интервал времени (будильник).

Понравилась статья? Поделить с друзьями:
  • Объектами в операционной системе windows являются
  • Объект ядра ос windows это структура данных в памяти доступная только
  • Объект ядра ос windows может наследовать только процесс потомок
  • Объект ядра ос windows может наследовать только мьютекс
  • Объект папка в windows соответствует понятию