Pull to refresh

Многопоточность, общие данные и мьютексы

Reading time 5 min
Views 58K

Введение


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

Для блокировки общих данных от одновременного доступа необходимо использовать объекты синхронизации.

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

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

Ниже рассматривается работа с мютексами в 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++»
Tags:
Hubs:
+26
Comments 50
Comments Comments 50

Articles