文章目录
原文链接:https://blog.csdn.net/qq_41453285/article/details/104875213
一、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:
- **其在前一篇文章中进行了介绍,可参阅:**https://blog.csdn.net/qq_41453285/article/details/104859230
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