【2021/7/19 更新】【梳理】简明操作系统原理 第八章 条件变量和信号量(docx)

配套教材:
Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Reiher
参考书目:
1、计算机操作系统(第4版) 汤小丹 梁红兵 哲凤屏 汤子瀛 编著 西安电子科技大学出版社

在线阅读:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 认为课本应该是免费的
————————————————————————————————————————
这是专业必修课《操作系统原理》的复习指引。
需要掌握的概念在文档中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
文档下载地址:
链接:https://pan.baidu.com/s/1c1ALECQLuoJfmAlj4bx4_A
提取码:0000

八 条件变量与信号量

条件变量(condition variable)是一种队列,其中的线程正在等待某个条件满足。当其它某个线程达成该条件后,该线程就可以唤醒若干个等待的线程继续执行。这个思想最早出自E. W. Dijkstra,他称之为私有信号量(private semaphores)。后来,Tony Hoare将其命名为条件变量。
引入条件变量,是为了能让线程实现在需要等待一个自定义的条件满足以后继续执行。有些情况下,如果不使用条件变量,这个目的只能通过旋转锁来实现,大量耗费CPU,甚至会引发错误。条件变量使得无需进行这样的忙等待。
C++11的头文件<condition_variable>提供了条件变量的跨平台实现。

一个条件变量有两个主要的成员函数:wait()和signal(),分别对应线程的睡眠和唤醒操作。具体来说,Linux中对应pthread_cond_wait()、pthread_cond_signal();而std::condition_variable中则对应wait()、notify_one()。为了方便,我们常用wait()和signal()来统一指代。这两个操作都是原子操作。

来看下面的代码:
#include <pthread.h>
#include

int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;

void thr_exit() {
pthread_mutex_lock(&m);
done = 1;
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}

void* child(void* arg) {
puts(“child”);
thr_exit();
return NULL;
}

void thr_join() {
pthread_mutex_lock(&m);
while (done == 0) pthread_cond_wait(&c, &m);
pthread_mutex_unlock(&m);
}

int main() {
puts(“parent: begin”);
pthread_t p;
pthread_create(&p, NULL, child, NULL);
thr_join();
puts(“parent: end”);
return 0;
}
wait()接受一个互斥锁为参数。当wait()被调用时,它假设传入的互斥锁是上锁的。调用wait()将尝试解锁并令线程进入睡眠。当然,这也是原子操作,调度器无法将其打断。当线程被唤醒(从等待中返回)后,又被再次上锁。使用条件变量需要防止的一个主要问题是:避免有线程陷入永久休眠而没有线程能够唤醒它。为什么要给条件变量传入锁作为参数?之后我们马上会解释。

设想两种情况:
(1)父线程创建子线程,然后继续运行(假设CPU只有一个核心),立即调用thr_join()等待子线程结束。这时候,父线程会获得锁,检查子线程是否已经结束,并调用wait()将自己睡眠(也释放了锁)。子线程输出字符串并结束后,调用thr_exit()来唤醒父线程。调用以后又会上锁,变量done置为1,父线程被唤醒。最后,父线程继续运行(从wait()带着锁返回),释放锁,输出消息,结束。
(2)子线程创建后立刻运行,把done设为1,唤醒沉睡的线程。但是并没有线程在休眠,所以没有唤醒任何线程。父线程此时调用了thr_join(),发现done = 1,不进行等待,直接返回。
函数thr_join()内有一个while循环。虽然从程序逻辑上来说,这并不是严格必要的,但使用while是个好习惯。这点在之后也会继续讲解。
为了明确thr_exit()和thr_join()中的每条代码的作用,我们尝试把这些代码替换一下。
【1】变量done在这里用于刻画子线程是否已经结束。如果不使用变量done,即把这两个函数修改成这样:

void thr_exit() {
pthread_mutex_lock(&m);
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}

void thr_join() {
pthread_mutex_lock(&m);
pthread_cond_wait(&c, &m);
pthread_mutex_unlock(&m);
}
很不幸,这个方法不可行。在情况(2)中,子线程在创建后立即运行并结束。父线程直接调用wait(),然后就会一直卡住,没有进程再来唤醒它。也就是说,当有线程需要等待其它线程结束再继续时,如果不使用与条件变量不同的专门的变量来指示一个被其它线程等待的线程是否已经退出,那么等待这个线程的线程可能永远无法被唤醒。换句话说,这个专门变量,使得尝试休眠的进程在条件变量发出信号之后依然能判定是否需要休眠。
【2】如果去掉锁:
void thr_exit() {
done = 1;
pthread_cond_signal(&c);
}

void thr_join() {
if (done == 0) pthread_cond_wait(&c);
}
当然,这份代码不会通过编译。因为pthread_cond_wait()的一个参数必须是锁。但是这并不影响我们分析不用锁会导致什么后果。
在情况(1)中,如果父线程调用了thr_join(),验证done = 0后尝试睡眠,但就在它准备调用wait()去睡眠之前,父线程被打断,子线程运行。子线程修改done = 1并唤醒父线程,并没有线程可以被唤醒,于是唤醒操作就这样结束了。当父线程继续运行时,执行wait()以后将永远休眠。也就是说,当有线程需要等待其它线程结束再继续时,如果不使用锁将检测指示变量与和该变量有关的动作合并为原子操作,一种比较坏的后果是:会有线程永远不能被唤醒。换句话说,这个作为条件变量的参数的锁,保护的并不是条件变量本身或者其它某个共享的缓冲区,而是纯粹用于将“判定应当休眠”和“进入休眠”两个动作合并为原子操作,使它们无法被调度器打断,导致进入睡眠的线程睡死。
综合来说,这两种情况都是所谓的notify-before-wait问题:notify(),或者说signal(),这个动作是瞬时的而非持续的。signal()动作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值