第十一章 信号量与管程
10.1 信号量
回顾:
- 信号量机制可以用来解决同步和互斥问题。它只能被两个标准的原语wait(S)和signal(S)访问,也记为:“P操作”和“V操作”
信号量的特性:
自旋锁不能实现先进先出
信号量的实现:
10.2 信号量使用
-
信号量分类
资源信号量为任何非负值的整数 -
用信号量实现临界区的互斥访问
-
第一个进程进来之后,把1变成0,然后再进来一个线程,把0变为-1.因为信号量小于0,所以把这个进程放入等待队列。然后第一个队列结束后,把-1变成0,知道有线程再等待,等待的线程就会在第一个结束的时候进入临界区。
- 条件同步
- 保证N模块在X模块后执行
- 生产者消费者问题
- 信号量解决生产者消费者问题
P,V操作顺序改变会导致死锁。如先互斥,再去看缓冲区是否空或者满。
使用信号量的困难
10.3 管程
-
管程与临界区结构上差别在于多了共享数据,共享数据作为条件变量,如果条件变量的数量为0则跟临界区完全一样。进入管程的线程因资源被占用而进入等待状态,每个条件变量表示一种等待原因,对应着一个等待队列。管程最重要的两个操作:wait()和Signal(),wait将自身阻塞在等待队列中,唤醒一个等待者或释放管程的互斥访问,Signal操作将等待队列中的一个线程唤醒,如果等待队列为空,则等同空操作。
-
管程是一种用于多线程互斥访问共享资源的程序结构。
- 采用面向对象的方法,简化线程间的同步控制;
- 保证任意时刻最多只有一个线程执行管程代码;
- 管程与临界区的区别是正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复,而临界区只有线程退出临界区才能放弃互斥访问。
-
管程的使用
- 在对象/模块中,收集相关共享数据
- 定义访问共享数据的方法
- 管程的组成
条件变量
条件变量,感觉这个“用信号量解决生产者-消费者问题有点问题”
用管程解决生产者-消费者问题
- 管程条件变量释放处理方式有两种如下图。可以看到Hoare的管程方式是更符合实际使用效果的,但是Hansen管程实现方式少了一次进程上下文切换,因此真实OS和Java中一般使用Hansen管程方式。
关于Hansen管程与Hoare管程分别使用while和if的理解:
可以看到,在T2线程调用x.signal之后,hansen模式会继续执行,所以当重新回到wait线程的时候,可能情况已经发生了变化,所以需要重新判断;而Hoare模式会立刻从T2线程切换到T1线程。Hansen看起来变得复杂,引入了不确定性,但是相比hoare模式,少了一次线程的切换,在真实的操作系统中就是这么实现的,所以我们编码的时候都需要用while循环判断条件是否成立。
经典的同步问题
10.4 哲学家就餐问题
- 问题描述
- 使用信号量的方法
- 不正确,可能导致死锁。如果五个人同时拿左边的刀叉
- 方案二:
-
使用pv操作,保证只有一个哲学家进餐
-
互斥访问正确,但每次只允许一人进餐
-
方案三
- 根据哲学家的奇偶拿不同的刀叉,不会出现死锁可以多人就餐
10.5 读者–写者问题
-
问题描述
-
需要的信号量
-
在此方案中,读者优先
读者/写者问题的优先策略: -
读者优先策略
- 只要有读者正在读状态,后来的读者都能直接进入
- 如读者持续不断进入,则写者就处于饥饿
-
写者优先策略
- 只要有写者就绪,写者应尽快执行写操作
- 如写者持续不断就绪,则读者就处于饥饿
管程实现的方法
- 写者优先
- 以上程序中,Read()程序中,等待的写者有两类,一类是正在写的写者,一类是正在等待的写者。而Write()程序中,只需等待正在读的读者。
以上的**broadcast()**表示等待在条件变量上的所有reader都被唤醒了