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