模版和多态策略化加锁模式

模版和多态策略化加锁模式

关键词:策略化模式 模版策略化 多态策略化 策略化加锁模式 ACE BOOST C++ 设计模式

在ACE和BOOST的实现中都有大量的策略化加锁(Strategized Locking)的模式,这种模式能比较方便的让你的类兼容加锁和不加锁的两种情况。ACE大师Douglas C. Schmidt有一片专门的论文Strategized Locking》对此做了介绍,国人Thzhang也对此问做过翻译《ACE策略化的加锁模式》。

本文的目的是介绍这种模式的两种实现方式模版参数和多态方式以外,同时介绍两者的优缺点。

模版参数(parameterized)策略化加锁

如果一个监控组件,同时要在多线程和单线程环境下使用,而且其使用的平率非常高,多线程下必须加锁,但如果因此而影响了单线程下的组件性能,是不合算的,但如果为每一种环境写一套代码显然不利于代码的重用。那么有没有方法兼容两种模式呢,这就是策略化加锁的模式。

策略化加锁的方法一般采用模版(ACE和BOOST都试用过类似方式)的方式完成中这个功能,这样带来的成本都在编译器,几乎没有什么性能影响。如下代码:

//ZEN_Null_Mutex是一种用于单线程下的策略锁,
class ZEN_Null_Mutex
{

public:

    //锁定,其实什么也没有做
    void lock()
    {
    }
    //解锁,其实什么也没有做
    void unlock()
    {
    }
}

//ZEN_Thread_Mutex是一种用于多线程下的策略锁,
class ZEN_Thread_Mutex
{
protected:
    //线程锁
    pthread_mutex_t  lock_;
public:

    //锁定, ZEN_OS是为为了跨平台封装的函数,这儿不展开,大家可以认为他就是mutex
    void lock()
    {
        ZEN_OS::pthread_mutex_lock(&lock_);
    }
    //解锁, 
    void unlock()
    {
        ZEN_OS::pthread_mutex_unlock(&lock_);
    }
}

//ZEN_Server_Status_T是一个要在多线程或者单线程情况下试用的模版
//模版参数_ZEN_LOCK 决定锁的行为是多线程还是单线程
template <class _ZEN_LOCK>
class ZEN_Server_Status_T
{

protected:
    //策略化的锁,根据模版参数决定行为
    _ZEN_LOCK            stat_lock_;

public:

    //增加某项监控值,如果是在多线程下试用要避免冲突
    void increase_ byidx(size_t index, int64_t incre)
    {
        stat_lock_.lock();
        (stat_sandy_begin_ + index)->counter_ += incre;
        stat_lock_.unlock();
    }
    ……
}

上面的代码template <class _ZEN_LOCK> class ZEN_Server_Status_T 就是带有策略锁功能的监控类,其中的模版参数_ZEN_LOCK 可以是ZEN_Null_Mutex 和 ZEN_Thread_Mutex,在不同的环境下我们可以试用不同的模版参数试用ZEN_Server_Status_T监控组件。这样大家皆大欢喜,

在多线程环境下,你如下使用可以得到同步安全保护,

ZEN_Server_Status_T<ZEN_Thread_Mutex >::instance()->increase_byidx 

而在单线程环境下你如下使用,你可以得到最快的性能。

ZEN_Server_Status_T<ZEN_NULL_Mutex >::instance()->increase_byidx

而且你在这种模式下可以更容易的改变内部行为,比如你实现一个读写锁的

这种模式最大的好处讲一个类的是否需要加锁行为的决定时间放到了编译期,给一段代码带来了不同的行为,而且性能代价最低。而且对于C++程序员,试用模版策略化的是一种很酷的表现,他让你的代码看上去更加上流(不要小瞧这种因素的影响力)。

模版策略化的不足

其实如果你仔细读完Douglas C. Schmidt的论文《Strategized Locking》(可惜当年阅读的时候能力有限,没有体会),你会发现这样一段。

There are two ways to strategize locking mechanisms: polymorphism and parameterized types . In general, parameterized types should be used when the locking strategy is known at compile-time. Likewise,polymorphism should be used when the locking strategy is not known until run-time. As usual, the trade-off is between the efficient run-time performance of parameterized types versus the potential for run-time extensibility with polymorphism.

翻译:有两种方式实现策略化锁模式,多态或者模板参数化,一般模板参数化用于在编译时决定锁行为,多态是在运行时决定锁策略。通常权衡使用模版参数化方法还是多态方法的因素是运行时的性能和运行时的多态扩展性。

最近在自己写的代码中对上面这句话有了一些新认识,由于代码的层级关系,我必须继承监控的的类,于是,而同时由于我要保证锁的效果,我仍然必须使用模版的锁策略。

//模版模式在继承的时候仍然必须试用模版
template <class _ZEN_LOCK>
class Comm_Server_Status :public ZEN_Server_Status< _ZEN_LOCK >

这还不是最让人痛苦的,由于监控的点很多,我必须在基础库(注意是库,而不是实现)的代码也加入一些监控操作。此时你就发现如果你继续保持模版的锁策略,你必须改写很多代码。

//如果类ComponentA 要使用ZEN_Server_Status,
//同时Component_A本身也是一个组件,也是提供给其他人使用的,也无法在现在决定是否使用什么锁策略
//那么你也只能也使用使用模版的策略锁模式了
template <class _ZEN_LOCK>
class ComponentA
{

    int fun_a1
    {
        //如果你要在函数fun_a1中间使用监控数据
        ZEN_Server_Status<_ZEN_LOCK >::instance()->increase_byidx
            ……
    }
}

//如果你要在B组件中也用ZEN_Server_Status,也必须使用策略锁
template <class _ZEN_LOCK>
class ComponentA
{
}

如果你使用希望使用监控的地方很多,那么你的大量代码就必须改成使用模版的方式。而且都必须使用策略锁的方式。虽然我也不认为这完全不可行,但这的确有将问题扩大化的嫌疑。特别是当你的ComponentA可能本身完全不需要考虑线程同步需求的时候。

观察ACE的代码,ACE的代码大部分就是使用模版化的锁策略,但这的确也给ACE的很多代码带来了痛苦(搜索ACE_LOCK就知道了),ACE的所有定时器其实都使用了模版化的策略锁模式,而为了代码的方便,ACE往往在线程锁作为模版参数的类上继承使用,比如ACE_Reactor默认就是加锁的实现,所以本质上这样并没有带来更好的性能。

//在多线程模式下
//ACE_Reactor
//实际是
//ACE_Select_Reactor_T<ACE_Reactor_Token_T<ACE_Token> >::handle_events(ACE_Time_Value & max_wait_time) 

多态(polymorphism)策略化

那么如果采用多态策略化是否可以在某种程度上避免上述的问题?是的。多态决定行为是在运行时,而不是编译时,虽然有少量的性能损失,但将决定行为时间的后移,也让代码在在某种程度获得了更多的灵活性。

下面就是简单的展现一段利用多态实现策略化锁。

//利用多态的方式,实现策略,ZEN_Lock_Base作为一个基类,
class ZEN_Lock_Base
{

public:

    //锁定,其实什么也没有做,但同时也是一个虚函数,
    visual void lock()
    {
    }
    //解锁,其实什么也没有做,但同时也是一个虚函数,
    visual void unlock()
    {
    }
}
typedef ZEN_Lock_Base ZEN_Null_Mutex;

//ZEN_Thread_Mutex是一种用于多线程下的策略锁,
class ZEN_Thread_Mutex :public ZEN_Lock_Base
{
protected:
    //线程锁
    pthread_mutex_t  lock_;
public:

    //锁定, ZEN_OS是为为了跨平台封装的函数,这儿不展开,大家可以认为他就是mutex
    visual void lock()
    {
        ZEN_OS::pthread_mutex_lock(&lock_);
    }
    //解锁, 
    visual void unlock()
    {
        ZEN_OS::pthread_mutex_unlock(&lock_);
    }
}

// ZEN_Server_Status_P是一个要在多线程或者单线程情况下试用的模版
//模版参数_ZEN_LOCK 决定锁的行为是多线程还是单线程
class ZEN_Server_Status_P
{

protected:
    //多态的基类指针,根据初始化的参数决定是那个类的行为
    ZEN_Lock_Base         *stat_lock_;

public:
    //初始化,根据外部的参数决定锁的行为
    void init(bool multi_thread)
    {
        if (multi_thread )
        {
            stat_lock_ = ZEN_Null_Mutex();
        }
        else
        {
            stat_lock_= new ZEN_Thread_Mutex();
        }
    }

    //增加某项监控值,如果是在多线程下试用要避免冲突
    void increase_ byidx(size_t index, int64_t incre)
    {
        //锁的多态表现
        stat_lock_->lock();
        (stat_sandy_begin_ + index)->counter_ += incre;
        stat_lock_->unlock();
    }
    ……
}

你会发现,锁策略可以在初始化函数时决定,不再需要将模版的参数的影响扩大到各个地方。比如你要在类库中你也可以大量直接使用ZEN_Server_Status_P,因为这时你并不用决定他的具体行为。当然,由于动态是在运行时决定行为,虚函数还是多多少少有一点点性能开销的,当然在现在的CPU运算能力下,你不用过多的考虑这个问题。

比较两种策略化的方式

模版策略化是使用模版参数实现策略化,将策略的行为决定时间放到了编译期,性能最优。但适合代码规模不大,或者本身就是模版代码使用,(追求酷代码)。同时由于是将策略决定时间放在了编译期,会在继承,大规模使用的时候也必须使用模版策略行为。从而使策略的影响扩大化。

多态策略化是使用多态方法决定策略行为,在运行时调用者通过参数决定使用什么多态行为,同时由于决定行为放在运行时,不需要相关代码做出多大改变就可以使用。不足是性能相较模版策略化要弱一点点,在运行是必须要调用者控制策略行为。

比较而言,我其实认为多态策略化有更好的应用场景。个人感觉,有些时候,过度的模版设计反而会降低代码的可用性,影响使用者。

【本文作者是雁渡寒潭,本着自由的精神,你可以在无盈利的情况完整转载此文档,转载时请附上BLOG链接:http://www.cnblogs.com/fullsail/ 或者http://blog.csdn.net/fullsail,否则每字一元,每图一百不讲价。对Baidu文库加价一倍】

原文地址:https://www.cnblogs.com/fullsail/p/2616106.html