среда, 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(...). Но такая ошибка является уже очень серьезной и говорит о нарушении стратегии взаимодействия потоков.

7 комментариев:

Alex комментирует...

А не логичнее забросить конструктор копирования и оператор присваивания в private-секцию?

Вудруф комментирует...

Как вариант:
class MutexImpl;

//Mutex
//Can be unlocked only once without lock
//lock and unlock must be called in one thread
class Mutex
{
public:
Mutex ();
void lock ();
void unlock();
~Mutex() throw ();

private:
Pointer <MutexImpl> impl;
};

cpp-файл:
//Implementation of Mutex
class MutexImpl
{
public:
//Constructor
MutexImpl();
//Lock mutex
inline void lock();
//Unlock mutex
inline void unlock();
//Destructor
~MutexImpl() throw ();

private:
//Protect from copying
MutexImpl (const MutexImpl &);
const MutexImpl & operator = (const MutexImpl &);

//Mutex
pthread_mutex_t mutex;
};

Где Pointer определён как шаблон:
//Safe pointer
template <typename T>
class Pointer
{
private:
class InnerPointer;
InnerPointer * innerPtr;
public:
//Constructor
explicit Pointer (T * ptr) : innerPtr (new InnerPointer (ptr)) {}
//Copy constructor
Pointer (const Pointer & right) {right.innerPtr -> use(); innerPtr = right.innerPtr;}
//Assignment operator
const Pointer & operator = (const Pointer & right)
{
if (innerPtr -> free())
delete innerPtr;
right.innerPtr -> use();
innerPtr = right.innerPtr;
}
//Is equal?
bool operator == (const Pointer & right) const throw() {return innerPtr == right.innerPtr;}
//Operator ->
T * operator -> () const throw() {return innerPtr -> operator -> ();}
//Operator *
T & operator * () const throw() {return innerPtr -> operator * ();}
//Destructor
~Pointer() throw () {if (innerPtr -> free()) delete innerPtr;}
};

template <typename T>
class Pointer<T>::InnerPointer
{
public:
//Constructor
InnerPointer (T * inPtr) : ptr (inPtr), used (1) {}
//TODO Check for overflow in use(). Can anyone create more than 4294967295 references? :)
//Creating another copy of Pointer
void use() {++used;}
//Releasing one of references to T *
bool free() throw()
{
if (--used)
return false;
delete ptr;
return true;
}
//Operator ->
T * operator -> () const throw() {return ptr;}
//Operator *
T & operator * () const throw() {return * ptr;}
private:
T * ptr;
unsigned long int used;
//Protect from copying
InnerPointer (const InnerPointer &);
const InnerPointer & operator = (const InnerPointer &);
};

Unknown комментирует...

Спасибо, интересно, жду продолжения в том же духе ;)

небольшой совет - прикрутите к блогу подсветку синтаксиса - http://softwaremaniacs.org/soft/highlight/

bitomaxsp комментирует...

Да действительно, не логичнее забросить копирование и присваивание в private секцию? Ведь тогда их точно нельзя будет использовать

Анонимный комментирует...

Требования к обьектам использующихся в STL-контейнерах по-моему и так знает любой мало-мальский грамотный программист. А вот зачем m_mutexAttr в обьекте хранить??? После использования в pthread_mutex_init аттрибуты можно смело закрывать

Анонимный комментирует...

Действительно интересная информация, правда не понял как CLocker помогает в обработке исключений:-)
И ещё, может быть имеет смысл сделать CLocker дружественным CMutex, а CMutex::lock() и CMutex::unlock() объявить закрытыми членами класса? Чтобы запретить их вызов в обход использования CLocker?

Анонимный комментирует...

пиши понятней сука
мы ни хуя не поняли бля казёл нах