Каким свойством в общем случае обладают мьютексы
Изображение 1: Попытка одновременного удаления двух узлов i и i + 1 приводит к сохранению узла i + 1.
Мью́текс (англ. mutex, от mutual exclusion — «взаимное исключение») — аналог одноместного семафора, служащий в программировании для синхронизации одновременно выполняющихся потоков.
Мьютекс отличается от семафора тем, что только владеющий им поток может его освободить, т.е. перевести в отмеченное состояние.
Мьютексы — это один из вариантов семафорных механизмов для организации взаимного исключения. Они реализованы во многих ОС, их основное назначение — организация взаимного исключения для потоков из одного и того же или из разных процессов.
Мьютексы — это простейшие двоичные семафоры, которые могут находиться в одном из двух состояний — отмеченном или неотмеченном (открыт и закрыт соответственно). Когда какой-либо поток, принадлежащий любому процессу, становится владельцем объекта mutex, последний переводится в неотмеченное состояние. Если задача освобождает мьютекс, его состояние становится отмеченным.
Задача мьютекса — защита объекта от доступа к нему других потоков, отличных от того, который завладел мьютексом. В каждый конкретный момент только один поток может владеть объектом, защищённым мьютексом. Если другому потоку будет нужен доступ к переменной, защищённой мьютексом, то этот поток блокируется до тех пор, пока мьютекс не будет освобождён.
Цель использования мьютексов — защита данных от повреждения в результате асинхронных изменений (состояние гонки), однако могут порождаться другие проблемы — например взаимная блокировка (клинч).
Мьютексы в Win32 API[править | править код]
Win32 API в Windows имеет две реализации мьютексов — собственно мьютексы[1], имеющие имена и доступные для использования между разными процессами, и критические секции[2], которые могут использоваться только в пределах одного процесса. Для каждого из этих двух типов мьютексов используются свои функции захвата и освобождения.
Критическая секция в Windows по возможности блокируется без использования вызова режима ядра (аналогично спинлоку), но при невозможности такой блокировки поток запрашивает ядро.
Мьютексы в Unix-подобных системах[править | править код]
Мьютекс в стандартной библиотеке Pthreads может использоваться в одном процессе или в разных, но в любом случае всем использующим процессам требуется доступ к памяти, в которой он размещён. Такой мьютекс может иметь один из следующих типов[3]:
- PTHREAD_MUTEX_NORMAL — нет контроля повторного захвата тем же потоком (англ. thread);
- PTHREAD_MUTEX_RECURSIVE — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов;
- PTHREAD_MUTEX_ERRORCHECK — повторные захваты тем же потоком вызывают немедленную ошибку.
Мьютексы в языке Си[править | править код]
Последний стандарт языка Си (ISO/IEC 9899:2011[4]) определяет тип mtx_t и функции для работы с ним, которые должны быть доступны, если макрос __STDC_NO_THREADS__ не был определён компилятором. Семантика и свойства мьютексов в целом совпадают со стандартом POSIX:
- mtx_plain — нет контроля повторного захвата тем же потоком;
- mtx_recursive — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов;
- mtx_timed — поддерживается захват мьютекса с тайм-аутом (следует отметить, что, в отличие от стандарта POSIX, поддержка этого свойства мьютекса не является опциональной).
Возможность использования мьютексов в разделяемой памяти различных процессов в стандарте Си11 не рассматривается.
Мьютексы в языке C++[править | править код]
Стандарт языка C++ (ISO/IEC 14882:2011[5]) определяет различные классы мьютексов:
- mutex — нет контроля повторного захвата тем же потоком;
- recursive_mutex — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов;
- timed_mutex — нет контроля повторного захвата тем же потоком, поддерживается захват мьютекса с тайм-аутом;
- recursive_timed_mutex — повторные захваты тем же потоком допустимы, ведётся счётчик таких захватов, поддерживается захват мьютекса с тайм-аутом.
Следует отметить библиотеку Boost, которая обеспечивает:
- реализацию мьютексов, совместимых по интерфейсу со стандартом C++11 для компиляторов и платформ, которые не поддерживают этот стандарт;
- реализацию дополнительных классов мьютексов: shared_mutex и др., которые позволяют захватывать мьютекс для совместного владения несколькими потоками только для чтения данных.
Примечания[править | править код]
См. также[править | править код]
- Многопоточность
- Семафор
- Фьютекс — быстрый пользовательский мьютекс
- Спинлок
Введение
При написании многопоточных приложений почти всегда требуется работать с общими данными, одновременное изменение которых может привести к очень неприятным последствиям.
Для блокировки общих данных от одновременного доступа необходимо использовать объекты синхронизации.
В данном топике рассмотренна методика работы с мютексами, существенно уменьшающая количество потенциальных ошибок связанных с созданием/удалением и захватом/освобождением.
Неудаление мютекса приводит к утечке памяти, незахват — к некорректным данным, а неосвобождение — к блокировке всех функций, работающих с общими данными.
Ниже рассматривается работа с мютексами в Windows и Unix, подобная идея может быть использована при работе с другими объектами синхронизации.
Эта идея является частным случаем методики «Выделение ресурса — есть инициализация (RAII)».
Создание, настройка и удаление мютекса
Для начала объявим класс CAutoMutex, который создает мютекс в конструкторе и удаляет в деструторе.
Плюсы:
— не нужно плодить по всему проекту похожие фрагменты коды инициализации, настройки и удаления мютекса
— автоматическое удаление мютекса и освобождение ресурсов, занятых им
// класс-оболочка, создающий и удаляющий мютекс (Windows)
class CAutoMutex
{
// дескриптор создаваемого мютекса
HANDLE m_h_mutex;// запрет копирования
CAutoMutex(const CAutoMutex&);
CAutoMutex& operator=(const CAutoMutex&);
public:
CAutoMutex()
{
m_h_mutex = CreateMutex(NULL, FALSE, NULL);
assert(m_h_mutex);
}
~CAutoMutex() { CloseHandle(m_h_mutex); }
HANDLE get() { return m_h_mutex; }
};* This source code was highlighted with Source Code Highlighter.
В Windows мютексы по умолчанию рекурсивные, а в Unix — нет. Если мютекс не является рекурсивным, то попытка захватить его два раза в одном потоке приведет к deadlock-у.
Чтобы в Unix создать рекурсивный мютекс, необходимо установить соответствующий флаг при инициализации. Соответствующий класс CAutoMutex выглядел бы так (проверки возвращаемых значений не показаны для компактности):
// класс-оболочка, создающий и удаляющий рекурсивный мютекс (Unix)
class CAutoMutex
{
pthread_mutex_t m_mutex;CAutoMutex(
const CAutoMutex&);
CAutoMutex& operator=(const CAutoMutex&);public:
CAutoMutex()
{
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&m_mutex, &attr);
pthread_mutexattr_destroy(&attr);
}
~CAutoMutex()
{
pthread_mutex_destroy(&m_mutex);
}
pthread_mutex_t& get()
{
return m_mutex;
}
};* This source code was highlighted with Source Code Highlighter.
Захват и освобождение мютекса
По аналогии с предыдущим классом объявим класс CMutexLock, который занимает мютекс в конструкторе и освобождает в деструкторе. Созданный объект этого класса автоматически захватит мютекс и освободит его в конце области действия независимо от того, какой именно был выход из этой области: нормальный выход, преждевременный return или выброс исключения. Плюсом также является, что можно не плодить похожие фрагменты кода работы с мютексами.
// класс-оболочка, занимающий и освобождающий мютекс
class CMutexLock
{
HANDLE m_mutex;// запрещаем копирование
CMutexLock(const CMutexLock&);
CMutexLock& operator=(const CMutexLock&);
public:
// занимаем мютекс при конструировании объекта
CMutexLock(HANDLE mutex): m_mutex(mutex)
{
const DWORD res = WaitForSingleObject(m_mutex, INFINITE);
assert(res == WAIT_OBJECT_0);
}
// освобождаем мютекс при удалении объекта
~CMutexLock()
{
const BOOL res = ReleaseMutex(m_mutex);
assert(res);
}
};* This source code was highlighted with Source Code Highlighter.
Для еще большего удобства объявим следующий макрос:
// макрос, занимающий мютекс до конца области действия
#define SCOPE_LOCK_MUTEX(hMutex) CMutexLock _tmp_mtx_capt(hMutex);* This source code was highlighted with Source Code Highlighter.
Макрос позволяет не держать в голове имя класса CMutexLock и его пространство имен, а также не ломать голову каждый раз над названием создаваемого (например _tmp_mtx_capt) объекта.
Примеры использования
Рассмотрим примеры использования.
Для упрощения примера объявим мютекс и общие данные в глобальной области:
// автоматически создаваемый и удаляемый мютекс
static CAutoMutex g_mutex;// общие данные
static DWORD g_common_cnt = 0;
static DWORD g_common_cnt_ex = 0;* This source code was highlighted with Source Code Highlighter.
Пример простой функции, использующей общие данные и макрос SCOPE_LOCK_MUTEX:
void do_sth_1( ) throw()
{
// …
// мютекс не занят
// …{
// занимаем мютекс
SCOPE_LOCK_MUTEX(g_mutex.get());// изменяем общие данные
g_common_cnt_ex = 0;
g_common_cnt = 0;
// здесь мютекс освобождается
}
// …
// мютекс не занят
// …
}* This source code was highlighted with Source Code Highlighter.
Не правда ли, что функция do_sth_1() выглядит элегантнее, чем следующая? do_sth_1_eq:
void do_sth_1_eq( ) throw()
{
// занимаем мютекс
if (WaitForSingleObject(g_mutex.get(), INFINITE) == WAIT_OBJECT_0)
{
// изменяем общие данные
g_common_cnt_ex = 0;
g_common_cnt = 0;// надо не забыть освободить мютекс
ReleaseMutex(g_mutex.get());
}
else
{
assert(0);
}
}* This source code was highlighted with Source Code Highlighter.
В следующем примере точек выхода из функции три, но упоминание о мютексе только одно (объявление области блокировки мютекса):
// какое-то исключение
struct Ex {};// фунцкция, использующая общие данные
int do_sth_2( const int data ) throw (Ex)
{
// …
// мютекс не занят
// …
// занимаем мютекс на критическом участке
SCOPE_LOCK_MUTEX(g_mutex.get());
int rem = data % 3;
if (rem == 1)
{
g_common_cnt_ex++;
// мютекс автоматически освободится при выбросе исключения
throw Ex();
}
else if (rem == 2)
{
// мютекс автоматически освободится при возврате
g_common_cnt++;
return 1;
}
// здесь мютекс автоматически освободится при возврате
return 0;
}* This source code was highlighted with Source Code Highlighter.
Примечание: я не сторонник использовать несколько return-ов в одной функции, просто пример от этого
становится чуть показательнее.
А если бы функция была длиннее и точек выброса исключений было бы с десяток? Без макроса нужно было поставить перед каждой из них ReleaseMutex(…), а ошибиться здесь можно очень легко.
Заключение
Приведенные примеры классов и макросов достаточно просты, они не содержат сложных проверок и ожидают освобождения мютекса в течение бесконечного времени. Но даже это облегчает жизнь во многих случаях. А если облегчает жизнь, то почему бы это не использовать?
UPD: Первый класс CAutoMutex по ошибке не был написан, вместо него было повторное объявление второго класса CMutexLock. Исправлено.
UPD2: Убраны слова inline в объявлении методов внутри классов за ненадобностью.
UPD3: Был добавлен вариант класса CAutoMutex с рекурсивным мютексом для Unix.
UPD4: Перенесено в блог «C++»
Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 11:24, 29 октября 2016.
Мью́текс (англ. mutex, от англ. mutual exclusion — «взаимное исключение») — является аналогом одноместного семафора, в программировании необходим для сопоставления синхронно выполняющихся потоков.[1].
Мью́текс представляет собой концепцию программирования, которая используется для решения вопросов многопоточности.
Мьютекс отличается от семафора тем, что допускает только один поток в контролируемом участке, заставляя другие потоки, которые пытаются получить доступ к этому разделу ждать, пока первый поток не вышел из этого раздела.
Принимает два значенения:
- открыт — поток может войти в свою критическую секцию;
- закрыт — поток не может войти в критическую секцию.
Задача мьютекса — защита объекта от доступа к нему других потоков, отличных от того, который завладел мьютексом. В каждый конкретный момент только один поток может владеть объектом, защищённым мьютексом. Если другому потоку будет нужен доступ к переменной, защищённой мьютексом, то этот поток засыпает до тех пор, пока мьютекс не будет освобождён.
Цель использования мьютексов — защита данных от повреждения в результате асинхронных изменений (состояние гонки), однако могут порождаться другие проблемы — такие, как взаимная блокировка (клинч).
Для описания мьютекса требуется всего один бит, хотя чаще используется целая переменная, у которой 0 означает не блокированное состояние, а все остальные значения соответствуют блокированному состоянию. Значение мьютекса устанавливается двумя процедурами. Если поток собирается войти в критическую область, он вызывает процедуру mutex_lock. Если мьютекс не заблокирован, запрос выполняется и вызывающий поток может попасть в критическую область[2]. Если mutex закрыт, то поток пытающийся войти в критическую секцию блокируется.
Мьютекс в операционных системах
Использование mutex в Windows
- Создание mutex
CreateMutex (параметры_безопасности, собственность_потока, имя)
Поименный mutex может использоваться для синхронизации процессов - Открытие mutex
OpenMutex (параметры_безопасности, собственность_потока, имя) - Ожидание и захват mutex
WaitForSingleObject (дескриптор_mutex, время_ожидания) - Освобождение mutex
ReleaseMutex (дескриптор_mutex)
Использование mutex в Linux
Mutex в Linux — это объект для синхронизации потоков в рамках одного процесса
- Описание mutex
pthread_mutex_t_mutex; - Инициализация mutex
pthread_mutex_init (&mutex, NULL); - Перед входом в критическую секцию поток должен попытаться захватить mutex. Если это не удается, то поток блокируется до освобождения mutex
pthread_mutex_lock (&mutex); - При выходе из критической секции поток должен освободить mutex
pthread_mutex_unlock (&mutex);
Имя | Описание |
---|---|
Close() | Освобождает все ресурсы, удерживаемые текущим объектом WaitHandle.(Наследуется от WaitHandle.) |
CreateObjRef(Type) | Создает объект, который содержит всю необходимую информацию для создания прокси-сервера, используемого для взаимодействия с удаленным объектом.(Наследуется от MarshalByRefObject.) |
Dispose() | Освобождает все ресурсы, используемые текущим экземпляром класса WaitHandle.(Наследуется от WaitHandle.) |
Equals(Object) | Определяет, равен ли заданный объект текущему объекту.(Наследуется от Object.) |
GetAccessControl() | Получает объект MutexSecurity, представляющий безопасность управления доступом для именованного мьютекса. |
GetHashCode() | Играет роль хэш-функции для определённого типа. (Наследуется от Object.) |
GetLifetimeService() | Извлекает объект обслуживания во время существования, который управляет политикой времени существования данного экземпляра.(Наследуется от MarshalByRefObject.) |
GetType() | Возвращает объект класса Type для текущего экземпляра. (Наследуется от Object.) |
InitializeLifetimeService() | Возвращает объект обслуживания во время существования для управления политикой времени существования данного экземпляра.(Наследуется от MarshalByRefObject.) |
OpenExisting(String) | Открывает указанный именованный мьютекс, если он уже существует. |
OpenExisting(String, MutexRights) | Открывает указанный именованный мьютекс, если он уже существует, с требуемыми правами доступа. |
ReleaseMutex() | Освобождает объект Mutex один раз. |
SetAccessControl(MutexSecurity) | Задает безопасность управления доступом для именованного системного мьютекса. |
ToString() | Возвращает строковое представление текущего объекта. (Наследуется от Object.) |
TryOpenExisting(String, Mutex) | Открывает указанный именованный мьютекс, если он уже существует, и возвращает значение, указывающее, успешно ли выполнена операция. |
TryOpenExisting(String, MutexRights, Mutex) | Открывает указанный именованный мьютекс, если он уже существует, с требуемыми правами доступа, и возвращает значение, указывающее, успешно ли выполнена операция. |
WaitOne() | Блокирует текущий поток до получения сигнала объектом WaitHandle.(Наследуется от WaitHandle.) |
WaitOne(Int32) | Блокирует текущий поток до получения текущим дескриптором WaitHandle сигнала, используя 32-разрядное целое число со знаком для указания интервала времени в миллисекундах.(Наследуется от WaitHandle.) |
WaitOne(Int32, Boolean) | Блокирует текущий поток до получения сигнала текущим объектом WaitHandle, используя 32-разрядное целое число со знаком для задания периода времени и указывая, следует ли выйти из домена синхронизации до начала ожидания.(Наследуется от WaitHandle.) |
WaitOne(TimeSpan) | Блокирует текущий поток до получения сигнала текущим экземпляром, используя значение типа TimeSpan для указания интервала времени.(Наследуется от WaitHandle.) |
WaitOne(TimeSpan, Boolean) | Блокирует текущий поток до получения сигнала текущим экземпляром, используя значение типа TimeSpan для задания интервала времени и указывая, следует ли выйти из домена синхронизации до начала ожидания.(Наследуется от WaitHandle.) |
Свойства
- Запоминание владельца — освободить мьютекс может только поток, который его захватил;
- Повторяющийся характер — поток может неоднократно захватить мьютекс (вызвать aquire()); для освобождения мьютекса поток должен соответствующее число раз захватить release();
- Наследование приоритета — поток, который захватил мьютекс, временно получает максимальный из приоритетных потоков, которые ждут освобождения этого мьютекса.
Структура управления мьютексом
Каждый мьютекс ассоциируется со структурой управления:
{
CDLL_QUEUE_S wait_queue;
CDLL_QUEUE_S mutex_queue;
CDLL_QUEUE_S lock_mutex_queue;
TN_UWORD attr;
TN_TCB_S * holder;
TN_UWORD ceil_priority;
TN_WORD cnt;
TN_OBJ_ID id_mutex;
} TN_MUTEX_S;
wait_queue | Очередь задач, ожидающих освобождение мьютекса |
mutex_queue | Элемент списка заблокированных задачей мьютексов |
ock_mutex_queue | Системная очередь заблокированных мьютексов |
attr | Атрибут (тип обхода инверсии приоритетов) мьютекса |
holder | Указатель на TCB(Task Control Block) задачи, блокирующей мьютекс |
ceil_priority | Максимальный приоритет из задач, которые могут использовать ресурс (требуется для протокола увеличения приоритета) |
cnt | Зарезервировано |
id_mutex | Поле идентификации объекта как мьютекса |
Структура мьютекса доступна только при определении TN_DEBUG. Тем не менее, прямой доступ к элементам структуры мьютекса крайне не рекомендуется, так как это является вмешательством в работу планировщика и других сервисов RTOS.[4]
Синхронизация файловых операций Mutex
Задача сводится к тому как заставить две программы работать с одним файлом, когда одна программа может писать, а вторая должна читать. Задача как избежать конфликтов при данной ситуации. Создадим два проекта, как Win32 console, один с именем WriteData, а другой с именем ReadData в каталоге TestMutex. Так будет и в прилагаемом проекте.
Код WriteFile:
//
#include «stdafx.h»
#include «windows.h»
#include «fstream.h»
#include «iostream.h»
void main()
{
HANDLE hShared = CreateMutex(NULL, TRUE, «WriteData»);
ofstream ofs(«d:\write.txt»);
ofs << «TestDataWrite» << endl;
ofs.close();
int i;
cout << «Press Key and Enter for access to file » << endl;
cin >> i;
ReleaseMutex(hShared);
CloseHandle(hShared);
}
Основа программы функция CreateMutex:
LPSECURITY_ATTRIBUTES, // атрибуты
BOOL bInitialOwner, // инициализация
LPCTSTR lpName // имя объекта
);
Теперь весь этот механизм запускается с помощью ReleaseMutex после использования указатель HANDLE нужно закрыть CloseHandle.
Код ReadData:
//
#include «stdafx.h»
#include «windows.h»
#include «fstream.h»
#include «iostream.h»
void main()
{
HANDLE hShared = OpenMutex(MUTEX_ALL_ACCESS, FALSE, «WriteData»);
cout << «Wait !!!! Write File Data Proccess » << endl;
WaitForSingleObject(hShared, INFINITE);
ifstream ifs(«d:\write.txt»);
char buffer[100];
ifs >> buffer;
cout << «Read File Data — » << buffer << endl;
CloseHandle(hShared);
}
Для доступа к Mutex теперь его нужно открыть с тем же именем. И поставить флаг MUTEX_ALL_ACCESS. Функция WaitForSingleObject будет стоять пока доступ не будет получен.
Запустите одновременно две программы. Программа чтения будет ждать пока программа записи не разрешит доступ. Вот примерно так как на экране.
Введите букву в программу записи и нажмите Enter, и программа чтения тут же опомнится и прочитает данные. Вот оно — понятие синхронизации. Кстати, данный механизм можно применять не только для записи/чтения файлов, но и для любой синхронизации между потоками, программами или еще чем.
Разница между мьютексом и Семафор (Операционные Системы)
Семафор работает в режиме пользователя и блокирует задачи.
Мьютекс работает в режиме ядра и блокирует сам ресурс.
На время выполнения мьютекса, задача, которая захватила мьютекс получает максимальный приоритет, соответственно, если задача выполняет какие-либо действия внутри семафора, то более высокоприоритетная задача может прервать его выполнение, если задача захватила мьютекс, то прекратить выполнение пока она освободит мьютекс, не может никакая другая задача.