关于 pthread cond 的注意事项

最近在使用 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 行),那么这样的写法是没问题的。

参考资料

[1] pthread_cond_signal(3) - Linux man page

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注