вторник, 26 августа 2008 г.

Синхронизация

Столкнулся недавно с интересной проблемой. Пускай у нас есть класс, который с некоторой вероятностью будет использоваться несколькими потоками одновременно, поэтому он должен содержать блокировки. Это можно организовать, к примеру с помощью вот такого кода:
class critical_section
{
    CRITICAL_SECTION cs_;
public:
    critical_section()
    {
        InitializeCriticalSection(&cs_);
    }
    ~critical_section()
    {
        DeleteCriticalSection(&cs_);
    }
    void enter()
    {
        EnterCriticalSection(&cs_);
    }
    void leave()
    {
        LeaveCriticalSection(&cs_);
    }
};

template< 
    class TLock, 
    void (TLock::*en)() = &TLock::enter, 
    void (TLock::*le)() = &TLock::leave
>
class scoped_lock
{
    TLock& lock_;
public:
    scoped_lock(TLock& lock) : lock_(lock) 
    { 
        (lock_.*en)(); 
    }
    ~scoped_lock() 
    { 
        (lock_.*le)(); 
    }
};

typedef scoped_lock<critical_section> slock;
Класс critical_section - критическая секция. Класс scoped_lock - реализует принцип RAII, для произвольного класса. Указатели на методы вынесены в параметры шаблона для того, что-бы можно было работать с более сложными объектами синхронизации, например реализующими read-write lock. Используется это очень просто:
critical_section lock_;

void foo ()
{
    slock lockit(lock_);
    //этот участок кода
    //может выполняться только
    //одним потоком
}
в общем, эта идиома широко известна... Далее мне нужно просто использовать этот прием во всех не константных методах класса. Если так сделать, то объект класса сможет выступать в качестве общего ресурса для нескольких потоков. Но здесь есть один существенный минус. Это сделает вызовы методов нашего класса более дорогими, даже, если объект будет использоваться только одним потоком. Что-бы этого избежать, нужно иметь возможность отключать синхронизацию.

Самый простой способ этого добиться - использовать препроцессор, гибкость нулевая, зато работает =) Более правильный способ - не хардкодить critical_section в качестве объекта синхронизации для нашего класса, а передавать его в качестве шаблонного параметра. Если нужно отключить блокировку, достаточно передать в шаблон класс - заглушку, с пустыми методами enter и leave. По сравнению с первым способом это очень гибко, можно в одном месте программы использовать потокобезопасную версию объекта, а в другой небезопасную, но более быструю. Но здесь то-же есть минус. Клиент нашего класса должен знать о том, что должен из себя представлять класс critical_section, это не очень хорошо... Оптимальный вариант с точки зрения чистоты и ясности кода - передавать в шаблон логическое значение. Что-то вроде этого:

template <
    bool enable_lock = false
>
class some_class...
если передали true, то использовать потокобезопасную версию объекта, если false - обычную. Теперь о том как это реализовать:
template<class TLock, class TScopedLock, bool enabled>
struct switch_lock;

template<class TLock, class TScopedLock>
struct switch_lock<TLock, TScopedLock, true>
{
    TScopedLock lock_it_;
    switch_lock(TLock& lock) : lock_it_(lock) {}

};

template<class TLock, class TScopedLock>
struct switch_lock<TLock, TScopedLock, false>
{
    switch_lock(TLock& lock) {}

};
Выглядит немного пугающе, но это всего-лишь враппер для scoped_lock, в качестве первого параметра он получает класс синхронизации, например critical_section, второй параметр - RAII враппер для первого параметра, например slock, ну и последний параметр отвечает за выбор реализации. Сам класс будет выглядеть так:
template <
    bool enable_lock = false
>
class some_class
{
    typedef switch_lock<critical_section, slock, enable_lock> locker;
    critical_section lock_;
    
public:

    some_class() 
    {
        std::cout << "ctor" << std::endl;
    }
    
    template<bool el>
    some_class(const some_class<el>& el)
    {
        std::cout << "copy ctor" << std::endl;
    }
    
    template<bool el>
    some_class& operator = (const some_class<el>& el)
    {
        std::cout << "assign" << std::endl;
        return *this;
    }
    
    void some_function()
    {
        locker l(lock_);//лочим (захватываем мьютэкс, входим в критическую секцию, итд)
        std::cout << "function called" << std::endl;
    }
};
весь трюк состоит в том, что-бы вместо scoped_lock, использовать враппер, вот и все :)

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

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

Прикольно конечно... но помоему слишком надумано. За мою, впрочем небольшую, карьеру программистом, приходилось только лочить функции\методы, так, что первого варианта просто за глаза. И потом пугать молодежь :)

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

ещё можно перегружать по volatile, как описанно в статье http://www.ddj.com/cpp/184403766

И кстати стоимость локания CriticalSection когда нет нужды переходить в режим ожидания очень мала, почти как стоимость проверки булёвого флага -- и это одна из ключевых разниц между секцией и мутексом.

Sergey Miryanov комментирует...

"Оптимальный вариант с точки зрения чистоты и ясности кода - передавать в шаблон логическое значение."

В случае когда классов очень много, то такое решение будет обладать не самой лучшей чистотой кода.

К тому же, надо либо код полностью писать в классе, либо в делать явные инстанциации.

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

Честно говоря, мне сейчас самому эта идея кажется не очень удачной =)

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

"Честно говоря, мне сейчас самому эта идея кажется не очень удачной =)"
несколько раз использовал вашу реализацию, мне она кажется весьма не плохА!
если же вы считаете что эту задачу можно реализовать лучше, прокомментируйте пожалуйста.


Ваш постоянный читатель.

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

"Честно говоря, мне сейчас самому эта идея кажется не очень удачной =)"
несколько раз использовал вашу реализацию, мне она кажется весьма не плохА!
если же вы считаете что эту задачу можно реализовать лучше, прокомментируйте пожалуйста.


Ваш постоянный читатель.

Sergey Miryanov комментирует...

"Оптимальный вариант с точки зрения чистоты и ясности кода - передавать в шаблон логическое значение."

В случае когда классов очень много, то такое решение будет обладать не самой лучшей чистотой кода.

К тому же, надо либо код полностью писать в классе, либо в делать явные инстанциации.