Время на прочтение6 мин
Количество просмотров58K
Синхронизация нужна в любой малтитредной программе. (Если, конечно, она не состоит из локлесс алгоритмов на 100%, что вряд ли). Будь то приложение или компонента ядра современной операционной системы.
Меня всё нижесказанное, конечно, больше волнует с точки зрения разработки ядра ОС. Но почти всё применимо и к пользовательскому коду.
Кстати, ядра старых ОС в примитивах синхронизации не нуждались, поскольку преемптивной мультизадачности внутри ядра в старые добрые времена не было. (Уж за Юникс 7-й версии я отвечаю. Не было.) Точнее, единственным методом синхронизации был запрет прерываний. Но об этом позже.
Сначала перечислим героев. Мне известны следующие примитивы синхронизации:
User/kernel mode: mutex+cond, sema, enter/leave critical section.
Kernel only: spinlock, управление прерываниями.
Зачем всё это нужно, читатель, наверное, знает, но всё же уточним.
Если некоторая структура данных может быть доступна двум параллельно работающим нитям (или нити и прерыванию), и являет собой сущность, к которой нельзя обеспечить атомарный доступ, то работу с такой структурой нужно производить так, чтобы только одна нить одновременно выполняла сложные манипуляции с состоянием структуры.
Простой пример. Список.
struct list { list *next; list *prev };
Вставляем элемент в список.
new_el->next = curr_el->next;
new_el->prev = curr_el;
curr_el->next->prev = new_el; // 3
curr_el->next = new_el;
Всё примитивно. Но если этот код будут исполнять две нити параллельно, то вместо связного списка получится взрыв на макаронной фабрике. Например, если вторая нить включится в момент, когда первая нить закончила строку 3, то обходя список слева направо мы встретим на одном и том же месте один объект, а справа налево — другой.
Неприятно.
Применим мьютекс — mutually exclusive lock. Этот замок запрещает параллельное исполнение запертого им кода — если одна нить начала его исполнять, вторая будет ждать на входе до тех пор, пока первая не закончит.
mutex_lock( &list->mutex);
new_el->next = curr_el->next;
new_el->prev = curr_el;
curr_el->next->prev = new_el; // 3
curr_el->next = new_el;
mutex_unlock( &list->mutex);
Теперь хорошо. (Ну, не очень хорошо, если у нас более ста процессоров и участок кода у них популярен, но это совсем отдельный разговор.)
Что происходит? Нить А делает вызов mutex_lock для мьютекса list->mutex. Который, очевидно, принадлежит списку, который мы хотим поменять, и защищает доступ именно к нему. Он не заперт, нить А запирает мьютекс (теперь он знает, что заперт, и знает, кто его запер) и продолжает работу. Если теперь нить Б попробует войти в тот же регион кода (или другой, защищённый тем же мьютексом — например, в функции удаления элемента списка), то второй раз запереть запертый мьютекс не получится. Нить Б будет ждать, пока нить А не вызовет mutex_unlock.
Кстати, если мы с вами — ядерные разработчики, то важно понимать ещё одно, неинтересное для прикладного программиста свойство мьютекса (как и всех «тяжеловесных» примитивов синхронизации) — если мы пытаемся запереть мьютекс, который уже заперт другой нитью, мы не просто ждём — нашу нить «снимут с процессора», произойдёт переключение контекста. Это ценно, потому что позволяет куда более эффективно загружать процессор, но есть и проблемы. Внутри обработчика прерываний, например, такие примитивы применять нельзя совсем, потому что переключение контекста внутри прерывания запрещено и грозит изрядным раздраем в работе системы. Но, наверное, об этом надо будет написать отдельно.
Это вполне решает задачу, если надо поработать со сложной структурой данных. Но есть и другие задачи. Например, проинформировать другую нить о событии, которое та, другая нить, ждёт.
Рассмотрим функции alloc_mem и free_mem:
// NB! Заведомо неверный код!
alloc_mem()
{
while(total_free_mem <= 0)
{
wait_cond(&got_free_mem);
}
// actually allocate
}
free_mem()
{
// actually free mem
total_free_mem++;
signal_cond(&got_free_mem);
}
Что здесь происходит? Всё банально. В функции аллокации памяти мы смотрим на глобальный счётчик свободной памяти. Если пусто, свободной памяти нет, ждём пока кто-то не освободит память — вызываем wait_cond, который нас приостанавливает, пока кто-то не просигналит — готово, память освободили.
Это, конечно, функция free_mem() — она возвращает память в кучу, увеличивает счётчик свободной памяти и вызывает signal_cond — сообщает страждущим, что память есть. Тот, кто спал внутри wait_cond, «проснётся» после такого сигнала, проверит что да, память есть, и выделит её. Всё верно?
Ну, нет, конечно. Если функцию alloc_mem вызовут две нити сразу, то будет беда — одна из них получит сигнал первой, проснётся, убедится, что свободная память есть, и тут вдруг шедулер возьми да сними её с процессора. И дай проснуться второй такой же нити. Вторая нить проснётся, тоже увидит, что память есть, заберёт её и закончится. Просыпается
мафия
первая нить, и у неё всё плохо. Только что она проверила переменную free_mem, убедилась, что всё есть, и вот — никакой свободной памяти в пуле не находится. Беда.
Для данного случая беда не смертельная — можно просто вернуться к началу функции и снова ждать у моря погоды. Хотя, конечно, и это плохо — мы теряем процессорное время на пустые метания.
Но, вроде бы, мы же знаем ответ? Добавим mutex!
// NB! Снова заведомо неверный код!
alloc_mem()
{
mutex_lock( &allocator_mutex );
while(total_free_mem <= 0)
{
wait_cond(&got_free_mem);
}
// actually allocate
mutex_unlock( &allocator_mutex );
}
free_mem()
{
mutex_lock( &allocator_mutex );
// actually free mem
total_free_mem++;
signal_cond(&got_free_mem);
mutex_unlock( &allocator_mutex );
}
Так хорошо? Нет. Освобождение памяти не случится — функция alloc_mem() его заперла, заснула на ожидании cond, и никто больше в мьютекс войти не может, и никто не освободит память, и не просигналит.
Беда. Но ладно же, мы знаем, что делать! Перед тем, как заснуть на ожидании cond, мы отопрём mutex, и позволим другим войти в free и вернуть нам память. Вот так:
// NB! И опять заведомо неверный код!
alloc_mem()
{
mutex_lock( &allocator_mutex );
while(total_free_mem <= 0)
{
mutex_unlock( &allocator_mutex );
wait_cond(&got_free_mem);
mutex_lock( &allocator_mutex );
}
// actually allocate
mutex_unlock( &allocator_mutex );
}
По комментарию вы уже видите, что опять не слава богу. Что теперь? А теперь есть щёлочка, тонкая линия между моментом, когда мы проснулись и вышли из функции wait_cond, получив от free_mem сигнал об освобождении памяти, и захватом мьютекса. В этот момент мьютекс не взят, и другие нити опять могут нас опередить и набезобразить. Именно по этой причине функция wait_cond выглядит несколько иначе:
wait_cond( cond *c, mutex *m );
Работает это вот как: функция принимает на вход conditional variable, которая передаст нам сигнал «проснуться», и запертый мьютекс:
alloc_mem()
{
mutex_lock( &allocator_mutex );
while(total_free_mem <= 0)
{
wait_cond(&got_free_mem,&allocator_mutex);
}
// actually allocate
mutex_unlock( &allocator_mutex );
}
Функция wait_cond отопрёт мьютекс, во-первых, самостоятельно, а во-вторых сделает это атомарно по отношению к переходу в спящее состояние. То есть нить, входящая в wait_cond сначала заснёт, а потом, не прерывая сна, отопрёт мьютекс. И наоборот, просыпаясь, она сначала захватит мьютекс, а потом проснётся и продолжит работу. (Это требует от кода переключения нитей изрядной хитрости, постараюсь рассказать об этом в одной из следующих заметок.)
Только такая семантика обеспечивает 100% консистентность и отсутствие «гонок» — race conditions.
Отметим, что код функции free у нас получился вполне правильный:
free_mem()
{
mutex_lock( &allocator_mutex );
// actually free mem
total_free_mem++;
signal_cond(&got_free_mem); // 4
mutex_unlock( &allocator_mutex ); // 5
}
Только с учётом вышесказанного надо понимать, что хотя формально мы пробуждаем аллокатор на строке 4, реально проснётся он после исполнения строки 5, потому что до этого момента он не в состоянии захватить мьютекс.
К сказанному, наверное, имеет смысл добавить, что реальная функция signal_cond пробуждает не все ожидающие потоки, а только один (обычно — с наивысшим приоритетом), так что ситуация в приведённом примере несколько проще и сложнее одновременно. Проще потому что уже внутри сигнализации встроен механизм, который после одного free пробудит только один alloc, а сложнее потому, что реально это ничего не решает — мы не знаем, подойдёт ли данному alloc-у освобождённый участок, так что надо вместо signal_cond применить broadcast_cond, который таки пробудит всех страждущих, дав им возможность в честной драке определиться, кому достанется ресурс.
Посмотреть на фактическую реализацию этих примитивов можно здесь:
mutex.c, cond.c
В следующей серии — sema семафор, который в одиночку заменяет и mutex, и cond. Практически без ансамбля.
Это серия статей «Обзор примитивов синхронизации»:
- mutex и cond
- Семафор и немного lockless-а
- спинлоки и тайны ядра процессора
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. Мьютексы (Mutexes)
Мьютекс (взаимное исключение) — это примитив синхронизации, который используется для предотвращения одновременного доступа к общему ресурсу. Мьютекс может находиться в двух состояниях: захваченном или свободном. Только один поток может захватить мьютекс, и пока он не освободит его, другие потоки будут блокироваться при попытке доступа к ресурсу.
- Применение: Используется для защиты критических секций кода, где происходит работа с общими данными.
- Преимущества: Простота использования и высокая эффективность.
- Недостатки: Может привести к взаимным блокировкам (deadlocks), если не использовать правильно.
2. Семафоры (Semaphores)
Семафор — это более общий механизм синхронизации по сравнению с мьютексом. Семафор имеет счетчик, который управляет доступом к ресурсу. Потоки могут увеличивать или уменьшать значение счетчика, чтобы сигнализировать о доступности ресурса.
- Двоичные семафоры: Подобны мьютексам, но могут использоваться для сигнализации между потоками.
- Счетные семафоры: Позволяют управлять доступом к ресурсу, который может быть использован несколькими потоками одновременно.
3. Условные переменные (Condition Variables)
Условные переменные используются для блокировки потоков до наступления определенного условия. Они работают в связке с мьютексами и позволяют потокам ожидать изменения состояния, не занимая процессорное время.
- Применение: Используются для реализации сложных схем синхронизации, таких как очереди задач или управление доступом к буферу.
- Преимущества: Позволяют эффективно управлять ожиданием и уведомлением потоков.
4. Барьеры (Barriers)
Барьер — это примитив синхронизации, который блокирует набор потоков до тех пор, пока все они не достигнут определенной точки выполнения. После того как все потоки достигают барьера, они могут продолжить выполнение.
- Применение: Используется в алгоритмах, требующих синхронизации на этапах выполнения, например, в параллельных вычислениях.
- Преимущества: Обеспечивает синхронизацию группы потоков, что полезно для параллельных алгоритмов.
5. Атомарные операции
Атомарные операции — это операции, которые выполняются как единое целое без возможности прерывания. Они обеспечивают безопасный доступ к общим данным без использования блокировок.
- Применение: Используются для инкрементации счетчиков, обновления указателей и других простых операций.
- Преимущества: Высокая производительность и отсутствие блокировок.
Заключение
Примитивы синхронизации играют ключевую роль в разработке надежных и эффективных многопоточных и многопроцессных приложений. Выбор подходящего примитива зависит от конкретных требований задачи, таких как необходимость в производительности, сложность алгоритма и требования к безопасности данных. Правильное использование этих механизмов позволяет избежать ошибок, связанных с параллельным выполнением, и обеспечивает корректную работу программного обеспечения.
-
Создано 18.02.2025 13:49:11
-
Михаил Русаков
Копирование материалов разрешается только с указанием автора (Михаил Русаков) и индексируемой прямой ссылкой на сайт (http://myrusakov.ru)!
Добавляйтесь ко мне в друзья ВКонтакте: http://vk.com/myrusakov.
Если Вы хотите дать оценку мне и моей работе, то напишите её в моей группе: http://vk.com/rusakovmy.
Если Вы не хотите пропустить новые материалы на сайте,
то Вы можете подписаться на обновления: Подписаться на обновления
Если у Вас остались какие-либо вопросы, либо у Вас есть желание высказаться по поводу этой статьи, то Вы можете оставить свой комментарий внизу страницы.
Если Вам понравился сайт, то разместите ссылку на него (у себя на сайте, на форуме, в контакте):
-
Кнопка:
Она выглядит вот так:
-
Текстовая ссылка:
Она выглядит вот так: Как создать свой сайт
- BB-код ссылки для форумов (например, можете поставить её в подписи):
Примитивы синхронизации
Примитивы взаимоисключения
- Семафоры — критическая секция с возможностью входа для нескольких потоков
- Мьютексы — бинарный семафор, имеющий два состояния: свободный и заблокированный. Бывают с рекурсивным захватом. Работают на уровне пользователя.
- Критические секции — Объект синхронизации, обычно эквивалентный мьютексу. Работают на уровне ядра.
- Спинлоки — блокировка не участков кода, а данных. Критическая секция на 1 переменную.
Принцип невероятно прост. Спинлок — это просто переменная, которая содержит ноль или единицу (бывают варианты).
Если ноль — спинлок свободен, и его можно захватить. Если не ноль — спинлок заперт, и нить, которая желает его захватить, будет ждать, крутясь (spin — вращение) в небольшом цикле непрерывной проверки освобождения спинлока.
- Мониторы — блокировка всего объекта, который работает с синхронизированной информацией для других потоков.
- Условные переменные — блокирование одного или нескольких потоков до момента поступления сигнала от другого потока о выполнении некоторого условия или до истечения максимального промежутка времени ожидания
- Сравнение с обменом — атомарная инструкция, сравнивающая значение в памяти с одним из аргументов, и в случае успеха записывающая второй аргумент в память
- RWLock — чтение блокирует запись, но не блокирует чтение других тредов, а запись блокирует все.
Примитивы управления последовательностью выполнения
- Барьеры (barrier), рандеву – примитив синхронизации во времени нескольких потоков/процессов. Поток, остановившийся на барьере, ожидает другой (другие потоки), когда они «дойдут» до этого же барьера
Другие средства
- Файл — обычный файл также является средством межпроцессорного взаимодействия
- Сигналы и сообщения — могут означать множество вещей: вызов функций, сигналы, данные.
- Сокет — программный итерфейс для обмена нанными между процессами
- Канал (Конвееер) — один процесс/поток записывает в поток ввода/вывода, а другой считывает
- Именованный канал — позволяет различным процессам обмениваться данными, даже если программы, выполняющиеся в этих процессах, изначально не были написаны для взаимодействия с другими программами
- Разделяемая память — средство синхронизации между процессами. Дополняется семафором или мьютексом
- Почтовый ящик (Mailslot) — обеспечивающий однонаправленную передачу информации и позволяющий производить широковещательную рассылку сообщений по сети
- Очередь сообщений
- Проецируемый в память файл (mmap) — для более быстрого доступа и синхранизации доступа к файлу
1. Синхронизация потоков в Windows
СИНХРОНИЗАЦИЯ
ПОТОКОВ В
WINDOWS
2.
Критические секции
Для работы с объектами типа CRITICAL_SECTION
используются следующие функции:
// инициализация критической секции
VOID
InitializeCriticalSection(LPCRITICAL_SECTION
lpCriticalSection);
// вход в критическую секцию
VOID
EnterCriticalSection(LPCRITICAL_SECTION
lpCriticalSection);
// попытка войти в критическую секцию
BOOL
TryEnterCriticalSection(LPCRITICAL_SECTION
lpCriticalSection);
// выход из критической секции
VOID
LeaveCriticalSection(LPCRITICAL_SECTION
lpCriticalSection);
3.
#include <windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI thread(LPVOID) {
int i, j ;
for (j = 0; j < 10; ++j) {
// выводим строку чисел j
for (i = 0; i < 10; ++i)
{
cout « j « ‘ ‘ « flush;
Sleep(17);
}
cout « endl;
return 0;
}
4.
int main() {
int i, j ;
HANDLE hThread; DWORD IDThread;
hThread=CreateThread(NULL, 0, thread, NULL, 0,
&IDThread) ;
if (hThread == NULL) return GetLastError();
for (j = 10; j < 20; ++j) {
for (i = 0; i < 10; ++i) {
// выводим строку чисел j
cout « j « ‘ ‘ « flush;
Sleep(17);
}
cout « endl;
}
// ждем, пока поток thread закончит свою работу
WaitForSingleObject(hThread, INFINITE);
return 0;
}
5.
#include <windows.h> #include <iostream>
using namespace std;
CRITICAL_SECTION cs;
DWORD WINAPI thread(LPVOID)
{
int i,j;
for (j = 0; j < 10; ++j)
{
// входим в критическую секцию
EnterCriticalSection (&cs);
for (i = 0; i < 10; ++i)
{
cout « j « ‘ ‘« flush;
Sleep(7);
}
cout « endl;
// выходим из критической секции
LeaveCriticalSection(&cs);
}
return 0;
}
6.
int main() {
int i,j;
HANDLE hThread; DWORD IDThread;
// инициализируем критическую секцию
InitializeCriticalSection(&cs);
hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread);
if (hThread == NULL) return GetLastError();
for (j = 10; j < 20; ++j) {
// входим в критическую секцию
EnterCriticalSection(&cs);
for (i = 0; i < 10; ++i) {
cout « j « ‘ ‘ « flush; Sleep(7) ;
}
cout « endl;
// выходим из критической секции
LeaveCriticalSection(&cs);
}
// ждем, пока поток thread закончит свою работу
WaitForSingleObject(hThread, INFINITE);
// закрываем критическую секцию
DeleteCriticalSection(&cs);
return 0;
}
7.
Объекты синхронизации и функции ожидания
В операционных системах Windows объектами
синхронизации
называются
объекты
ядра,
которые могут находиться в одном из двух
состояний: сигнальном (signaled) и несигнальном
(nonsignaled). Объекты синхронизации могут быть
разбиты на четыре класса.
8.
К первому классу относятся объекты синхронизации, т. е. те,
которые служат только для решения задач синхронизации
параллельных потоков:
мьютекс (mutex);
событие (event);
семафор (semaphore).
Ко второму классу объектов синхронизации относится
ожидающий таймер (waitable timer), который переходит в
сигнальное состояние по истечении заданного интервала
времени.
К третьему классу синхронизации относятся объекты, которые
переходят в сигнальное состояние по завершении своей работы:
работа (job);
процесс (process);
поток (thread).
К четвертому классу относятся объекты синхронизации, которые
переходят в сигнальное состояние после получения сообщения об
изменении содержимого объекта. К ним относятся:
изменение состояния каталога (change notification);
консольный ввод (console input).
9.
Функции ожидания в Windows это такие функции,
параметрами
которых
являются
объекты
синхронизации. Эти функции обычно используются
для блокировки потоков.
Для ожидания перехода в сигнальное состояние
одного объекта синхронизации используется функция
WaitForSingieObject, которая имеет следующий
прототип:
DWORD WaitForSingleObject(
HANDLE hHandle,
// дескриптор объекта
DWORD dwMilliseconds // интервал ожидания в
миллисекундах
);
10.
Для ожидания перехода в сигнальное состояние
нескольких объектов синхронизации или одного из
нескольких объектов синхронизации используется
функция WaitForMuitipieObject, которая имеет
следующий прототип:
DWORD WaitForMultipleObjects(
DWORD nCount,
// количество объектов
CONST HANDLE *lpHandles,
// массив
дескрипторов
объектов
BOOL bWaitAll,
// режим ожидания
DWORD dwMilliseconds
// интервал
ожидания в
миллисекундах
);
11.
Мьютексы
Для
решения
проблемы
взаимного
исключения между параллельными потоками,
выполняющимися в контекстах разных
процессов, в операционных системах Windows
используется объект ядра мьютекс. Слово
мьютекс происходит от английского слова
mutex, которое в свою очередь является
сокращением от выражения mutual exclusion,
что на русском языке значит «взаимное
исключение».
Мьютекс
находится
в
сигнальном
состоянии,
если
он
не
принадлежит ни одному потоку. В противном
случае мьютекс находится в несигнальном
состоянии. Одновременно мьютекс может
принадлежать только одному потоку.
12.
Создается мьютекс вызовом функции CreateMutex,
которая имеет следующий прототип:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
// атрибуты защиты
BOOL blnitialOwner,
// начальный владелец мьютекса
LPCTSTR lpName
// имя мьютекса
);
13.
#include <windows.h>
#include <iostream>
HANDLE hMutex;
char IpszAppName [] = «С:\\ConsoleProcess.exe»;
STARTUPINFO si;
PROCESS_INFORMATION pi;
// создаем мьютекс
hMutex = CreateMutex(NULL, FALSE,
«DemoMutex»);
if (hMutex == NULL)
{
cout « «Create mutex failed.» « endl;
cout « «Press any key to exit.» « endl;
cin.get();
return GetLastError();
}
14.
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
// создаем новый консольный процесс
if (!CreateProcess(IpszAppName, NULL, NULL, NULL,
FALSE, NULL, NULL, NULL, &si, &pi))
{
cout « «The new process is not created.» « endl;
cout « «Press any key to exit.» « endl;
cin.get();
return GetLastError();
}
// выводим на экран строки
for (int j = 0; j < 10; ++j) {
// захватываем мьютекс
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 10; i++) {
cout « j « ‘ ‘ « flush; Sleep(10);
15.
}
cout « endl; // освобождаем мьютекс
ReleaseMutex (hMutex) ;
}
// закрываем дескриптор мьютекса
CloseHandle(hMutex);
// ждем пока дочерний процесс закончит работу
WaitForSingleObject (pi .hProcess, INFINITE) ;
// закрываем дескрипторы дочернего процесса в
текущем процессе
CloseHandle (pi .hThread) ;
CloseHandle(pi.hProcess);
return 0;
}
16.
#include <windows.h>
#include <iostream>
int main() {
HANDLE hMutex;
int i , j ;
// открываем мьютекс
hMutex = OpenMutex(SYNCHRONIZE,
FALSE,»DemoMutex»);
if (hMutex == NULL) {
cout « «Open mutex failed.» « endl;
cout « «Press any key to exit.» « endl;
cin.get();
return GetLastError();
for (j = 10; j < 20; j++) {
17.
// захватываем мьютекс
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 10; i++) {
cout « j « ‘ ‘ « flush;
Sleep(5);
}
cout « endl; // освобождаем мьютекс
ReleaseMutex(hMutex);
}
// закрываем дескриптор объекта
CloseHandle(hMutex);
return 0;
}
18.
События
Событием называется оповещение о
некотором выполненном действии. В
программировании
события
используются для оповещения одного
потока о том, что другой поток
выполнил некоторое действие. Сама же
задача оповещения одного потока о
некотором действии, которое совершил
другой поток, называется задачей
условной
синхронизации.
В
операционных
системах
Windows
события описываются объектами ядра
Events.
При этом различают два типа событий:
события с ручным сбросом;
события с автоматическим сбросом.
19.
Создаются
события
CreateEvent,
которая
прототип:
вызовом
имеет
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES
IpSecurityAttributes,
// атрибуты защиты
BOOL bManualReset,
// тип события
BOOL blnitialState,
// начальное состояние события
LPCTSTR lpName
// имя события
);
функции
следующий
20.
Для перевода любого события в сигнальное
состояние используется функция SetEvent,
которая имеет следующий прототип:
BOOL SetEvent(
HANDLE hEvent // дескриптор события
);
21.
#include <windows.h>
#include <iostream>
HANDLE hOutEvent, hAddEvent;
DWORD WINAPI thread (LPVOID) {
for (int i = 0; i < 10; ++i)
if (i == 4) {
SetEvent(hOutEvent);
WaitForSingleObject(hAddEvent, INFINITE);
}
return 0;
}
int main() {
HANDLE hThread; DWORD IDThread;
22.
// создаем события с автоматическим сбросом
hOutEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (hOutEvent == NULL)
return GetLastError();
hAddEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (hAddEvent == NULL) return GetLastError();
// создаем поток thread
hThread = CreateThread(NULL, 0, thread, NULL, 0,
&IDThread);
if (hThread == NULL)
return GetLastError();
// ждем, пока поток thread выполнит половину работы
WaitForSingleObject(hOutEvent, INFINITE);
23.
// выводим значение переменной
cout « «A half of the work is done.» « endl;
cout « «Press any key to continue.» « endl;
cin.get();
// разрешаем дальше работать потоку thread
SetEvent (hAddEvent) ;
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hOutEvent);
CloseHandle(hAddEvent);
cout « «The work is done.» « endl;
return 0;
}
24.
Доступ к существующему событию можно открыть
с помощью функции
CreateEvent или OpenEvent.
HANDLE OpenEvent(
DWORD dwDesiredAccess, // флаги доступа
BOOL blnheritHandle, // режим наследования
LPCTSTR lpName
// имя события
);
25.
Параметр dwDesiredAccess определяет доступ к
событию и может быть равен любой логической
комбинации следующих флагов:
ЕVENT_ALL_АСCESS — полный доступ;
ЕVENT_MODIFY_SТАТЕ — модификация состояния;
SYNCHRONIZE — синхронизация.
Эти флаги устанавливают следующие режимы
доступа к событию:
флаг ЕVENT_ALL_АСCESS означает, что поток
может выполнять над событием любые действия;
флаг EVENT_MODIFY_STATE означает, что поток
может использовать функции SetEvent и
ResetEvent для изменения состояния события;
флаг SYNCHRONIZE означает, что поток может
использовать событие в функциях ожидания.
26.
#include <windows.h>
#include <iostream>
HANDLE hlnEvent;
char IpEventName[ ] = «InEventName»;
int main() {
DWORD dwWaitResult;
char szAppName[] = «D:\\ConsoleProcess.exe»;
STARTUPINFO si;
PROCESS_INFORMATION pi;
// создем событие, отмечающее ввод символа
hlnEvent = CreateEvent(NULL, FALSE, FALSE,
IpEventName);
if (hlnEvent == NULL)
return GetLastError();
27.
// запускаем процесс, который ждет ввод символа
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
if (!CreateProcess(szAppName, NULL, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
return 0;
// закрываем дескрипторы этого процесса
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
// ждем оповещение о наступлении события о вводе символа
dwWaitResult = WaitForSingieObject(hlnEvent, INFINITE);
if (dwWaitResult != WAIT_OBJECT_0) return dwWaitResult;
cout « «A symbol has got. » « endl;
CloseHandie (hlnEvent) ;
cout « «Press any key to exit.»;
cin.get();
return 0;
}
28.
#include <windows.h>
#include <iostream>
HANDLE hlnEvent;
CHAR lpEventName[]=»InEventName» ;
int main () {
char c;
hlnEvent = OpenEvent (EVENT_MODIFY_STATE,
FALSE, lpEventName);
if (hlnEvent == NULL) {
cout « «Open event failed.» « endl;
cout « «Input any char to exit.» « endl;
cin.get();
return GetLastError();
}
29.
cout « «Input any char: «;
cin » c;
// устанавливаем событие о вводе символа
SetEvent(hlnEvent);
// закрываем дескриптор события в текущем
процессе
CloseHandie(hlnEvent);
cin.get();
cout << «Press any key to exit.» << endl;
cin.get();
return 0;
}
30.
Семафоры
Семафоры
в операционных системах Windows
описываются объектами ядра semaphores. Семафор
находится в сигнальном состоянии, если его значение
больше нуля. В противном случае семафор находится
в несигнальном состоянии.
Создаются семафоры посредством вызова функции
CreateSemaphore,
которая
имеет
следующий
прототип:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES IpSemaphoreAttribute,
// атрибуты защиты
LONG llnitialCount,
// начальное значение семафора
LONG IMaximumCount,
// максимальное значение семафора
LPCTSTR lpName
// имя семафора
);
31.
BOOL ReleaseSemaphore
(
HANDLE hSemaphore,// дескриптор семафора
LONG IReleaseCount, // положительное число,
на которое увеличивается значение
семафора
LPLONG lpPreviousCount // предыдущее
значение семафора
);
32.
HANDLE OpenSemaphore(
DWORD dwDesiredAccess, // флаги доступа
BOOLblnheritHandle,
// режим наследования
LPCTSTR lpName
// имя события
);
Параметр dwDesiredAccess определяет доступ к
семафору и может быть равен любой логической
комбинации следующих флагов:
sEMAPHORE_ALL_ACCESS — полный доступ к
семафору;
SEMAPHORE_MODIFY_STATE
—
изменение
состояния семафора;
SYNCHRONIZE — синхронизация.
33.
#include <windows.h>
#include <iostream>
volatile int a[10];
HANDLE hSemaphore;
DWORD WINAPI thread(LPVOID)
{
for (int i = 0; i < 10; i++)
{
a [i] = i + 1;
// отмечаем, что один элемент готов
ReleaseSemaphore(hSemaphore,1,NULL);
Sleep(500) ;
}
return 0;
}
34.
int main()
{
int i;
HANDLE hThread;
DWORD IDThread;
cout « «An initial state of the array: «;
for (i = 0; i < 10; i++)
cout « a[i] «’ ‘;
cout « endl;
// создаем семафор
hSemaphore=CreateSemaphore(NULL/ 0, 10, NULL);
if (hSemaphore == NULL) return GetLastError();
// создаем поток, который готовит элементы массива
hThread = CreateThread(NULL, 0, thread, NULL, 0,
&IDThread);
if (hThread == NULL) return GetLastError();
35.
// поток main выводит элементы массива только после их
подготовки потоком thread
cout « «A final state of the array: «;
for (i = 0; i < 10; i++)
{
WaitForSingleObject(hSemaphore, INFINITE);
cout « a[i] « » \a» « flush;
}
cout « endl;
CloseHandle(hSemaphore);
CloseHandle(hThread);
return 0;
}