本文承接上文而来,主要是为了解决上一文中轮询所带来的 CPU 浪费问题。这里我们再把原问题复述一遍:
学生线程写作业,老师线程检查作业。要求:只有学生线程写完作业了,老师线程才能检查作业。
pthread 线程库为线程同步提供一了种机制——条件变量。它允许线程在睡眠的情况下等待特定的条件发生,从而被唤醒。
根据条件变量的这种特性,我们可以应用它来改写上一篇文章中的代码。
条件变量的数据类型是 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 和 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);
#include
#include
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;
}
$ gcc do_homework.c -o do_homework -lpthread
$ ./do_homework
从图 1 的结果中可以看到,老师第一次询问学生,有两种情况:
练习1:完成本文中的实验,在此基础上再新增一个老师线程,即一个学生写完作业,可以让其中任何一个老师检查,可以使用 pthread_cond_broadcast 函数唤醒所有老师线程,从而产生竞争。
练习2:4 个线程,线程 1 循环打印 A, 线程 2 循环打印 B, 线程 3 循环打印 C, 线程 4 循环打印 D. 完成下面两个问题:
1) 输出 ABCDABCDABCD…
2) 输出 DCBADCBADCBA…