вторник, 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, использовать враппер, вот и все :)