Мьютекс

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

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

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

По типу реализации мьютекс может быть быстрым,  или с контролем ошибок.

Проблемы использования

Инверсия приоритетов 

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

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

Прикладное программирование

Мьютексы в Win32 API 
Win32 API в Windows имеет две реализации мьютексов — собственно мьютексы, имеющие имена и доступные для использования между разными процессами, и критические секции, которые могут использоваться только в пределах одного процесса разными потоками. Для каждого из этих двух типов мьютексов используются свои функции захвата и освобождения. Критическая секция в Windows работает несколько быстрее и является более эффективной по сравнению с мьютексом и семафором, поскольку использует специфичную для процессора инструкцию test-and-set.

Мьютексы в POSIX 
Пакет Pthreads предоставляет различные функции, которые можно использовать для синхронизации потоков. Среди этих функций есть и функции для работы с мьютексами. В дополнение к функциям захвата и освобождения мьютекса предусмотрена функция попытки захвата мьютекса, которая возвращает ошибку, если предвидится блокировка потока. Данную функцию можно использовать в активном цикле ожидания, если возникает такая необходимость.

Для решения специализированных задач мьютексам могут задаваться различные атрибуты. Через атрибуты с помощью функции pthread_mutexattr_settype() можно задать тип мьютекса, что повлияет на поведение функций захвата и освобождения мьютекса. Мьютекс может быть одного из трёх типов:
 PTHREAD_MUTEX_NORMAL — при повтроной попытке захвата мьютекса владеющим потоком происходит взаимоблокировка;
 PTHREAD_MUTEX_RECURSIVE — повторные захваты тем же потоком допустимы, ведётся подсчёт таких захватов;
 PTHREAD_MUTEX_ERRORCHECK — попытка повторного захвата тем же потоком возвращает ошибку.

Мьютексы в языке Си 
Стандарт С17 языка программирования Си определяет тип mtx_t и набор функций для работы с ним, которые должны быть доступны, если макрос __STDC_NO_THREADS__ не был определён компилятором. Семантика и свойства мьютексов в целом совпадают со стандартом POSIX.

Тип мьютекса определяется передачей комбинации флагов в функцию mtx_init():
 mtx_plain — нет контроля повторного захвата тем же потоком;
 mtx_recursive — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов;
 mtx_timed — поддерживается захват мьютекса с возвращением ошибки по истечении указанного времени.
Возможность использования мьютексов через разделяемую память различными процессами в стандарте C17 не рассматривается.

Мьютексы в языке C++ 
Стандарт C++17 языка программирования C++ определяет 6 различных классов мьютексов:
 mutex — мьютекс без контроля повторного захвата тем же потоком;
 recursive_mutex — повторные захваты тем же потоком допустимы, ведётся подсчёт таких захватов;
 timed_mutex — нет контроля повторного захвата тем же потоком, имеет дополнительные методы захвата мьютекса с возвратом значения false в случае истечения тайм-аута или по достижении указанного времени;
 recursive_timed_mutex — повторные захваты тем же потоком допустимы, ведётся подсчёт таких захватов, имеет дополнительные методы захвата мьютекса с возвратом кода ошибки по истечении тайм-аута или по достижении указанного времени;
 shared_mutex — разделяемый мьютекс;
 shared_timed_mutex — разделяемый мьютекс, имеет дополнительные методы захвата мьютекса с возвратом кода ошибки по истечении тайм-аута или по достижении указанного времени.

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

Детали реализации

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

В архитектурах x86 и x86_64 
Простота мьютексов позволяет реализовать их в пространстве пользователя с помощью ассемблерной команды XCHG, которая может атомарно копировать значение мьютекса в регистр и одновременно выставлять значение мьютекса в 1 (предварительно записанное в тот же регистр). Нулевое значение мьютекса означает, что он находится в заблокированном состоянии, а единичное — в разблокированном. Значение из регистра может быть протестировано на 0, и в случае нулевого значения управление должно быть возвращено программе, что означает захват мьютекса, если же значение являлось ненулевым, то управление должно быть передано планировщику для возобновления работы другого потока с последующей повторной попыткой захвата мьютекса, что служит аналогом активной блокировки. Разблокировка мьютекса осуществляется сохранением в мьютексе значения 0 с помощью команды XCHG. Альтернативно может использоваться LOCK BTS (реализация TSL для одного бита) или  CMPXCHG (реализация CAS).

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

В архитектуре ARM 

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

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

Перед освобождением мьютекса также вызывается инструкция DMB, после чего в переменную мьютекса записывается значение 0 с помощью инструкции STR, что означает перевод в разблокированное состояние. После разблокировки мьютекса должно произойти сигнализирование ожидающим задачам, если таковые есть, о том, что мьютекс был освобождён.

См. также 
 Многопоточность
 Семафор
 Фьютекс
 Спин-блокировка

Примечания

Литература 
 
 
 
 

Управление конкурентными потоками