Начнем с мьютексов. Простейший враппер, часто встречающийся в исходном коде, выглядит следующим образом:
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 комментариев:
А не логичнее забросить конструктор копирования и оператор присваивания в 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 &);
};
Спасибо, интересно, жду продолжения в том же духе ;)
небольшой совет - прикрутите к блогу подсветку синтаксиса - http://softwaremaniacs.org/soft/highlight/
Да действительно, не логичнее забросить копирование и присваивание в private секцию? Ведь тогда их точно нельзя будет использовать
Требования к обьектам использующихся в STL-контейнерах по-моему и так знает любой мало-мальский грамотный программист. А вот зачем m_mutexAttr в обьекте хранить??? После использования в pthread_mutex_init аттрибуты можно смело закрывать
Действительно интересная информация, правда не понял как CLocker помогает в обработке исключений:-)
И ещё, может быть имеет смысл сделать CLocker дружественным CMutex, а CMutex::lock() и CMutex::unlock() объявить закрытыми членами класса? Чтобы запретить их вызов в обход использования CLocker?
пиши понятней сука
мы ни хуя не поняли бля казёл нах
Отправить комментарий