Linux(muduo网络库):04---线程同步概要之(封装MutexLock、MutexLockGuard、Condition)

  • 本文内容衔接于前一篇文章(互斥器、条件变量、读写锁、信号量、sleep)https://blog.csdn.net/qq_41453285/article/details/104859230
  • MutexLock、MutexLockGuard、Condition等class完整代码在muduo/base中都可以找到,这几个class都不允许拷贝构造和赋值

一、MutexLock、MutexLockGuard的封装

  • MutexLock:封装临界区(critical section),这是一个简单的资源类,用RAII手法封装互斥器的创建与销毁。MutexLock一般是别的class的数据成员
    • 临界区在Windows上是struct CRITICAL_SECTION,是可重入的
    • 在Linux下是pthread_mutex_t,默认是不可重入的
  • MutexLockGuard:封装临界区的进入和退出,即加锁和解锁。 MutexLockGuard一般是个栈上对象,它的作用域刚好等于临界区域

MutexLock

  • 说明:
    • MutexLock的附加值在于其提供了isLockedByThisThread()函数,用于程序断言
    • 关于tid()函数,在后面文章我们还会详细介绍“Linux的线程标识”
  • 代码如下:
class MutexLock :boost::noncopyable
{
public:
    MutexLock() :holder_(0)
    {
        pthread_mutex_init(&mutex_, NULL);
    }

    ~MutexLock()
    {
        assert(holder_ == 0);
        pthread_mutex_destory(&mutex_);
    }

    bool isLockByThisThread()
    {
        return holder_ == CurrentThread::tid();
    }

    void assertLocked()
    {
        assert(isLockByThisThread());
    }

    //仅供MutexLockGuard调用,严禁用户代码调用
    void lock()
    {
        //这两行顺序不能反
        pthread_mutex_lock(&mutex_);
        holder_ = CurrentThread::tid();
    }
    //仅供MutexLockGuard调用,严禁用户代码调用
    void unlock()
    {
        holder_ = 0;
        pthread_mutex_unlock(&mutex_);
    }

    //仅供Condition调用,严禁用户代码调用
    pthread_mutex_t* getPthreadMutex()
    {
        return &mutex_;
    }
private:
    pthread_mutex_t mutex_;
    pid_t holder_;
};

MutexLockGuard

  • 代码如下:
class MutexLockGuard :boost::noncopyable
{
public:
    explicit MutexLockGuard(MutexLock& mutex):mutex_(mutex)
    {
        mutex_.lock();
    }

    ~MutexLockGuard()
    {
        mutex_.unlock();
    }
private:
    MutexLock& mutex_;
};

#define MutexLockGuard(x) static_assert(false,"missing mutex guard var name");
  •  注意上面代码的最后一行定义了一个宏,这个宏是为了防止程序里出现下面这样的错误:
void doit()
{
    //错误,产生一个临时对象,互斥器创建之后立马又销毁了,下面的临界区没有锁住
    MutexLockGuard(mutex);
    //正确的做法要加上变量名,例如:MutexLockGuard lock(mutex)
    
    //...临界区
}
  • 注意事项:
    • 有人把MutexLockGuard写成template,此处没有这么做是因为它的模板类型参数只有MutexLock一种可能,没有必要随意增加灵活性,于是我手工把模板具现化(instantiate)了
    • 此外一种更激进的写法是,把lock/unlock放到private区,然后把MutexLockGuard设为MutexLock的friend。我认为在注释里告知程序员即可,另外check-in之前的code review也很容易发现误用的情况(grep getPthreadMutex)
  • 这段代码没有达到工业强度:
    • mutex创建为PTHREAD_MUTEX_DEFAULT类型,而不是我们预想的PTHREAD_MUTEX_NORMAL类型(实际上这二者很可能是等同的),严格的做法是用mutexattr来显示指定mutex的类型(互斥量属性可以参阅:https://blog.csdn.net/qq_41453285/article/details/90904833
    • 没有检查返回值。这里不能用assert()检查返回值,因为assert()在release build里是空语句。我们检查返回值的意义在于防止ENOMEM之类的资源不足情况,这一般只可能在负载很重的产品程序中出现。一旦出现这种错误,程序必须立刻清理现场并主动退出,否则会莫名其妙地崩溃,给事后调查造成困难。这里我们需要non-debug的assert,或许google-glog的CHECK()宏是个不错的思路
  • 一些其他想法:
    • muduo库的一个特点是只提供最常用、最基本的功能,特别有意避免提供多种功能近似的选择。muduo不是“杂货铺”,不会不分青红皂白地把各种有用的、没用的功能全铺开摆出来。muduo删繁就简,举重若轻;减少选择余地,生活更简单
    • MutexLock没有提供trylock()函数,因为我没有在生成代码中用过它。我想不出什么时候程序需要“试着去锁 一锁”,或许我写过的代码太简单了(trylock的一个用途是用来观察lock contention,见[RWC]“Consider using nonblocking synchronization routines to monitor contention”)

二、Condition的封装

  • 条件变量(condition variable)允许在 wait()的时候指定mutex
  • 关于为什么要自己封装Condition这个类:
    • 但是我想不出有什么理由一个condition variable会和不同的mutex配合使用。Java的intrinsic condition和Condition class都不支持这么做,因此我觉得可以放弃这一灵活性,老老实实地一对一好了
    • 相反,boost::thread的condition_variable是在wait的时候指定mutex, 请参观其同步原语的庞杂设计:
      • Concept有四种:Lockable、TimedLockable、SharedLockable、 UpgradeLockable
      • Lock有六种:lock_guard、unique_lock、shared_lock、 upgrade_lock、upgrade_to_unique_lock、scoped_try_lock
      • Mutex有七种:mutex、try_mutex、timed_mutex、 recursive_mutex、recursive_try_mutex、recursive_timed_mutex、 shared_mutex
    • 恕我愚钝,见到boost::thread这样小题大做的库,我只得三揖绕道而行。很不幸C++11的线程库也采纳了这套方案。这些class名字也很无厘头,为什么不老老实实用readers_writer_lock这样的通俗名字呢?非得增加精神负担,自己发明新名字。我不愿为这样的灵活性付出代价,宁愿自己做几个简简单单的一看就明白的class来用,这种简单的几行代码的“轮子”造造也无妨。提供灵活性固然是本事,然而在不需要灵活性的地方把代码写死,更需要大智慧

Condition

  • 下面这个muduo::Condition class简单地封装了条件变量,用起来也容易
  • 关于成员函数的命名规则:
    • 这里用notify/notifyAll作为函数名,因为signal有别的含义,C++里的signal/slot、C里的signalhandler等等
    • 就以为了不产生冲突,我们自己定义了这些成员函数的名称
class Condition :boost::noncopyable
{
public:
    explicit Condition(MutexLock& mutex) :mutex_(mutex)
    {
        pthread_cond_init(&pcond_);
    }
	~Condition()
    {
        pthread_cond_destory(&pcond_);
    }

    void wait()
    {
        pthread_cond_wait(&pcond_, mutex_.getPthreadMutex());
    }
    void notify()
    {
        pthread_cond_signal(&pcond_);
    }
    void notifyAll()
    {
        pthread_cond_broadcast(&pcond_);
    }
private:
    MutexLock& mutex_;
    pthread_cond_t pcond_;
};

关于条件变量与互斥器的使用

  • 如果一个类要包含MutexLock和Condition,一定要注意它们的声明顺序和初始化顺序:
    • MutexLock应该先于Condition构造
    • 并且MutexLock用来初始化Condition
  • 例如下面一个CountDownLatch(倒计时)class:
class CountDownLatch
{
public:
    //构造函数中,初始化顺序要与声明顺序一致
    //并且使用mutex_初始化conditon_
    CountDownLatch(MutexLock& mutex)
        :mutex_(mutex), conditon_(mutex_), count_(0) {}
private:
    MutexLock& mutex_;   //互斥器先于条件变量定义
    Condition conditon_;
    int count_;
};

三、总结

  • 请允许我再次强调,虽然本章花了大量篇幅介绍如何正确使用mutex和condition variable,但并不代表我鼓励到处使用它们:
    • 这两者都是非常底层的同步原语,主要用来实现更高级的并发编程工具
    • 一个多线程程序里如果大量使用mutex和condition variable来同步,基本跟用铅笔刀锯大树(孟岩语)没啥区别
  • 在程序里使用Pthreads库有一个额外的好处:分析工具认得它们, 懂得其语意。线程分析工具如Intel Thread Checker和Valgrind-Helgrind 33 等能识别Pthreads调用,并依据happens-before关系(参阅:http://research.microsoft.com/en-us/um/people/lamport/pubs/time-clocks.pdf)分析程序有无data race

四、附加

发布了1525 篇原创文章 · 获赞 1084 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104875213
今日推荐