86- 条件变量 condition

本文承接上文而来,主要是为了解决上一文中轮询所带来的 CPU 浪费问题。这里我们再把原问题复述一遍:

学生线程写作业,老师线程检查作业。要求:只有学生线程写完作业了,老师线程才能检查作业。

1. 条件变量

pthread 线程库为线程同步提供一了种机制——条件变量。它允许线程在睡眠的情况下等待特定的条件发生,从而被唤醒。

根据条件变量的这种特性,我们可以应用它来改写上一篇文章中的代码。

2. 条件变量的数据类型和相关函数

条件变量的数据类型是 pthread_cond_t.

  • 初始化
    • 通过常量 PTHREAD_COND_INITIALIZER 对其进行静态初始化
    • 通过下面的函数进行初始化和回收:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
  • 等待条件发生的函数
// 阻塞版本
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

// 超时版本
int pthread_cond_timedwait(pthread_cond_t *cond,
  pthread_mutex_t *mutex, const struct timespec *tsptr);

这里我们只关心阻塞版本的函数。可以看到有两个参数,第一个参数我们可以理解,但是第二个参数为什么会有个互斥量呢?

(1) 先来说 wait 函数的语义

wait 函数表示将本线程加入等待队列,同时将传入的 mutex 变量解锁,这两步是一个“原子操作”(加双引号后面解释)。所有进入等待队列的线程都在等待条件变量 cond 条件成立。所谓 cond 条件成立,指的是有其它线程通过函数 pthread_cond_signal 将等待队列中的某一个线程唤醒,或者使用 pthread_cond_broadcast 函数将等待队列中所有的线程唤醒。一旦等待队列中的线程被唤醒,会再次对传入的 mutex 变量加锁

(2) 为什么需要传入互斥量

如果把 pthread_cond_wait 函数分解成三步:

// 线程 A 中
pthread_mutex_unlock(&lock); // a1
pthread_cond_wait(&cond); // a2
pthread_mutex_lock(&lock); // a3
  • 假设上面的语句 a1 在执行完成后,线程被调度,此时线程 B 就能进入临界区(锁已经被释放)去执行代码,导致了线程 A 在执行语句 a2 前条件提前发生(图 1),线程 B 于是提前 pthread_cond_signal 去唤醒一个空的等待队列。接下来,线程 A 恢复执行语句 a2,导致永久等待


这里写图片描述
图1 未加锁时对条件变量的访问导致线程 A 永远等待下去

  • 有人说,不释放锁行不行?答曰:不行,条件(如前一篇文章实验里的 finished 变量)是共享资源,必然是被互斥量保护,如果不释放锁,直接执行 a2 线程 A 进入等待,另一方面学生线程永远无法进入临界区改变 finished 变量,死锁。

所以解决此方案的办法就是 a1 和 a2 必须保证是“原子”的,即一次执行完。注意这里的原子只是加了引号的,实际实现上,是没办法做到的,为什么?你想这样做吗?

ACQUIRE(&mylock); //a0
pthread_mutex_unlock(&lock); // a1
pthread_cond_wait(&cond); // a2
RELEASE(&mylock); //a3

那么上面语句 a2 完成后,a3 的锁由谁来释放?只不过在实际的实现中,a1 和 a2 的执行看起来像是“原子的”,这里的原子是说(有点拗口,反复读几遍):

如果线程 A 在释放锁后(语句 a1),执行语句 a2 时即将要阻塞的时候,线程 B 此时调用了 pthread_cond_signal 或者 pthread_cond_broadcast,就好像在那个即将要被阻塞的线程 A 已经阻塞过一样。

这里面的原理很复杂,我们在下一篇《深入条件变量》讨论,实际使用 pthread_cond_wait 函数的时候,大家就认为是原子的吧。知乎上有关于条件变量的讨论传送门,也请大家仔细甄别,有些回答不一定对,所以最好还是先看下一篇,再去看看知乎上的.

  • 唤醒函数
// 唤醒等待队列中的一个线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒等待队列中的所有线程,并重新参与调度
int pthread_cond_broadcast(pthread_cond_t *cond);

3. 使用条件变量改写实验

3.1 程序清单

#include <stdio.h>
#include <pthread.h>

int finished = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* do_homework(void* arg) {
  // doing homework
  sleep(5);
  pthread_mutex_lock(&lock);
  finished = 1;
  pthread_mutex_unlock(&lock);
  // 唤醒队列中的一个线程(如果有线程的话)
  pthread_cond_signal(&cond);
  printf("发送条件信号--------------\n");
}

void* check_homework(void* arg) {
  // 打电话
  sleep(1);
  // 电话接通
  pthread_mutex_lock(&lock);
  // 作业写完了吗?
  printf("老师:作业写完了吗?!\n");
  while(finished == 0) {
    // 没写完呐!
    printf("学生:没写完呐!\n");
    // 好的,你接着写
    printf("老师:好的,你接着写吧!\n");
    printf("-------------------------\n");
    // 因为此时加了锁,所以 finished 变量不可能产生变化。
    // 将线程加入等待队列,线程让出 cpu,同时将 mutex 解锁。
    pthread_cond_wait(&cond, &lock);
    printf("老师:作业写完了吗?!\n");
  }
  printf("学生:写完啦!\n");
  pthread_mutex_unlock(&lock);
  printf("老师开始检查---------------\n");
}


int main() {
  pthread_t tid1, tid2;
  pthread_create(&tid1, NULL, do_homework, NULL);
  pthread_create(&tid2, NULL, check_homework, NULL);
  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);
  return 0;
}

3.2 编译和运行

  • 编译和运行
$ gcc do_homework.c -o do_homework -lpthread
$ ./do_homework
  • 运行结果


这里写图片描述
图1 运行结果

3.3 结果分析

从图 1 的结果中可以看到,老师第一次询问学生,有两种情况:

  • 学生如果已经完成作业了,此时 finished == 1, while 循环体不会执行,老师直接为学生检查作业。
  • 如果学生作业没写完,会进入循环体,老师调用 wait 进入等待队列,同时对互斥量解锁。试想如果不解锁,学生永远无法进入临界区,这就导致了死锁。解锁后,学生写完作业,最后唤醒队列中的老师线程,老师线程从 wait 函数返回,立即又对 mutex 加锁,试想,如果不加锁,学生线程又操作了 finished 变量,导致竞争错误。因为老师线程醒来后就直接去检查 while 中的 finished 变量了,此时检查变量是必须的,只是在这个案例中体现不出来,如果有多个老师,一个学生,学生可以让任意一个老师检查作业(学生可以通过 broadcast 唤醒所有老师),就显得很有必要了。

4. 总结

  • 理解条件变量的作用
  • 理解等待队列
  • 理解 wait 函数 signal 函数的意义
  • 进入 wait 函数和离开 wait 函数发生了什么事情

练习1:完成本文中的实验,在此基础上再新增一个老师线程,即一个学生写完作业,可以让其中任何一个老师检查,可以使用 pthread_cond_broadcast 函数唤醒所有老师线程,从而产生竞争。

练习2:4 个线程,线程 1 循环打印 A, 线程 2 循环打印 B, 线程 3 循环打印 C, 线程 4 循环打印 D. 完成下面两个问题:
1) 输出 ABCDABCDABCD…
2) 输出 DCBADCBADCBA…

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值