东阳的学习笔记
如果要等待某个条件成立,我们必须使用条件变量(condition variable)。条件变量顾名思义是一个或者多个线程等待某个布尔表达式为真,即等待别的线程唤醒它
。条件变量的学名叫管程
。Java Object 中内置的 wait()、notify()、notifyAll()是条件变量。
条件变量只有一种正确使用的方式,几乎不可能用错。
对于wait端:
- 必须与 mutex 一起使用,该布尔表达式的读写受此 mutex 保护
- 在 mutex 已上锁的时候才能调用 wait()。
- 把判断布尔条件和 wait() 放到 while 循环中。
下面代码中必须用 while 循环来等待条件变量,而不能使用 if 语句,原因是 spurious wakeup
。
spurious wakeup: (https://en.m.wikipedia.org/wiki/Spurious_wakeup)当线程从等待已发出信号的条件变量中醒来时,只是发现未满足其正在等待的条件时,就会发生虚假唤醒。 之所以称其为“虚假”,是因为线程似乎无缘无故被唤醒。 但是,虚假唤醒不会无缘无故地发生:它们通常是因为在发出条件变量的信号和等待线程最终运行之间的时间内,另一个线程运行并更改了条件。 线程之间存在竞争条件,典型的结果是,有时在条件变量中醒来的线程首先运行,赢得了比赛,有时又运行了,从而失去了比赛。
在许多系统上,特别是在多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在信号通知条件变量时在等待条件变量,则系统可能会决定全部唤醒它们,将每个信号都视为唤醒一个线程 广播唤醒所有它们,从而破坏信号和唤醒之间可能存在的预期的1:1关系。[1] 如果有十个线程在等待,则只有一个线程将获胜,而另外九个线程将遭受虚假唤醒。
因此当线程在条件变量中唤醒时,它应始终检查其满足的条件是否得到满足。 如果不是,则应返回到条件变量的休眠状态,等待其他机会。
muduo::MutexLock mutex_;
muduo::Condition cond(mutex_);
std::deque<int> queue;
int dequeue()
{
MutexLockGuard lock(mutex_);
while (queue.empty()) // 必须用循环;必须在判断之后再 wait
{
cond.wait(); // 这一步会原子地 unlock mutex_ 并进入等待, 不会与 enqueue 死锁
// wait() 执行完毕后会自动重新加锁
}
}
对于 signal/broadcast 端
- 不一定要在 mutex 已上锁的情况下调用 signal(理论上)。
- 在 signal 之前一定要修改布尔表达式。
- 修改布尔表达式通常需要用 mutex 保护(至少用作 full memory barrier)
- 注意区分 signal 和 broadcast:broadcast 通常表明状态变化,signal 通常表示资源可用。
muduo::Condition 采用了 notify() 和 notifyAll() 为函数名,避免了重载 signal 这个术语
void enqueue(int x)
{
MutexLockGuard lock(mutex);
queue.push_back(x);
cond.notify(); // 可以移出临界区之外
}
条件变量是非常底层的同步原语,很少直接使用,一般都是用它来实现高层的同步措施,如 BlockingQueue<T> 或 CountDownLatch。
倒计时、是一种常用且易用的同步手段。它主要有两种用途:
- 主线程发起多个子线程,等这些子线程各自完成一定的任务后,主线程才继续执行。通常用于主线程等待多个子线程完成初始化。
- 主线程发起多个子线程,子线程都等待主线程,主线程完成其他一些任务之后,通知所有的子线程开始执行。通常用于多个子线程等待主线程发出 “起跑” 命令。
class CountDownLatch: boost::noncopyable {
public:
explicit CountDownLatch(int count); // 倒数几次
void wait(); // 等待计数值变为0
void CountDown(); // 计数减1
private:
mutable muduo::MutexLock mutex_;
muduo::Condition condition_;
int count_;
};
// 构造函数
CountDownLatch::CountDownLatch(int count)
: mutex_(),
condition_(mutex_),
count_(count)
{ }
void CountDownLatch::wait()
{
muduo::MutexLockGuard lock(mutex_);
while (count_ > 0)
condition_.wait();
}
void CountDownLatch::CountDown()
{
muduo::MutexLockGuard lock(mutex_);
--count_;
if (count_ == 0)
{
condition_.notifyAll();
}
}
思考:CountDownLatch::countDown() 使用的是 Condition::notifyAll(),而前面p.41的 enqueue() 使用的是 Condition::notify()?
答:enqueue处明确知道是要唤醒单个线程 dequeue,而这里却不知道被唤醒的是哪一个线程,而是一次唤醒多个,所以需要用 noticeAll。(自己的理解,不知道对不对,如有错误欢迎指正!)