среда, 16 января 2008 г.

Mutex-ы и C++

Начнем с мьютексов. Простейший враппер, часто встречающийся в исходном коде, выглядит следующим образом:
class CMutex  {
public:
    CMutex( … some initializers…);
    ~CMutex();

    void lock();
    void unlock();

private:
    ::pthread_mutex_t m_mutex;
С первого взгляда, все выглядит отлично, но на самом деле это не так. Представим себе что объект содержащий этот мьютекс попадает, например, в std::map. Что интересно некоторое время все будет в порядке, до тех пор, пока map не наполнится до определенного размера и не реаллокирует свою память. При этом произойдет перемещение CMutex:m_mutex, и мьютекс после этого перестанет работать. Чаще всего это выглядит как deadlock, но эффекты могут быть совершенно разными. Как этого избежать? Есть два варианта. Первый – недопускать попадания CMutex в контейнеры. Но в данном случае мы полностью полагаемся на программиста использующего этот враппер. Лучше этого не делать. Второй вариант намного лучше. Нам нужно объявить в CMutex конструктор копирования и оператор присвоения, но не определять их тело. В результате код CMutex будет выглядеть так:
class CMutex
{
public:
 
    CMutex(int _mutexType = PTHREAD_MUTEX_FAST_NP);

    CMutex(const CMutex&);
    CMutex& operator=(const CMutex&);

    ~CMutex();

    void lock();
    void unlock();

private:
    ::pthread_mutex_t m_mutex;
    ::pthread_mutexattr_t m_mutexAttr;
};

inline 
CMutex::CMutex(int _mutexType)
{
    ::pthread_mutexattr_init(&m_mutexAttr);

    ::pthread_mutexattr_settype(&m_mutexAttr, _mutexType);
    ::pthread_mutex_init(&m_mutex, &m_mutexAttr);
}

inline
CMutex::~CMutex()
{
    ::pthread_mutexattr_destroy(&m_mutexAttr);
    ::pthread_mutex_destroy(&m_mutex);
}

inline
void CMutex::lock()
{
    switch(::pthread_mutex_lock(&m_mutex))
    {
        case EDEADLK:  throw Exception::Mutex::Deadlock();
        case EINVAL:  throw Exception::Mutex::Initialization();
    }
}

inline 
void CMutex::unlock()
{
    switch(::pthread_mutex_unlock(&m_mutex))
    {
        case EPERM:  throw Exception::Mutex::Permission();
        case EINVAL: throw Exception::Mutex::Initialization();
    }
}
и т.д., без тел для CMutex(const CMutex&) и operator=(const CMutex&). При компиляции, если CMutex каким либо образом попадет в контейнер, будет выдана ошибка компиляции. Стоит отметить, что такое решение рекомендуется применять для всех структур, копирование для которых запрещено. С одной проблемой мы разобрались. Продолжим дальше.В приведенном выше исходном коде при deadlock мьютекса (если он создавался с _mutexType = PTHREAD_MUTEX_ERRORCHECK_NP) будет выброшен некий exception Exception::Mutex::Deadlock. Насколько такой подход к обработке ошибок нам подходит? Представим себе следующую ситуацию. Есть некое дерево содержащие указатели на некие объекты. Мы можем запирать мьютексом все дерево и каждый его лист в отдельности. Т.е. можно использовать такой алгоритм - заперли дерево, нашли лист, заперли лист, отпустили дерево, поработали с листом, отпустили лист. Представим себе что в при вызове этого алгоритма мы получили exception Exception::Mutex::Deadlock или Exception::Mutex::Permission. Как нам корректно обработать эту ситуацию, что бы наше приложение продолжило работать? Единственное толковое решение, которое спасет нас в некоторых случаях, это паттерн MutexLocker. Реализовать его можно следующим образом:
class CMutex {
...
class CLocker
{
public:
    CLocker(CMutex &_mutex) : m_mutex(_mutex) { m_mute.lock(); }
    ~CLocker(){ m_mutex.unlock(); }

private:
    CMutex &m_mutex;
};
...
};
Используется он таким образом:
...
{
CMutex::CLocker lock(somemutex);
... /* do some job */
};
...
Таким образом ответственность за запирание мьютекса ложится на стек. При создании CMutex::CLocker мьютекс запирается, а при его уничтожении отпирается. Преимущество этого решения в том что, всегда будет вызван CMutex.unlock() и программисту не придется следить за этим самому. В тоже время это не дает гарантии что программист не будет использовать lock() на уже запретом CLocker мьютексе. Конечно данное решение не поможет нам в том случае если мы получаем EPERM при вызове ::pthread_mutex_unlock(...). Но такая ошибка является уже очень серьезной и говорит о нарушении стратегии взаимодействия потоков.