Мьютекс

(перенаправлено с «Мютекс»)

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

Попытка одновременного удаления двух узлов i и i + 1 приводит к сохранению узла i + 1.

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

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

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

Инверсия приоритетовПравить

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

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

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

Мьютексы в Win32 APIПравить

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

Мьютексы в POSIXПравить

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

Функции пакета Pthreads для работы с мьютексами
Функция Описание
pthread_mutex_init() Создание мьютекса[11].
pthread_mutex_destroy() Уничтожение мьютекса[11].
pthread_mutex_lock() Перевод мьютекса в заблокированное состояние (захват мьютекса)[11].
pthread_mutex_trylock() Попытка перевода мьютекса в заблокированное состояние, и возврат ошибки в случае, если должна произойти блокировка потока из-за того, что у мьютекса уже есть владелец[11].
pthread_mutex_timedlock() Попытка перевода мьютекса в заблокированное состояние, и возврат ошибки в случае, если попытка не удалась до наступления указанного момента времени[12].
pthread_mutex_unlock() Перевод мьютекса в незаблокированное состояние (отпускание мьютекса)[11].

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

  • PTHREAD_MUTEX_NORMAL — при повтроной попытке захвата мьютекса владеющим потоком происходит взаимоблокировка[14];
  • PTHREAD_MUTEX_RECURSIVE — повторные захваты тем же потоком допустимы, ведётся подсчёт таких захватов[14];
  • PTHREAD_MUTEX_ERRORCHECK — попытка повторного захвата тем же потоком возвращает ошибку[14].

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

Стандарт С17 языка программирования Си определяет тип mtx_t[15] и набор функций для работы с ним[16], которые должны быть доступны, если макрос __STDC_NO_THREADS__ не был определён компилятором[15]. Семантика и свойства мьютексов в целом совпадают со стандартом POSIX, тип семафора определяется передачей комбинации флагов в функцию mtx_init()[17]:

  • mtx_plain — нет контроля повторного захвата тем же потоком[18];
  • mtx_recursive — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов[19];
  • mtx_timed — поддерживается захват мьютекса с возвращением ошибки по истечению указанного времени[19].

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

Мьютексы в языке C++Править

Стандарт С++17 языка программирования C++ определяет 4 различных класса мьютексов[20]:

  • mutex — мьютекс без контроля повторного захвата тем же потоком[21];
  • recursive_mutex — повторные захваты тем же потоком допустимы, ведётся подсчёт таких захватов[22];
  • timed_mutex — нет контроля повторного захвата тем же потоком, поддерживается захват мьютекса с выбрасыванием исключительной ситуации по истечении указанного времени в случае неудачи[23];
  • recursive_timed_mutex — повторные захваты тем же потоком допустимы, ведётся подсчёт таких захватов, поддерживается захват мьютекса с выбрасыванием исключительной ситуации по истечении указанного времени в случае неудачи[24].

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

Детали реализацииПравить

На уровне операционных системПравить

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

В архитектурах x86 и x86_64Править

Простота мьютексов позволяет реализовать их в пространстве пользователя с помощью ассемблерной команды XCHG, которая может атомарно копировать значение мьютекса в регистр и одновременно выставлять значение мьютекса в 1 (предварительно записанное в тот же регистр). Нулевое значение мьютекса означает, что он находится в заблокированном состоянии, а единичное — в разблокированном. Значение из регистра может быть протестировано на 0, и в случае нулевого значения управление должно быть возвращено программе, что означает захват мьютекса, если же значение являлось ненулевым, то управление должно быть передано планировщику для возобновления работы другого потока с последующей повторной попыткой захвата мьютекса, что служит аналогом активной блокировки. Разблокировка мьютекса осуществляется сохранением в мьютексе значения 0 с помощью команды XCHG[28].

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

В архитектуре ARMПравить

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

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

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

См. такжеПравить

ПримечанияПравить

  1. Таненбаум, 2011, 2.3.6. Мьютексы, с. 165.
  2. Олег Цилюрик. Инструменты программирования в ядре: Часть 73. Параллелизм и синхронизация. Блокировки. Часть 1. — www.ibm.com, 2013. — 13 августа. — Дата обращения: 12.06.2019.
  3. The Open Group Base Specifications Issue 7, 2018 edition, Rationale for System Interfaces, General Information (англ.).
  4. C++17, 2017, 33.4.3.4 Shared mutex types, p. 1373.
  5. Таненбаум, 2011, 2.3.6. Мьютексы, с. 165—166.
  6. 1 2 Steven Rostedt, Alex Shi. RT-mutex implementation design — The Linux Kernel documentation (англ.). The Linux Kernel documentation. The kernel development community (7 June 2017). Дата обращения 16 июня 2020.
  7. Create Mutex. Дата обращения 20 декабря 2010. Архивировано 14 февраля 2012 года.
  8. 1 2 Michael Satran, Drew Batchelor. Critical Section Objects (англ.). Documentation. Microsoft (31 May 2018). Дата обращения 20 декабря 2010. Архивировано 14 февраля 2012 года.
  9. Michael Satran, Drew batchelor. Synchronization Functions - Win32 apps (англ.). Documentation. Microsoft (31 May 2018). Дата обращения 18 июня 2020. Архивировано 18 июня 2020 года.
  10. Таненбаум, 2011, 2.3.6. Мьютексы, Мьютексы в Pthreads, с. 167.
  11. 1 2 3 4 5 6 7 Таненбаум, 2011, 2.3.6. Мьютексы, Мьютексы в Pthreads, с. 168.
  12. IEEE, The Open Group. pthread_mutex_timedlock (англ.). pubs.opengroup.org. The Open Group (2018). Дата обращения 18 июня 2020. Архивировано 18 июня 2020 года.
  13. 1 2 IEEE, The Open Group. pthread_mutexattr_settype(3) (англ.). The Open Group Base Specifications Issue 7, 2018 edition. The Open Group (2018). Дата обращения 20 декабря 2010. Архивировано 14 февраля 2012 года.
  14. 1 2 3 IEEE, The Open Group. pthread_mutex_lock (англ.). The Open Group Base Specifications Issue 7, 2018 edition. The Open Group (2018). Дата обращения 17 июня 2020. Архивировано 17 июня 2020 года.
  15. 1 2 C17, 2017, 7.26 Threads <threads.h>, p. 274.
  16. C17, 2017, 7.26.4 Mutex functions, p. 277—279.
  17. C17, 2017, 7.26.4.2 The mtx_init function, p. 277—278.
  18. C17, 2017, 7.26.1 Introduction, p. 274.
  19. 1 2 C17, 2017, 7.26.1 Introduction, p. 275.
  20. C++17, 2017, 33.4.3.2 Mutex types, p. 1368.
  21. C++17, 2017, 33.4.3.2.1 Class mutex, p. 1369—1370.
  22. C++17, 2017, 33.4.3.2.2 Class recursive_mutex, p. 1370.
  23. C++17, 2017, 33.4.3.3 Timed mutex types, p. 1370—1371.
  24. C++17, 2017, 33.4.3.3.2 Class recursive_timed_mutex, p. 1372—1373.
  25. Synchronization mechanisms (англ.). Boost C++ Libraries 1.73.0. Дата обращения 18 июня 2020. Архивировано 18 июня 2020 года.
  26. Ulrich Drepper. Futexes Are Tricky : [арх. 24.06.2020] : [PDF]. — Red Hat, Inc., 2005. — 11 December.
  27. Karim Yaghmour, Jon Masters, Gilad Ben-Yossef, Philippe Gerum. Building Embedded Linux Systems: Concepts, Techniques, Tricks, and Traps. — "O'Reilly Media, Inc.", 2008. — С. 400. — 466 с. — ISBN 978-0-596-55505-4.
  28. Таненбаум, 2011, 2.3.6. Мьютексы, с. 166.
  29. Таненбаум, 2011, 2.3.6. Мьютексы, с. 166—167.
  30. ARM, 2009, 1.2.1 LDREX and STREX, p. 4.
  31. ARM, 2009, 1.2.2 Exclusive monitors, p. 5.
  32. ARM, 2009, 1.2.1 LDREX and STREX, LDREX, p. 4.
  33. ARM, 2009, 1.2.1 LDREX and STREX, STREX, p. 4.
  34. 1 2 ARM, 2009, 1.3.2 Implementing a mutex, p. 12—13.
  35. ARM, 2009, 1.2.3 Memory barriers, p. 8.

ЛитератураПравить