最近在使用 pthread condition variable 的时候出现了问题,经过排查发现是 pthread_cond_signal()
放在了 pthread_mutex_unlock()
之后调用导致的。本来不是个什么复杂的问题,但是由于自己一直以来的忽略,觉得有必要记录一下。
先来看下很多文章中都用到的关于 pthread_cond_
系列函数的例子:
/* ----- producer ----- */
pthread_mutex_lock(&mutex); /* 1 */
queue.push(item); /* 2 */
pthread_cond_signal(&cond); /* 3 */
pthread_mutex_unlock(&mutex); /* 4 */
/* ----- consumer ----- */
pthread_mutex_lock(&mutex); /* 5 */
while (queue.empty()) { /* 6 */
pthread_cond_wait(&cond, &mutex); /* 7 */
} /* 8 */
item = queue.pop(); /* 9 */
pthread_mutex_unlock(&mutex); /* 10 */
在消费者的代码片段(语句 5~10)中,第 6 行为什么要用 while 而不能用 if,是因为在多个消费者的情况下,由于 pthread_cond_signal()
的实现不同,有可能导致消费者被唤醒后拿不到 item。原因可以看下参考资料 [1] 或者之前写的 一篇笔记。
在生产者的代码片段(语句 1~4)中,第 3 行和第 4 行的顺序问题似乎没看到太多的介绍或解释。单从上面的片段来说,第 3 和第 4 行不管哪个在前哪个在后都不影响正确性,甚至把 3 放在 4 之后性能还有那么一丢丢的提升,因为 unlock 之后再 signal,在第 7 行就能立马拿到锁;而 signal 在 unlock 之前的话,第 7 行还要在获取锁上面花费一点点时间。
考虑到这一点点几乎可以忽略不计的性能提升以及自己的强迫症,我在实现中都采用 3 在 4 之后的写法,然后在实际应用中的一个场景,这种写法出了问题。
简化后的场景是这样的:
/* ----- producer ----- */
pthread_mutex_lock(&mutex); /* 11 */
queue.push(item); /* 12 */
pthread_mutex_unlock(&mutex); /* 13 */
pthread_cond_signal(&cond); /* 14 */
/* ----- consumer ----- */
pthread_mutex_lock(&mutex); /* 15 */
while (queue.empty()) { /* 16 */
pthread_cond_wait(&cond, &mutex); /* 17 */
} /* 18 */
item = queue.pop(); /* 19 */
pthread_mutex_unlock(&mutex); /* 20 */
pthread_cond_destroy(&cond); /* 21 */
pthread_mutex_destroy(&mutex); /* 22 */
和代码 1~10 的区别有两处:
- 第 13 和第 14 行交换了第 3 和第 4 行的位置;
- 多了 21 和 22 两行,消费者在 unlock 之后立刻销毁了 mutex 和 cond。
导致问题的执行顺序是这样的(从上往下是执行顺序,注释对应的是上面的代码标号):
/* ----- producer ----- */ | /* ----- consumer ----- */
|
pthread_mutex_lock(&mutex); /* 11 */ |
queue.push(item); /* 12 */ |
pthread_mutex_unlock(&mutex); /* 13 */ |
| pthread_mutex_lock(&mutex); /* 15 */
| while (queue.empty()) { /* 16 */
| pthread_cond_wait(&cond, &mutex); /* 17 */
| } /* 18 */
| item = queue.pop(); /* 19 */
| pthread_mutex_unlock(&mutex); /* 20 */
|
| pthread_cond_destroy(&cond); /* 21 */
| pthread_mutex_destroy(&mutex); /* 22 */
pthread_cond_signal(&cond); /* 14 */ |
由于调度的缘故,当生产者往队列里放入 item 并 unlock(第 13 行)后,消费者刚好拿到锁(第 15 行)并跳过了 wait 这一步,接着消费了队列中的 item,然后把 cond 和 mutex 都析构了,最后才调度到生产者线程执行最后的 signal,也就是说,在第 14 行的 pthread_cond_signal()
操作了一个被析构的 cond,最终的行为是不确定的(在我遇到的问题中的表现是死锁,但并不是真正的原因)。
当然并不是说第 13 和第 14 行这样的顺序就一定会有问题,要具体问题具体分析。具体到这个例子中是因为在拿到 item 后就立刻析构导致的,如果在拿到 item 后并没有析构的操作(只有代码 11~20 行),那么这样的写法是没问题的。