Блокированные вызовы
Лабораторная работа № 6
Объекты и функции синхронизации Win32
ОС Windows предоставляет широкий набор средств, предназначенных для синхронизации потоков и процессов. Рассмотрим их в порядке их усложнения и возрастания мощности. Наиболее простым средством являются блокированные вызовы или функции взаимоблокировки. Приостановить поток до перехода какого-либо объекта ядра в сигнальное состояние позволяют функции ожидания. Затем рассматриваются четыре основных объекта синхронизации: критическая секция, мьютекс, семафор и событие. Иногда для синхронизации применяются два других типа объектов – таймер ожидания и порты завершения ввода/вывода.
Блокированные вызовы
Рассмотрим два потока, в каждом из которых увеличивается значение одной и той же переменной.
long i = 0; //определение глобальной переменной процесса
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
i++;
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
i++;
return(0);
}
Оба потока увеличивают значение переменной i на 1, таким образом можно предположить, что при одновременном их запуске значение общей переменной станет равно 2. Однако это не всегда так. Рассмотрим примерную последовательность машинных команд, которую сгенерировал бы компилятор для рассматриваемых потоковых функций.
MOV ЕАХ, i ; значение из i помещается в регистр
INC ЕАХ ; значение регистра увеличивается на 1
MOV i, ЕАХ ; значение из регистра помещается обратно в i
При последовательном выполнении потоков, то есть когда второй поток начнет выполнение после первого, значение переменной i действительно получит значение 2.
MOV ЕАХ, i ; поток 1
INC ЕАХ ; поток 1
MOV i, ЕАХ ; поток 1
MOV ЕАХ, i ; поток 2
INC ЕАХ ; поток 2
MOV i, ЕАХ ; поток 2
Но в Windows переключение потоков может произойти в любой момент времени, и последовательность выполнения команд может измениться.
MOV ЕАХ, i ; поток 1
IHC ЕАХ ; поток 1
MOV ЕАХ, i ; поток 2
INC ЕАХ ; поток 2
MOV i, ЕАХ ; поток 2
MOV i, ЕАХ ; поток 1
После выполнение такой последовательности команд значение переменной станет равно 1. Приведенный пример наглядно демонстрирует сложность разработки многопоточных приложений и необходимость включения в программный код средств синхронизации доступа потоков к разделяемым данным.
Один из самых простых и действенных способов, гарантирующих изменение значение переменной на уровне атомарного доступа (без прерывания другими потоками) – это использование функций взаимоблокировки (Interlocked-функций).
Interlocked-функции – это простейшая форма механизмов синхронизации операций над целыми числами и выполнения сравнений в многопроцессорной среде. Данный набор функций опирается на аппаратную поддержку процессора при доступе к памяти. Например, функция InterlockedDecrement использует префикс команды lock для блокировки шины на время операции вычитания, чтобы другой процессор, модифицирующий тот же участок памяти, не мог выполнить свою операцию в момент между чтением исходных данных и записью их нового значения.
InterlockedDecrement | Вычитает единицу из блокированной переменной |
InterlockedIncrement | Добавляет единицу к блокированной переменной |
InterlockedExchange | Меняет местами значение блокированной переменной и значение другой переменной |
InterlockedExchangeAdd | Добавляет значение к блокированной переменной |
InterlockedCompareExchange | Сравнивает значение блокированной переменной с некоторым значением и в случае, если значения равны, меняет местами значение блокированной переменной и значение некоторой другой переменной |
Приведенный выше пример, реализованный с помощью функции InterlockedExchangeAdd, всегда будет давать верный результат.
long i = 0;
DWORD WINAPI ThreadFund(PVOID pvParam)
{
InterlockedExchangeAdd(&i, 1);
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
InterlockedExchangeAdd(&i, 1);
return(0);
}
Важный аспект, связанный с Interlocked-функциями, состоит в том, что они выполняются чрезвычайно быстро. Вызов такой функции обычно требует не более 50 тактов процессора, и при этом не происходит перехода из пользовательского режима в режим ядра (а он отнимает не менее 1000 тактов).