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

原创 2013年09月09日 03:02:55


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 系列函数的介绍非常到位,值得细读。

Linux 线程同步---条件变量

1. 相关函数                                                                                             ...
  • hiflower
  • hiflower
  • 2008-03-18 22:17:00
  • 43139

用条件变量(Condition Variable)实现信号量(Semaphore)

用条件变量(Condition Variable)实现信号量(Semaphore), 主要是通过条件变量控制资源数的加减操作,在这里定义sem_t 为     struct sem{      ...
  • jungxiangyi
  • jungxiangyi
  • 2012-10-08 18:01:25
  • 1432

同步条件变量(1)————等待多次事件

在c++多线程中,我们学习了用各种方法去保护在线程间共享的数据,但有时我们不只是需要保护数据,还需要在独立的线程上进行同步操作。例如一个线程在完成其任务之前需要等待另一个线程完成任务,c++标准库便提...
  • qq_31098037
  • qq_31098037
  • 2017-09-18 21:38:57
  • 275

线程的虚假唤醒

(转载)线程假唤醒的原因   2013-12-15 09:45:09|  分类: LINUX编程 |  标签:linuxunix知识  c++小知识  |举报|字号 订阅 ...
  • pi9nc
  • pi9nc
  • 2014-07-05 11:48:16
  • 4568

多线程 条件变量

作者:王东   1.1       什么是条件变量和条件等待? 简单的说: 条件变量(condition variable)是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个...
  • wangjiechen
  • wangjiechen
  • 2016-08-05 14:55:50
  • 685

pthread_cond_broadcast相关

pthread_cond_timedwait()函数阻塞住调用该函数的线程,等待由cond指定的条件被触发(pthread_cond_broadcast() or pthread_cond_signa...
  • cupidove
  • cupidove
  • 2013-07-15 14:28:13
  • 21939

[操作系统] pthread同步互斥:十字路口小车的死锁

不知道做的对不对,仅供参考。 1. 有两条道路双向两个车道,即每条路每个方向只有一个车道,两条道路十字交叉。假设车辆只能向前直行,而不允许转弯和后退。如果有4辆车几乎同时到达这个十字路口,如图(...
  • ZJU_fish1996
  • ZJU_fish1996
  • 2016-10-30 12:11:24
  • 3709

互斥量、条件变量与pthread_cond_wait()函数的使用,详解

pthread_cond_wait 条件变量 互斥量 线程
  • digu
  • digu
  • 2010-08-27 00:59:00
  • 5156

pthread_mutex_lock线程锁使用简单示例

#define __USE_LARGEFILE64 #define _LARGEFILE64_SOURCE #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif...
  • oywoywoyw
  • oywoywoyw
  • 2014-12-16 17:47:04
  • 2820

为什么pthread_cond_wait需要传递mutex参数

这是来自知乎的一个问题,由@吴志强提出,有意思的是,他看了大家的回答后,突然顿悟了,同时也发现有人答错了,于是乎,他自己回答了自己的问题。我看完后,发现他分析的很精彩,于是就记录在这。下面是他的自答:...
  • booirror
  • booirror
  • 2014-06-13 01:10:03
  • 2749
收藏助手
不良信息举报
您举报文章:用条件变量实现事件等待器的正确与错误做法
举报原因:
原因补充:

(最多只允许输入30个字)