Введение
При написании многопоточных приложений почти всегда требуется работать с общими данными, одновременное изменение которых может привести к очень неприятным последствиям.
Для блокировки общих данных от одновременного доступа необходимо использовать объекты синхронизации.
В данном топике рассмотренна методика работы с мютексами, существенно уменьшающая количество потенциальных ошибок связанных с созданием/удалением и захватом/освобождением.
Неудаление мютекса приводит к утечке памяти, незахват — к некорректным данным, а неосвобождение — к блокировке всех функций, работающих с общими данными.
Ниже рассматривается работа с мютексами в 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++»