最近在看陈硕写的多线程服务端编程,感叹真是本好书,写作严谨且内容丰富,没有一定的功力和多年实战经验是写不出来的,赞一个。
回到正题,书中第二章讲到了条件变量,对于这个同步原语,我的了解不多,也没曾深入去了解,只知道大概就是个用来当信号处理用的东西,以前在多线程方面,一般就 mutex, semaphore 用的多,似乎也能处理大部分的需求了。
知识面过窄的铁血证据。。。于是赶紧顺手去查一下。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_signal(pthread_cond_t *cond);
乍一看还以为和 semaphore 一个样,接口看起来仿佛挺像的。。。 啥眼神呢,差远了!从使用上来说,semaphore 是有状态的,允许先 post_semaphore,再 wait_semaphore,但 condition variable 就不行了,它是一种无状态的东西,如果调用 pthread_cond_signal() 在前,pthread_cond_wait() 在后,那 wait 是需要 block 住等待下一个 signal 的,因为前面的 signal 已经丢了。
陈硕在书中以绝对肯定的口吻指出正确使用条件变量的方式只有一种,按这方式的来套是“几乎不可能用错”的。论断很有意思,莫非条件变量很容易用错吗?再回头细查一下pthread_cond_wait 的 manual,发现这货用起来确实够麻烦的,不但稀奇古怪的结合了一个mutex,还要防止掉各种坑,比如说 spurious wakeup。
那什么是spurious wakeup呢,书中没有展开,估计默认是常识,我到网上查了下,spurious wakeup 说的是这样一种行为:
“a thread might be awoken from its waiting state even though no thread signaled the condition variable”
上面这段话摘自 wikipedia,可以这样理解,pthread_cond_wait 返回后,并不一定就真的是因为别的地方调用了pthread_cond_signal(),有可能是因为别的原因而返回了,因此这个 wakeup 是假的(spurious).
在 manual 中也有说明:
"When using condition variables there is always a boolean predicate involving shared variables associated with each condition wait that is true if the thread should proceed. Spurious wakeups from the pthread_cond_wait() or pthread_cond_timedwait() functions may occur. Since the return from pthread_cond_wait() or pthread_cond_timedwait() does not imply anything about the value of this predicate, the predicate should be re-evaluated upon such return."
结论是: 条件变量应该始终结合一个 bool 变量来使用,这个 bool 变量用来指示是否真的有人调用了signal,从而解决 spurious wakeup 的问题。
因此,按书中的说法,正确使用条件变量的方法有且只有下面这一种:
对于 wait 端:
1) 必须与mutex一起使用,且相应的bool变量要受该mutex的保护。
2) 先lock mutex,再wait。
3) 且,wait()要放到循环中,直到bool变量已改变。
对于signal 端:
1) signal()调用可以不用mutex保护。
2) 要先修改bool变量再进行signal().
3) 修改该bool变量需要用mutex进行保护。
写成代码的话,大概如下:
1 bool signaled = false; 2 pthread_mutex_t g_mutex; 3 pthread_cond_t g_cond; 4
5 void wait() 6 { 7 pthread_mutex_lock(&g_mutex); 8 while (!g_signaled) 9 { 10 pthread_cond_wait(&g_cond, &g_mutex); 11 } 12 //reset g_signaled if necessary. 13 //g_signaled = false;
14 pthread_mutex_unlock(&g_mutex); 15 } 16
17 void signal() 18 { 19 pthread_mutex_lock(&g_mutex); 20 g_signaled = true; 21 pthread_mutex_unlock(&g_mutex); 22 pthread_cond_signal(&g_cond); 23 }
根据前面的讨论,条件变量的代码写的这么麻烦,似乎完全就是因为这个spurious wakeup,那这个问题究竟是怎么引起的呢?为什么会有这样的问题呢?
stackoverflow上有过一个讨论:http://stackoverflow.com/questions/8594591/why-does-pthread-cond-wait-have-spurious-wakeups
结论是:
"Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations."
也就是,不处理 spurious wakeup 使得条件变量的实现效率更高而且更容易实现,这看起来倒和有些长系统调用会被中断有些类似。
如下代码来自这里,实现了一个简单的 wait 和 signal,解释了为什么会有 spurious wakeup,代码中的 wait,signal 分别在两个线程中进行,执行的顺序按最右边的序号进行。
pthread_cond_wait(mutex, cond): value = cond->value; /* 1 */ pthread_mutex_unlock(mutex); /* 2 */ pthread_mutex_lock(cond->mutex); /* 10 */
if (value == cond->value) { /* 11 */ me->next_cond = cond->waiter; cond->waiter = me; pthread_mutex_unlock(cond->mutex); unable_to_run(me); } else pthread_mutex_unlock(cond->mutex); /* 12 */ pthread_mutex_lock(mutex); /* 13 */ pthread_cond_signal(cond): pthread_mutex_lock(cond->mutex); /* 3 */ cond->value++; /* 4 */
if (cond->waiter) { /* 5 */ sleeper = cond->waiter; /* 6 */ cond->waiter = sleeper->next_cond; /* 7 */ able_to_run(sleeper); /* 8 */ } pthread_mutex_unlock(cond->mutex); /* 9 */
显然,如果调用 wait 的线程在第10步那里卡住了且之前已经有线程在等着 signal 的时候, 这时一旦有人去 signal,必然就会有多个线程同时被唤醒了,显然那些被卡在第10步那里的线程就是spurious wakeup了。说实话,确实有些不好用,也不够直接,如果能够用 c++ 的类来包装一层,对新手来说,学习的成本会低些吧。不过对写代码来说,求简单是有代价的,很多时候需要折衷,一直都这样。