下面我们来介绍管程
这是我们这里
今天说到的第二种同步方法
我们在刚才的
同步方法介绍当中呢
有了信号量
它使得我们在操作系统参与下
我们处理临界区呢
比原来会变得更方便
好 那接下来我们介绍的管程呢
实际上是想改进信号量
在处理临界区的时候一些麻烦
比如说在我们刚才看到的
生产者消费者问题当中
信号量的PV操作
是分散在生产者和消费者
两个不同的进程当中
在这种情况下
我们PV操作的配对
是比较困难的
好 我们试图把这些
配对的PV操作
集中到一起
这就是我们这里说到的管程
它也是一种并发程序的编程方法
那为了支持它呢
我们在底下呢
还得再加上一个条件变量
这是在管程内部呢
使用的一种同步机制
下面我们就具体来看管程的做法
管程是一种多线程
互斥访问共享资源的程序结构
它采用的是面向对象的方法
也就说把共享资源相关的PV操作
集中到一起从而简化
线程之间的同步控制
它保证的是任何一个时刻
最多只有一个线程
在执行管程代码
那从这句字面上解释呢
大家觉得管程好像
和我们的临界区是一样的
临界区也是允许一个线程
在临界区执行代码
管程也是这样
那这两者有什么区别呢
它的区别体现在下边这一条
正在管程中执行的线程
可以临时放弃管程的互斥访问
等待事件出现时恢复
这指什么意思呢
一个线程在临界区当中执行
它必须执行到它退出临界区
它才可能放弃临界区的互斥访问
而管程呢 允许我在执行的过程当中
临时放弃
那临时放弃之后呢
那其它的线程呢
就可以进到管程这个区域里头了
这种做法呢一种形象的比喻
像我们在赛车场
假定赛车场的赛道呢
为了安全我只允许
一辆跑车在上头跑
那它在跑的过程当中呢
它可能会加燃料
可能会更换零件
那在这个更换零件或者说
加燃料这个时间呢
它处于暂停的状态
这时候允许其他的赛车进入赛道
这是管程的一个基本比喻办法
假定它能干活了
那我们这时候
会需要做什么呢
管程的使用就是
我把我现在要同步的这些模块当中
收集共享的这些数据
然后编写对这些
共享数据的访问方法
有了这个之后
那在各处用的时候呢
它就不用在其它地方再做
同步互斥这些操作了
那怎么做到这一点呢
首先我们把这个区域里的代码
理解为我们的管程
如果说你把它
视为是一个临界区的话
那这时候呢
只需要在入口的地方
加一个互斥访问的锁就可以
管程也是一样
在这个地方入口队列里头呢
它只允许一个线程在管程内部执行
如果说在这个内部
没有其它共享数据的话
这时候呢就和我们的
临界区是完全一样的
任何一个线程
在进来的时候都在入口排队
允许只有一个线程进来
到它退出的时候
另外的线程可以进去
好 管程在这个临界区
基础上又加了一条
就是零个或者是多个条件变量
如果是零个的话
那就等同于一个临界区
如果是其中有一个以上的条件变量
那么这时候呢
就是我们这里管程所特有的了
它用来管理共享数据的并发访问
需要这些共享资源的时候
那么在这儿呢对应着
有相应的条件变量允许有了
那你才可以
使用中间这些互斥的操作
这是管程的基本思路
那这中间就用到一个条件变量
条件变量是什么呢
条件变量是管程内部的等待机制
进入管程的线程
因为资源占用而进入等待状态
那这时候呢它就等在条件变量上
每一个条件变量呢
表示一种等待原因
它对应着一个等待队列
有了这个之后 我们在上面
再附着两个相应的操作
一个是等待
那等待将自己堵塞在
等待队列当中 同时唤醒
另外一个者 或者说
放弃管程的互斥访问
后面这两种情况就是
允许另外一个线程进入管程
它自己呢等在这个
管程内部的等待队列上
这是我们这里的条件变量
还有一个操作呢是释放操作
这个释放操作呢
它将等待队列当中一个线程唤醒
如果说没有
这就相当于是一个空操作
好 有了关于条件变量
这种约定之后
那下面是它的实现
从这儿呢我们看到
它和我们前面讲的
信号量的实现呢很接近
它在这儿条件变量
是有一个整型变量和一个等待队列
只是说跟我们的信号量不同呢
条件变量的初值是0
而在信号量里头呢
它的初值是和你的资源数是一致的
然后对应着它有两个操作
Wait和Signal 等待和这里的释放
那等待在里面做什么呢 计数加1
好 那就相当于我正数表示其中
有线程处于等待状态
然后把它自己放到等待队列当中
释放管程的互斥访问权
然后执行调度
这实际上就说有了调度之后呢
我们就可以切换到另外的进程
或者是线程来执行
等到它回来的时候
它再去请求管程的访问权限
而释放呢
它都是放在这一个if语句里头
也就相当于如果说
这个条件不成立的话
这个操作相当于是空操作
好 条件成立也就说有另外的线程
等在这个条件变量上
它会做一些什么呢
好 把这个线程从等待队列里移出来
放到就绪队列里头 让它可以执行
然后把它的计算减1
也就说这时候等的这个
线程的数目呢少了一个
好 基于这个实现
那我们就可以来看
如何用管程来实现
生产者 消费者问题
这是呢 刚开始的初始化定义
两个条件变量一个入口等待队列
入口的锁
这和我们前面用信号量解决
生产者 消费者问题的设置呢很类似
然后在这管程内部的值呢
它当时是0
这个数呢用来表示
你写的缓冲区里的数据的数目
然后我们看这时候生产者
和消费者分别对应着一个函数
这是我们管程的封装作用
把它封装到这两个函数里头
其它地方要使用的时候呢
只需要调生成数据和读取数据
这两个函数就可以了
那这里头本质性的操作呢
是往缓冲区里写一个数据
并且把这个计数加1
消费者呢是从缓冲区里
读一个数据并且把计数减1
好 那我们在做这件事情之前
各自需要一些什么准备呢
首先我把它放到一个管程里头
这是由管程进入的申请和释放
那这是在管程内部
你可以理解为这两个函数
是构成我们管程的内部的代码
我申请到了管程的
互斥访问权之后
那我在这里头呢
我就可以往里写数据了吗
不是 我得先看你是不是有空地
那在这儿 就是去看是否有空地
如果说没有
就相当于我这里头
N个缓冲区里都有数据了
那么这时候呢
我就等待一个条件变量上
notfull 等在这个条件变量上
有空地之后呢
我再回来往里写数据
这边呢跟我们前面的
信号量做法类似的
在消费者这头呢
是读出一个数据之后
它就释放一个条件变量
说如果你在那里头有生产者
等在需要空闲缓冲区的情况
那么这时候它就把它唤醒了
那这时候有一个问题
我们用信号量解决的时候
我必须先检查缓冲区的状态
然后再申请互斥操作
那现在在管程里头你把它倒过来
这样就没问题了吗
实际上这里的原因在于
因为管程我在内部检查的时候
如果不成功我还可以放弃
管程的互斥访问权限
而在信号量那个地方实现里头
我进到那个临界区里头了
好 我已经占用了
对缓冲区的互斥访问
那这时候别人就再进不来了
因为我没有办法放弃
所以这是这两者之间的区别
也正是由于这个区别
我可以把这个检查空满的判断呢
放在管程的内部
跟这相对应
消费者呢读数据之前
需要检查是否缓冲区里头有数据
如果说没有
就这个计数为0
那么这时候呢
它有放弃管程的使用权
等在这个非空的这个条件变量上
好 一旦里头有数据之后
那有数据呢是生产者
写完数据之后它做释放操作
写完数据之后那这边释放
好 它出来再检查成功之后
它就可以往外读数据了
好 那这是呢我们用管程来实现
生产者消费者问题的解法
我们比较基于信号量
和管程的实现方案
我们可以看到在管程里头呢
它可以把PV操作都
集中到一个模块里头
从而呢简化和降低
同步机制的实现难度
在这里头我们
还有一个问题需要来讨论
管程里头条件变量
到底是内部的线程优先执行
还是正占用管程处于执行状态的
这个线程有更优先权限来执行
这两种不同的处理办法呢
对应到我们这里的两种不同管程
一种叫Hanson一种叫Hoare
这两种做法的区别是这样的
在Hanson管程里头呢
一个线程T1等待条件变量
那么它进入等待状态
这时候允许进程T2开始执行
在T2执行过程当中呢
等待的条件成立了
好 那这地方呢它在管程里头呢
这T2给了一个释放
允许x条件变量所对应的线程
可以开始运行
那在Hanson的做法里头呢
它释放完了之后
当前这个T2还会继续执行
一直到它放弃
管程的互斥访问权限
然后T1才恢复执行
这种做法呢
是当前正在执行的这个线程更优先
而Hoare的做法呢是倒过来
第一个线程执行到
等待条件变量的时候
进入等待状态
第二开始执行
这跟前面都是一样的
它们区别在于T2认为
T1等待的事件已经出现了
好 那这时候它唤醒T1
唤醒完了之后这时候
它立即放弃管程的互斥访问权
好 这时候T1马上开始执行
等它执行结束之后
T2再继续执行
如果说我们从通常的
优先的角度来讲
T1更优先是合理的
这个呢在我们现在的
基本原理介绍里头呢
Hoare做法
是更容易证明它的确定性
而第一种呢
它连续执行 它的效率会更高
在这儿呢就少了一次切换
所以这两种做法呢
目前在真实系统里头呢
一般用的是Hanson的做法
而在教科书里头呢
用的是Hoare的做法
基于这两种不同的做法
我们再来看
它在生产者消费者问题当中的体现
这是我们刚才生产者消费者
问题当中基于管程的实现代码
好 两种不同做法它们的区别在哪呢
在这个地方 一个是while一个是if
这两种区别怎么体现出来呢
在这个地方
条件变量等待的时候
这种释放呢它仅仅是一个提示
你回来之后你要重新去检查
这是while的含义
再次检查呢
实际上相当于我要再重新去排队
因为在前面那个释放操作里呢
释放完之后它还会再继续执行
有可能在这个执行过程当中呢
这个地方的状态是会有变化的
好 也正是由于这个原因
在Hanson的做法里头呢
当前正在执行那个线程优先级更高
好 那这个地方呢你需要重新来检查
所以这个地方是while
而在Hoare的做法里头呢
条件变量的释放操作
表示条件变量的立即释放
和放弃管程使用权
那这时候呢
它出来之后不再需要进行检查
而直接开始后续的执行
那这就是等待条件变量
那个线程它的优先级更高
这两种做法呢
明显是第一个效率更高
第二个呢效率会低一些
因为它多了一次切换
当然第二个行为的确定性
会更好一些
所以一个用在基本原理里头
一个是在实际系统里头