Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Синхронизация.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
229.38 Кб
Скачать

Мьютексы в Unix-подобных системах

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

  • PTHREAD_MUTEX_NORMAL — нет контроля повторного захвата тем же потоком (thread)

  • PTHREAD_MUTEX_RECURSIVE — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов

  • PTHREAD_MUTEX_ERRORCHECK — повторные захваты тем же потоком вызывают немедленную ошибку

Мьютексы в языке Си

Последний стандарт языка Си (ISO/IEC 9899:2011) определяет тип mtx_t и функции для работы с ним, которые должны быть доступны если макрос __STDC_NO_THREADS__ не был определён компилятором. Семантика и свойства мьютексов, в целом, совпадают со стандартом POSIX:

  • mtx_plain — нет контроля повторного захвата тем же потоком;

  • mtx_recursive — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов;

  • mtx_timed — поддерживается захват мьютекса с тайм-аутом (следует отметить, что в отличие от стандарта POSIX, поддержка этого свойства мьютекса не является опциональной).

Возможность использования мьютексов в разделяемой памяти различных процессов в стандарте Си11 не рассматривается.

Мьютексы в языке C++

Последний стандарт языка C++ (ISO/IEC 14882:2011) определяет различные классы мьютексов:

  • mutex — нет контроля повторного захвата тем же потоком;

  • recursive_mutex — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов;

  • timed_mutex — нет контроля повторного захвата тем же потоком, поддерживается захват мьютекса с тайм-аутом;

  • recursive_timed_mutex — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов, поддерживается захват мьютекса с тайм-аутом.

Следует отметить библиотеку Boost, которая обеспечивает:

  • Реализацию мьютексов, совместимых по интерфейсу со стандартом C++11 для компиляторов и платформ, которые не поддерживают этот стандарт;

  • Реализацию дополнительных классов мьютексов: shared_mutex и др., которые позволяют захватывать мьютекс для совместного владения несколькими потоками только для чтения данных.

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

  1. Два процесса не должны одновременно находиться в критических областях.

  2. В программе не должно быть предположений о скорости или количестве процессоров.

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

  4. Невозможна ситуация, в которой процесс вечно ждет попадания в критическую область.

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

Между мьютексом и критической секцией есть терминологические различия, так процедура, аналогичная захвату мьютекса, называется входом в критическую секцию (enter), снятию блокировки мьютекса — выходом из критической секции (leave).

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

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

Критические секции Windows имеют оптимизацию, заключающуюся в использовании атомарно изменяемой переменной наряду с объектом «событие синхронизации» ядра. Захват критической секции означает атомарное увеличение переменной на 1. Переход к ожиданию на событии ядра осуществляется только в случае, если значение переменной до захвата было уже больше 0, то есть происходит реальное «соревнование» двух или более потоков за ресурс.

Таким образом, при отсутствии соревнования захват/освобождение критической секции обходятся без обращений к ядру.

Кроме того, захват уже занятой критической секции до обращения к ядру какое-то малое время ждёт в цикле (кол-во итераций цикла (spin count) задаётся функциями InitializeCriticalSectionAndSpinCount() или SetCriticalSectionSpinCount()) опроса переменной числа текущих пользователей, и, если эта переменная становится равной 0, то захват происходит без обращений к ядру.

Сходный объект в ядре Windows называется FAST_MUTEX (ExAcquire/ReleaseFastMutex). Он отличается от критической секции отсутствием поддержки рекурсивного повторного захвата тем же потоком.

Аналогичный объект в Linux называется фьютекс.

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

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

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

Состоя́ние го́нки (race condition) — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Своё название ошибка получила от похожей ошибки проектирования электронных схем (Гонки сигналов). Состояние гонки — «плавающая» ошибка (гейзенбаг), проявляющаяся в случайные моменты времени и «пропадающая» при попытке её локализовать.

Пример кода (на Java).

volatile int x;

// Поток 1:

while (!stop)

{

x++;

}

// Поток 2:

while (!stop)

{

if (x%2 == 0)

System.out.println("x=" + x);

}

Пусть x=0. Предположим, что выполнение программы происходит в таком порядке:

  1. Оператор if в потоке 2 проверяет x на чётность.

  2. Оператор «x++» в потоке 1 увеличивает x на единицу.

  3. Оператор вывода в потоке 2 выводит «x=1», хотя, казалось бы, переменная проверена на чётность.

Способы решения

Локальная копия

Самый простой способ решения — копирование переменной x в локальную переменную. Вот исправленный код:

// Поток 2:

while (!stop)

{

int cached_x = x;

if (cached_x%2 == 0)

System.out.println("x=" + cached_x);

}

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

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

Более сложный, но и более универсальный метод решения — синхронизация потоков, а именно:

int x;

// Поток 1:

while (!stop)

{

synchronized(SomeObject)

{

x++;

}

}

// Поток 2:

while (!stop)

{

synchronized(SomeObject)

{

if (x%2 == 0)

System.out.println("x=" + x);

}

}

Здесь семантика happens before не требует ключевое слово volatile.

Комбинированный способ

Предположим, что переменных две (и ключевое слово volatile не действует), а во втором потоке вместо System.out.println стоит более сложная обработка. В этом случае оба метода неудовлетворительны: первый — потому что одна переменная может измениться, пока копируется другая; второй — потому что засинхронизирован слишком большой объём кода.

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

volatile int x1, x2;

// Поток 1:

while (!stop)

{

synchronized(SomeObject)

{

x1++;

x2++;

}

}

// Поток 2:

while (!stop)

{

int cached_x1, cached_x2;

synchronized (SomeObject)

{

cached_x1 = x1;

cached_x2 = x2;

}

if ((cached_x1 + cached_x2)%100 == 0)

DoSomethingComplicated(cached_x1, cached_x2);

}

Очевидных способов выявления и исправления состояний гонки не существует. Лучший способ избавиться от гонок — правильное проектирование многозадачной системы.

Взаи́мная блокиро́вка (deadlock) — ситуация в многозадачной среде или СУБД, при которой несколько процессов находятся в состоянии бесконечного ожидания ресурсов, занятых самими этими процессами.

Взаимная блокировка двух процессов P1 и P2 нуждающихся в двух ресурсах.