陈硕的Blog

用条件变量实现事件等待器的正确与错误做法

TL;DR 如果你能一眼看出 https://gist.github.com/chenshuo/6430925 中的那 8 个 Waiter classes 哪些是对的哪些是错的,本文就不必看了。

前几天,我发了一条微博 http://weibo.com/1701018393/A7FrW7ZVd ,质疑某本书对 Pthreads 条件变量的封装是错的,因为它没有把 mutex 的 lock()/unlock() 函数暴露出来,导致无法实用。后来大家讨论的分歧是这个 cond class 是不是通用的条件变量封装,还是只是一个特殊的“事件等待器”。作为事件等待器,其实现也是错的,因为存在丢失事件的可能,可以算是初学者使用条件变量的典型错误。

本文的代码位于 recipes/thread/test/Waiter_test.cc,这里提到的某书的版本相当于 Waiter1 class。

我在拙作《Linux 多线程服务端编程:使用 muduo C++ 网络库》第 2.2 节总结了条件变量的使用要点:

条件变量只有一种正确使用的方式,几乎不可能用错。对于 wait 端:
1. 必须与 mutex 一起使用,该布尔表达式的读写需受此 mutex 保护。
2. 在 mutex 已上锁的时候才能调用 wait()。
3. 把判断布尔条件和 wait() 放到 while 循环中。

对于 signal/broadcast 端:
1. 不一定要在 mutex 已上锁的情况下调用 signal (理论上)。
2. 在 signal 之前一般要修改布尔表达式。
3. 修改布尔表达式通常要用 mutex 保护(至少用作 full memory barrier)。
4. 注意区分 signal 与 broadcast:“broadcast 通常用于表明状态变化,signal 通常用于表示资源可用。(broadcast should generally be used to indicate state change rather than resource availability。)”

如果用条件变量来实现一个“事件等待器/Waiter”,正确的做法是怎样的?我的最终答案见 WaiterInMuduo class。“事件等待器”的一种用途是程序启动时等待初始化完成,也可以直接用 muduo::CountDownLatch 到达相同的目的,将初值设为 1 即可。

以下根据微博上的讨论过程给出几个正确或错误的版本,博大家一笑。只要记住 Pthread 的条件变量是边沿触发(edge trigger),即 signal()/broadcast() 只会唤醒已经等在 wait() 上的线程(s),我们在编码时必须要考虑 signal() 早于 wait() 的可能,那么就很容易判断以下各个版本的正误了。代码见 recipes/thread/test/Waiter_test.cc

版本一:错误。某书上的原始版,有丢失事件的可能。

1

版本二:错误。lock() 之后再 signal(),同样有丢失事件的可能。

2

版本三:错误。引入了 bool signaled_; 条件,但没有正确处理 spurious wakeup。

版本四五六:正确。仅限 single waiter 使用。

版本七:最佳。可供 multiple waiters 使用。

版本八:错误。存在 data race,且有丢失事件的可能。理由见 http://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex

总结:使用条件变量,调用 signal() 的时候无法知道是否已经有线程等待在 wait() 上。因此一般总是要先修改“条件”,使其为 true,再调用 signal();这样 wait 线程先检查“条件”,只有当条件不成立时才去 wait(),避免了丢事件的可能。换言之,通过使用“条件”,将边沿触发(edge trigger)改为电平触发(level trigger)。这里“修改条件”和“检查条件”都必须在 mutex 保护下进行,而且这个 mutex 必须用于配合 wait()。

思考题:如果用两个 mutex,一个用于保护“条件”,另一个专门用于和 cond 配合 wait(),会出现什么情况?

最后注明一点,http://stackoverflow.com/questions/6419117/signal-and-unlock-order 这篇帖子里对 spurious wakeup 的解释是错的,spurious wakeup 指的是一次 signal() 调用唤醒两个或以上 wait()ing 的线程,或者没有调用 signal() 却有线程从 wait() 返回。manpage 里对 Pthreads 系列函数的介绍非常到位,值得细读。

posted on 2013-09-09 03:01 陈硕 阅读(13982) 评论(21)  编辑 收藏 引用

评论

# re: 用条件变量实现事件等待器的正确与错误做法[未登录] 2013-09-09 11:47 春秋十二月

不错,假唤醒解释得对。  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-09 18:31 askforemore1018

"因此一般总是要先修改条件,使其为 true,再调用 signal();这样 wait 线程先检查条件,只有当条件不成立时才去 wait(),避免了丢事件的可能"

版本四中, 在 lock、while之后, wait之前,如果有发 signal的话,也会丢失 signal吧  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-09 23:49 陈硕

@askforemore1018
在你说的这种情况下,版本八有可能丢,版本四不可能丢,想想为什么。  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法[未登录] 2013-09-10 01:02 天道酬勤

对于版本四,只有当signal所在线程先获得锁修改条件变量再释放锁后,此时wait所在线程获得锁正好第一次运行到while处,这时并没有来得及wait,所以信号丢失了。  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法[未登录] 2013-09-10 01:06 天道酬勤

@askforemore1018
版本八没有加锁,当在 lock、while之后,wait之前,这个时间窗内被切换到signal所在线程时,信号就丢失了。  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 01:18 陈硕

@天道酬勤
版本四,在你说的情况下,根本不会去 wait(),因为 while 条件不满足,因此不会丢信号。  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 09:52 天道酬勤

@陈硕
signal不是发出信号了吗?
  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 10:00 陈硕

@ 天道酬勤
你想说啥?  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法[未登录] 2013-09-10 10:56 andy

class Waiter6 : private WaiterBase
{
public:
void wait()
{
CHECK_SUCCESS(pthread_mutex_lock(&mutex_));
while (!signaled_)
{
CHECK_SUCCESS(pthread_cond_wait(&cond_, &mutex_));
}
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_));
}

void signal()
{
CHECK_SUCCESS(pthread_mutex_lock(&mutex_));
CHECK_SUCCESS(pthread_cond_signal(&cond_));
signaled_ = true;
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_));
}

private:
bool signaled_ = false;
};
有个问题我始终搞不明白,如果一个线程先执行wait() 这样这个线程在被唤醒前,互斥量一直被加锁,pthread_mutex_lock(&mutex_)。那么signal(),只有互斥量被unlock了,才能调用pthread_cond_signal(&cond_),那岂不是永远也唤醒不了wait()线程了?小弟不才,希望有人帮忙解释下。  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 12:45 Rider

@andy
感觉你不知道信号量这个概念,最好能先查下手册或者翻下OS的书再提问,别人的时间也是时间啊。wait的时候是失去锁的,并且这一步是原子的。
  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 12:47 Rider

@andy
说错了,是条件变量。  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 13:34 slade

弱问版本3 spurious wakeup是怎么解释的啊,为什么会唤醒多个线程啊  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法[未登录] 2013-09-10 14:44 joe

@andy
我记得条件变量wait的内部实现是:在所在进场被阻塞前unlock互斥量,这就是为什么wait的参数需要传入互斥量的原因了
  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 15:19 天道酬勤

@陈硕
@天道酬勤
版本四,在你说的情况下,根本不会去 wait(),因为 while 条件不满足,因此不会丢信号。
但是signal已经被调用,信号不是发出去了吗?  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 15:48 wingc

@ 天道酬勤
这里要处理的情况是是等待线程和信号线程的同步,如果等待函数没有进入同步区,则说明根本没有等待线程执行,就算信号线程发出信号但因为没有等待也就不存在所谓丢失。
这个和版本一不一样,版本一因为信号函数与等待函数没有同步,这等待函数有可能在进入一个完全无谓的"同步区"时而没有开始等待时,信号被另一个线程发出,因此会又信号丢失。
不知道我解释得对不对,也不知道你理解了没有?  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 17:24 rtemslinux

@andy
phtread_cond_wait的时候,这一个api内部实现有三步
1. unlock
2. wait
3. lock again  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-10 17:27 rtemslinux

@slade

我看wiki说,在多核情况下cond_wait返回,并不代表之前的while条件满足。
至于pthread_cond_wait为啥会返回,说是
Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations. The race conditions that cause spurious wakeups should be considered rare.  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2013-09-11 16:22 tb

画图比较易懂些了   回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2014-04-22 22:26 xanpeng

试解答思考题。
思考题“如果用两个 mutex,一个用于保护“条件”,另一个专门用于和 cond 配合 wait(),会出现什么情况?”的代码应该是按下面这么写。
* mutex_signaled_用来控制对signaled_的访问,mutex_cond_结合条件变量使用。
* 最大的问题就是原来mutex_unlock+wait是原子的(通过pthread_cond_wait()),现在相当于人为拆分成mutex_unlock,wait两步。这样可能丢失事件(unlock和wait之间发生signal)。

class Waiter9 : private WaiterBase
{
public:
void wait()
{
CHECK_SUCCESS(pthread_mutex_lock(&mutex_signaled_));
while (!signaled_)
{
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_signaled_)); // 第一步
CHECK_SUCCESS(pthread_mutex_lock(&mutex_cond_));
CHECK_SUCCESS(pthread_cond_wait(&cond_, &mutex_cond_)); // 第二步
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_cond_));
CHECK_SUCCESS(pthread_mutex_lock(&mutex_signaled_));
}
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_signaled_));
}

void broadcast()
{
CHECK_SUCCESS(pthread_mutex_lock(&mutex_signaled_));
signaled_ = true;
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_signaled_));

CHECK_SUCCESS(pthread_mutex_lock(&mutex_cond_)); // 可以不要
CHECK_SUCCESS(pthread_cond_broadcast(&cond_));
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_cond_));
}

private:
bool signaled_ = false;
};  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2015-10-30 09:16 @sunjun

加锁的情况下,情况4不可能有signal丢失。因为这个waiter在进入之前会获取mutex锁,如果条件不满足cond_wait返回之前会释放锁,而在此之前signal是没有办法获取锁的,也就是说必须要cond_wait释放锁之后,signal才有可能执行到@陈硕
  回复  更多评论   

# re: 用条件变量实现事件等待器的正确与错误做法 2016-01-01 22:10 dodo

版本三为什么错误  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2011年3月>
272812345
6789101112
13141516171819
20212223242526
272829303112
3456789

导航

统计

常用链接

随笔分类

随笔档案

相册

搜索

最新评论

阅读排行榜

评论排行榜