前言
muduo库也对线程同步原语封装了一下,为下面三个类:
(1) MutexLock (2)Condition (3) CountDownLatch
我将简化类,或以伪代码的形式作为讲解
mutex类
简化后的代码
class MutexLock : noncopyable
{
public:
MutexLock()
{
//pthread_mutex_init 系统调用 初始化一个锁
MCHECK(pthread_mutex_init(&mutex_, NULL));
}
~MutexLock()
{
//pthread_mutex_destroy 系统调用 销毁锁
MCHECK(pthread_mutex_destroy(&mutex_));
}
void lock()
{
//pthread_mutex_lock系统调用 加锁
MCHECK(pthread_mutex_lock(&mutex_));
}
void unlock()
{
//pthread_mutex_lock系统调用 解锁
MCHECK(pthread_mutex_unlock(&mutex_));
}
pthread_mutex_t* getPthreadMutex() /* non-const */
{
return &mutex_;
}
private:
friend class Condition;
pthread_mutex_t mutex_;
};
该类就是对线程同步原语的简单封装,友元类Condition,condition的成员函数,使用MutexLock对象时可以无视访问控制规则(c++primer友元类部分,有详细讲述该内容)
Condition
class Condition : noncopyable
{
public:
explicit Condition(MutexLock& mutex)
: mutex_(mutex)
{
//创建条件变量
MCHECK(pthread_cond_init(&pcond_, NULL));
}
~Condition()
{
//销毁条件变量
MCHECK(pthread_cond_destroy(&pcond_));
}
void wait()
{
//条件变量阻塞,所有使用该mutex_锁的线程会进入一个等待队列中,暂时无法获取锁
MCHECK(pthread_cond_wait(&pcond_, mutex_.getPthreadMutex()));
}
void notify()
{
//条件变量解除堵塞,通知堵塞队列中一个线程获取锁
MCHECK(pthread_cond_signal(&pcond_));
}
void notifyAll()
{
//条件变量解除堵塞,通知堵塞队列中所有线程去获取锁(注意锁争用)
MCHECK(pthread_cond_broadcast(&pcond_));
}
private:
MutexLock& mutex_;
pthread_cond_t pcond_;
};
上述代码对条件变量做了简单封装,这里就不缀述原理了,不了解这块内容可自行搜索条件变量或者线程同步原语
CountDownLatch类
头文件代码
class CountDownLatch : noncopyable
{
public:
explicit CountDownLatch(int count);//避免传入bool等意义不明的参数,禁止隐式转换
void wait();//堵塞直到计数减到0
void countDown();
int getCount() const;
private:
mutable MutexLock mutex_;
Condition condition_ GUARDED_BY(mutex_);
int count_ GUARDED_BY(mutex_);//确保对count_ 的操作是原子的
};
构造函数为什么要exexplicit修饰
exexplicit 作用是禁止传参时的隐式转换。避免传入bool等意义不明的参数。
个人测试:bool(true)传给int为1
mutable作用是什么
mutable修饰的成员变量,被const成员函数修改,突破了const 对象无法修改成员变量的限制。日常中还是建议不用。
源代码
CountDownLatch::CountDownLatch(int count)
: mutex_(),
condition_(mutex_),
count_(count)
{
}
void CountDownLatch::wait()
{
MutexLockGuard lock(mutex_);
//必须用循环,堵塞该函数调用之后的逻辑,count_ == 0时,解除堵塞
while (count_ > 0)
{
condition_.wait();
}
}
void CountDownLatch::countDown()
{
MutexLockGuard lock(mutex_);
--count_;
if (count_ == 0)
{
condition_.notifyAll();
}
}
int CountDownLatch::getCount() const
{
MutexLockGuard lock(mutex_);
return count_;
}
分析
CountDownLatch 和golang 中的waitgroup类似,学过go的同学理解这应该相对容易些。CountDownLatch一般和线程一起使用,试想有这样一个场景:
如果你想创建n个线程,确定这些线程运行起来后,再执行后面逻辑,你该怎么做?
void func(){ //创建CountDownLatch对象,设count_为线程数n //创建n个线程,将CountDownLatch的指针作为线程传入参数传进去, 线程函数体中调用CountDownLatch->CountDownLatch方法,是count_-- //CountDownLatch.wait() //其他逻辑 }
当n个线程运行起来后,CountDownLatch.wait()才会解除堵塞,进而执行后面的逻辑。
注意点1:
调用CountDownLatch.wait()时,如果此时thread1启动 了,thread1函数体中调用CountDownLatch->CountDownLatch()使count-- 了,thread1依旧无法获取锁,只有最后一个线程都启动了,此时由于count数 <=0,CountDownLatch->CountDownLatch()会调用notifyall通知所有线程 去争夺锁,同时调用countDownLatch.wait()的线程解除堵塞,执行后面逻辑。
注意点2:
锁临界区范围
void CountDownLatch::countDown()
{
MutexLockGuard lock(mutex_);
--count_;
if (count_ == 0)
{
condition_.notifyAll();
}
}
锁的临界区必须为count- -和if判断。如果只对count的操作加锁,可能会导致调用不止一次condition_.notifyAll()。理解多线程下锁的临界区问题的最好方法还是把函数割裂成一个个代码块来看。