条件变量 之 稀里糊涂的锁

条件变量 之 稀里糊涂的锁

0. 前言

条件变量(condition variable)和互斥锁(mutex),这两个寻常百姓人家常用的同步工具,想必大家都不陌生。但是在最近遇到的关于条件变量和互斥锁错误使用的 bug,才发现自己之前对条件变量的理解不够深入,可以尝试问自己这个问题:为什么条件变量和互斥锁总是搭配着使用 ?互斥锁的作用是什么?非加互斥锁不可吗 ?是不是迟疑了一下,看来条件变量也用点有点糊涂,为了加深理解,特意写在这里和大家分享。

实际上,这问题并不难,这里将从一个错误的使用例子来看,相信只需要花大家几分钟时间,就能够搞明白,废话不多说,先看错误写法

1. 错误写法

初始变量:

bool ready = false;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

Thread A

1: pthread_mutex_lock(&mutex);
2: while (false == ready) {
3:     pthread_cond_wait(&cond, &mutex);
4: }
5: pthread_mutex_unlock(&mutex);

Thread B

1: ready = true;
2: pthread_cond_signal(&cond);

2. 怎么就错了

证明一个东西是错的,简单,给出一种错误的 case 就好,如下的执行序列 Thread A 就会丢失 Thread B 的条件变量的唤醒永久 wait,如下表格,在 ready = false 的时候,Thread A 进入 while 循环,但是还没有执行 wait 的时候,thread B 执行了 ready = true 和 signal 唤醒,那么就出现条件变量唤醒 signal 先于 wait,那么相当于 Thread A 还没有被加入唤醒队列,这个时候,你已经 signal 唤醒了,那么这次唤醒自然就丢失了,执行序列的第 5 行,也就是 Thread 的第 3 行pthread_cond_wait(&cond, &mutex) 就会一直等在那里了。


3. 错在哪

核心出错的原因就是一个点,当判断 ready 为 false 进入 while 循环(Thread A 第 2 行),调用 pthread_cond_wait (Thread A 第 3 行)之前,Thread B 修改了 ready 为 true,并且 signal 了条件变量,导致 signal 先于 wait,出现丢失。这里根本的原因就是没有保证 ready == false 判断成立和 Thread A 调用 wait 进入唤醒队列的原子性,也就是我一旦判定 ready 为 false,那么必须进入等待队列,且在这期间不允许有人修改 ready 为 true,并执行 signal 操作。所以正确使用条件变量有两个约束

  • (1) 保证 ready == false 判断成立和 Thread A 调用 wait 进入唤醒队列的原子性
  • (2) 设置 ready = true 在 signal 唤醒之前,也就是 Thread B 的 1、2 两行的顺序绝对不能反过来

上面错误的写法仅仅满足了约束 (2),但是却确忽略了(1)。一般来说约束(2)大部分人都能够意识到,因为错误 case 比较容易想到,这里为了不保持连贯性,错误 case 放在附录 1,后面再看。

上面错误的写法虽然给 Thread A 加锁了,但是这个锁加得有点糊里糊涂,没弄明白为什么要给 pthread_cond_wait(&cond, &mutex) 传递一个互斥量,以为是 pthread_cond_t 内部存在竞态条件,其实并不是,pthread_cond_wait(&cond, &mutex)调用之前会加锁,然后在内部将 thread 加入唤醒队列,然后才释放锁,其实就是为了保证约束(1),显然仅仅 Thread A 加锁,而 Thread B 设置 ready = true 没有加锁,并不能保证约束(1)的原子性。所以为了满足约束(1),需要给 Thread B 设置 ready = true 也加锁,正确的写法如下:

Thread A

1: pthread_mutex_lock(&mutex);
2: while (false == ready) {
3:     pthread_cond_wait(&cond, &mutex);
4: }
5: pthread_mutex_unlock(&mutex);

Thread B

1: pthread_mutex_lock(&mutex);
2: ready = true;
3: pthread_mutex_unlock(&mutex);
4: pthread_cond_signal(&cond);

这样就保证了原子性约束(1),那么无论线程如何运行,都只会有只有两种情况,情况 1:如果 Thread A 先拿到 mutex,那么此时 ready 为 false,Thread A 调用 pthread_cond_wait 进入等待队列,接着释放 mutex,然后 Thread B 才能修改 ready,并 signal;情况 2:如果 Thread A 没有拿到 mutex,Thread B 拿到 mutex,然后修改 ready 为 true,然后释放锁,这样 Thread A 在拿到 mutex,就不会再进 while 循环调 wait 了。

4. 这样也对

其实 Thread B 还可以这么写,也是正确的:

1: pthread_mutex_lock(&mutex);
2: ready = true;
3: pthread_cond_signal(&cond);
4: pthread_mutex_unlock(&mutex);

这样写也是正确的,但是性能会稍差写,考虑一种情况,Thread B 执行完第3行,但是第4行还未执行,那么 Thread A 将被唤醒,然后 Thread A 尝试去加锁,但是 Thread B 还没释放锁,所以 Thread A 会继续睡眠,然后 Thread B 再释放锁,会再次唤醒 Thread A,所以这种写法相比上面正确的写法可能会多一次线程上下文切换。实际上这里将 pthread_cond_signal(&cond) 加入到临界区中,保证了整个原子性,那么就不需要上面的约束(2)了,因此下面的写法也是正确的。

1: pthread_mutex_lock(&mutex);
2pthread_cond_signal(&cond);
3: ready = true; 
4: pthread_mutex_unlock(&mutex);

非常感谢 @lyyfer @rsy56640 指出在具体实现中,上述情况可能会多的一次线程上下切换在 pthread 中已经被优化,所以不会存在这个问题。

5. 附录

Thread A

1: pthread_mutex_lock(&mutex);
2: while (false == ready) {
3:     pthread_cond_wait(&cond, &mutex);
4: }
5: pthread_mutex_unlock(&mutex);

Thread B

1: pthread_cond_signal(&cond);
2: ready = true;

这种错误写法实际上既没有满足约束(1),也没有满足约束(2),约束(1)上面讲过了,这里不在赘述,直接给违背约束(2)带来的 error case:


Notes

于作者水平,难免有理解和描述上有疏漏或者错误的地方,欢迎共同交流;部分参考已经在正文和参考文献中列表注明,但仍有可能有疏漏的地方,有任何侵权或者不明确的地方,欢迎指出,必定及时更正或者删除;文章供于学习交流,转载注明出处

编辑于 2019-03-31

文章被以下专栏收录