0 00:00:00,000 --> 00:00:07,200 1 00:00:07,240 --> 00:00:09,440 下面我们来介绍管程 2 00:00:09,480 --> 00:00:10,320 这是我们这里 3 00:00:10,360 --> 00:00:14,560 今天说到的第二种同步方法 4 00:00:14,600 --> 00:00:16,200 我们在刚才的 5 00:00:16,240 --> 00:00:18,200 同步方法介绍当中呢 6 00:00:18,240 --> 00:00:19,840 有了信号量 7 00:00:19,880 --> 00:00:23,280 它使得我们在操作系统参与下 8 00:00:23,320 --> 00:00:25,640 我们处理临界区呢 9 00:00:25,680 --> 00:00:28,120 比原来会变得更方便 10 00:00:28,160 --> 00:00:30,600 好 那接下来我们介绍的管程呢 11 00:00:30,640 --> 00:00:34,720 实际上是想改进信号量 12 00:00:34,760 --> 00:00:37,880 在处理临界区的时候一些麻烦 13 00:00:37,920 --> 00:00:40,680 比如说在我们刚才看到的 14 00:00:40,720 --> 00:00:43,240 生产者消费者问题当中 15 00:00:43,280 --> 00:00:45,160 信号量的PV操作 16 00:00:45,200 --> 00:00:47,560 是分散在生产者和消费者 17 00:00:47,600 --> 00:00:49,960 两个不同的进程当中 18 00:00:50,000 --> 00:00:51,440 在这种情况下 19 00:00:51,480 --> 00:00:54,040 我们PV操作的配对 20 00:00:54,080 --> 00:00:56,320 是比较困难的 21 00:00:56,360 --> 00:00:58,600 好 我们试图把这些 22 00:00:58,640 --> 00:01:00,440 配对的PV操作 23 00:01:00,480 --> 00:01:01,800 集中到一起 24 00:01:01,840 --> 00:01:04,080 这就是我们这里说到的管程 25 00:01:04,120 --> 00:01:08,320 它也是一种并发程序的编程方法 26 00:01:08,360 --> 00:01:10,040 那为了支持它呢 27 00:01:10,080 --> 00:01:11,400 我们在底下呢 28 00:01:11,440 --> 00:01:14,040 还得再加上一个条件变量 29 00:01:14,080 --> 00:01:15,560 这是在管程内部呢 30 00:01:15,600 --> 00:01:18,840 使用的一种同步机制 31 00:01:18,880 --> 00:01:23,080 下面我们就具体来看管程的做法 32 00:01:23,120 --> 00:01:25,640 管程是一种多线程 33 00:01:25,680 --> 00:01:29,680 互斥访问共享资源的程序结构 34 00:01:29,720 --> 00:01:32,200 它采用的是面向对象的方法 35 00:01:32,240 --> 00:01:35,000 也就说把共享资源相关的PV操作 36 00:01:35,040 --> 00:01:37,200 集中到一起从而简化 37 00:01:37,240 --> 00:01:40,680 线程之间的同步控制 38 00:01:40,720 --> 00:01:42,960 它保证的是任何一个时刻 39 00:01:43,000 --> 00:01:45,280 最多只有一个线程 40 00:01:45,320 --> 00:01:47,640 在执行管程代码 41 00:01:47,680 --> 00:01:50,440 那从这句字面上解释呢 42 00:01:50,480 --> 00:01:52,440 大家觉得管程好像 43 00:01:52,480 --> 00:01:54,360 和我们的临界区是一样的 44 00:01:54,400 --> 00:01:57,400 临界区也是允许一个线程 45 00:01:57,440 --> 00:01:59,880 在临界区执行代码 46 00:01:59,920 --> 00:02:01,480 管程也是这样 47 00:02:01,520 --> 00:02:03,200 那这两者有什么区别呢 48 00:02:03,240 --> 00:02:05,640 它的区别体现在下边这一条 49 00:02:05,680 --> 00:02:08,760 正在管程中执行的线程 50 00:02:08,800 --> 00:02:12,640 可以临时放弃管程的互斥访问 51 00:02:12,680 --> 00:02:15,400 等待事件出现时恢复 52 00:02:15,440 --> 00:02:16,720 这指什么意思呢 53 00:02:16,760 --> 00:02:18,720 一个线程在临界区当中执行 54 00:02:18,760 --> 00:02:22,520 它必须执行到它退出临界区 55 00:02:22,560 --> 00:02:26,360 它才可能放弃临界区的互斥访问 56 00:02:26,400 --> 00:02:29,280 而管程呢 允许我在执行的过程当中 57 00:02:29,320 --> 00:02:30,880 临时放弃 58 00:02:30,920 --> 00:02:32,440 那临时放弃之后呢 59 00:02:32,480 --> 00:02:34,560 那其它的线程呢 60 00:02:34,600 --> 00:02:37,640 就可以进到管程这个区域里头了 61 00:02:37,680 --> 00:02:39,560 这种做法呢一种形象的比喻 62 00:02:39,600 --> 00:02:41,680 像我们在赛车场 63 00:02:41,720 --> 00:02:43,080 假定赛车场的赛道呢 64 00:02:43,120 --> 00:02:44,240 为了安全我只允许 65 00:02:44,280 --> 00:02:46,920 一辆跑车在上头跑 66 00:02:46,960 --> 00:02:48,560 那它在跑的过程当中呢 67 00:02:48,600 --> 00:02:49,840 它可能会加燃料 68 00:02:49,880 --> 00:02:51,440 可能会更换零件 69 00:02:51,480 --> 00:02:54,080 那在这个更换零件或者说 70 00:02:54,120 --> 00:02:55,640 加燃料这个时间呢 71 00:02:55,680 --> 00:02:57,440 它处于暂停的状态 72 00:02:57,480 --> 00:03:01,520 这时候允许其他的赛车进入赛道 73 00:03:01,560 --> 00:03:04,680 这是管程的一个基本比喻办法 74 00:03:04,720 --> 00:03:06,400 假定它能干活了 75 00:03:06,440 --> 00:03:07,160 那我们这时候 76 00:03:07,200 --> 00:03:08,520 会需要做什么呢 77 00:03:08,560 --> 00:03:09,800 管程的使用就是 78 00:03:09,840 --> 00:03:13,520 我把我现在要同步的这些模块当中 79 00:03:13,560 --> 00:03:16,880 收集共享的这些数据 80 00:03:16,920 --> 00:03:18,480 然后编写对这些 81 00:03:18,520 --> 00:03:21,720 共享数据的访问方法 82 00:03:21,760 --> 00:03:22,960 有了这个之后 83 00:03:23,000 --> 00:03:24,040 那在各处用的时候呢 84 00:03:24,080 --> 00:03:26,640 它就不用在其它地方再做 85 00:03:26,680 --> 00:03:30,040 同步互斥这些操作了 86 00:03:30,080 --> 00:03:33,360 那怎么做到这一点呢 87 00:03:33,400 --> 00:03:36,240 首先我们把这个区域里的代码 88 00:03:36,280 --> 00:03:38,360 理解为我们的管程 89 00:03:38,400 --> 00:03:39,920 如果说你把它 90 00:03:39,960 --> 00:03:41,680 视为是一个临界区的话 91 00:03:41,720 --> 00:03:42,520 那这时候呢 92 00:03:42,560 --> 00:03:44,080 只需要在入口的地方 93 00:03:44,120 --> 00:03:47,080 加一个互斥访问的锁就可以 94 00:03:47,120 --> 00:03:49,480 管程也是一样 95 00:03:49,520 --> 00:03:51,800 在这个地方入口队列里头呢 96 00:03:51,840 --> 00:03:55,480 它只允许一个线程在管程内部执行 97 00:03:55,520 --> 00:03:56,800 如果说在这个内部 98 00:03:56,840 --> 00:03:59,240 没有其它共享数据的话 99 00:03:59,280 --> 00:04:02,400 这时候呢就和我们的 100 00:04:02,440 --> 00:04:04,680 临界区是完全一样的 101 00:04:04,720 --> 00:04:05,880 任何一个线程 102 00:04:05,920 --> 00:04:08,560 在进来的时候都在入口排队 103 00:04:08,600 --> 00:04:10,080 允许只有一个线程进来 104 00:04:10,120 --> 00:04:11,000 到它退出的时候 105 00:04:11,040 --> 00:04:12,760 另外的线程可以进去 106 00:04:12,800 --> 00:04:14,960 好 管程在这个临界区 107 00:04:15,000 --> 00:04:16,720 基础上又加了一条 108 00:04:16,760 --> 00:04:20,880 就是零个或者是多个条件变量 109 00:04:20,920 --> 00:04:21,840 如果是零个的话 110 00:04:21,880 --> 00:04:23,640 那就等同于一个临界区 111 00:04:23,680 --> 00:04:26,520 如果是其中有一个以上的条件变量 112 00:04:26,560 --> 00:04:27,520 那么这时候呢 113 00:04:27,560 --> 00:04:30,080 就是我们这里管程所特有的了 114 00:04:30,120 --> 00:04:33,080 它用来管理共享数据的并发访问 115 00:04:33,120 --> 00:04:35,240 需要这些共享资源的时候 116 00:04:35,280 --> 00:04:36,280 那么在这儿呢对应着 117 00:04:36,320 --> 00:04:38,400 有相应的条件变量允许有了 118 00:04:38,440 --> 00:04:39,840 那你才可以 119 00:04:39,880 --> 00:04:43,560 使用中间这些互斥的操作 120 00:04:43,600 --> 00:04:47,520 这是管程的基本思路 121 00:04:47,560 --> 00:04:49,720 那这中间就用到一个条件变量 122 00:04:49,760 --> 00:04:51,440 条件变量是什么呢 123 00:04:51,480 --> 00:04:55,320 条件变量是管程内部的等待机制 124 00:04:55,360 --> 00:04:58,040 进入管程的线程 125 00:04:58,080 --> 00:05:01,080 因为资源占用而进入等待状态 126 00:05:01,120 --> 00:05:04,120 那这时候呢它就等在条件变量上 127 00:05:04,160 --> 00:05:05,960 每一个条件变量呢 128 00:05:06,000 --> 00:05:07,520 表示一种等待原因 129 00:05:07,560 --> 00:05:10,440 它对应着一个等待队列 130 00:05:10,480 --> 00:05:13,200 有了这个之后 我们在上面 131 00:05:13,240 --> 00:05:16,240 再附着两个相应的操作 132 00:05:16,280 --> 00:05:17,720 一个是等待 133 00:05:17,760 --> 00:05:21,040 那等待将自己堵塞在 134 00:05:21,080 --> 00:05:23,680 等待队列当中 同时唤醒 135 00:05:23,720 --> 00:05:26,040 另外一个者 或者说 136 00:05:26,080 --> 00:05:28,600 放弃管程的互斥访问 137 00:05:28,640 --> 00:05:30,040 后面这两种情况就是 138 00:05:30,080 --> 00:05:33,320 允许另外一个线程进入管程 139 00:05:33,360 --> 00:05:35,160 它自己呢等在这个 140 00:05:35,200 --> 00:05:37,240 管程内部的等待队列上 141 00:05:37,280 --> 00:05:39,400 这是我们这里的条件变量 142 00:05:39,440 --> 00:05:42,600 还有一个操作呢是释放操作 143 00:05:42,640 --> 00:05:44,640 这个释放操作呢 144 00:05:44,680 --> 00:05:48,120 它将等待队列当中一个线程唤醒 145 00:05:48,160 --> 00:05:49,760 如果说没有 146 00:05:49,800 --> 00:05:52,720 这就相当于是一个空操作 147 00:05:52,760 --> 00:05:55,040 好 有了关于条件变量 148 00:05:55,080 --> 00:05:56,120 这种约定之后 149 00:05:56,160 --> 00:05:59,040 那下面是它的实现 150 00:05:59,080 --> 00:06:00,200 从这儿呢我们看到 151 00:06:00,240 --> 00:06:01,120 它和我们前面讲的 152 00:06:01,160 --> 00:06:03,360 信号量的实现呢很接近 153 00:06:03,400 --> 00:06:05,520 它在这儿条件变量 154 00:06:05,560 --> 00:06:09,640 是有一个整型变量和一个等待队列 155 00:06:09,680 --> 00:06:12,040 只是说跟我们的信号量不同呢 156 00:06:12,080 --> 00:06:14,600 条件变量的初值是0 157 00:06:14,640 --> 00:06:16,840 而在信号量里头呢 158 00:06:16,880 --> 00:06:20,800 它的初值是和你的资源数是一致的 159 00:06:20,840 --> 00:06:23,160 然后对应着它有两个操作 160 00:06:23,200 --> 00:06:27,720 Wait和Signal 等待和这里的释放 161 00:06:27,760 --> 00:06:31,040 那等待在里面做什么呢 计数加1 162 00:06:31,080 --> 00:06:33,640 好 那就相当于我正数表示其中 163 00:06:33,680 --> 00:06:36,920 有线程处于等待状态 164 00:06:36,960 --> 00:06:41,480 然后把它自己放到等待队列当中 165 00:06:41,520 --> 00:06:45,040 释放管程的互斥访问权 166 00:06:45,080 --> 00:06:46,560 然后执行调度 167 00:06:46,600 --> 00:06:47,960 这实际上就说有了调度之后呢 168 00:06:48,000 --> 00:06:50,880 我们就可以切换到另外的进程 169 00:06:50,920 --> 00:06:52,920 或者是线程来执行 170 00:06:52,960 --> 00:06:54,280 等到它回来的时候 171 00:06:54,320 --> 00:06:59,120 它再去请求管程的访问权限 172 00:06:59,160 --> 00:07:01,600 而释放呢 173 00:07:01,640 --> 00:07:04,360 它都是放在这一个if语句里头 174 00:07:04,400 --> 00:07:05,560 也就相当于如果说 175 00:07:05,600 --> 00:07:06,920 这个条件不成立的话 176 00:07:06,960 --> 00:07:09,560 这个操作相当于是空操作 177 00:07:09,600 --> 00:07:12,040 好 条件成立也就说有另外的线程 178 00:07:12,080 --> 00:07:14,080 等在这个条件变量上 179 00:07:14,120 --> 00:07:16,600 它会做一些什么呢 180 00:07:16,640 --> 00:07:20,200 好 把这个线程从等待队列里移出来 181 00:07:20,240 --> 00:07:23,720 放到就绪队列里头 让它可以执行 182 00:07:23,760 --> 00:07:26,080 然后把它的计算减1 183 00:07:26,120 --> 00:07:27,040 也就说这时候等的这个 184 00:07:27,080 --> 00:07:30,040 线程的数目呢少了一个 185 00:07:30,080 --> 00:07:31,440 好 基于这个实现 186 00:07:31,480 --> 00:07:33,000 那我们就可以来看 187 00:07:33,040 --> 00:07:35,480 如何用管程来实现 188 00:07:35,520 --> 00:07:37,680 生产者 消费者问题 189 00:07:37,720 --> 00:07:42,400 这是呢 刚开始的初始化定义 190 00:07:42,440 --> 00:07:46,360 两个条件变量一个入口等待队列 191 00:07:46,400 --> 00:07:47,600 入口的锁 192 00:07:47,640 --> 00:07:50,760 这和我们前面用信号量解决 193 00:07:50,800 --> 00:07:55,040 生产者 消费者问题的设置呢很类似 194 00:07:55,080 --> 00:07:58,080 然后在这管程内部的值呢 195 00:07:58,120 --> 00:07:59,720 它当时是0 196 00:07:59,760 --> 00:08:01,160 这个数呢用来表示 197 00:08:01,200 --> 00:08:04,760 你写的缓冲区里的数据的数目 198 00:08:04,800 --> 00:08:07,480 然后我们看这时候生产者 199 00:08:07,520 --> 00:08:09,600 和消费者分别对应着一个函数 200 00:08:09,640 --> 00:08:11,960 这是我们管程的封装作用 201 00:08:12,000 --> 00:08:14,160 把它封装到这两个函数里头 202 00:08:14,200 --> 00:08:15,520 其它地方要使用的时候呢 203 00:08:15,560 --> 00:08:18,840 只需要调生成数据和读取数据 204 00:08:18,880 --> 00:08:20,880 这两个函数就可以了 205 00:08:20,920 --> 00:08:22,640 那这里头本质性的操作呢 206 00:08:22,680 --> 00:08:25,440 是往缓冲区里写一个数据 207 00:08:25,480 --> 00:08:28,240 并且把这个计数加1 208 00:08:28,280 --> 00:08:30,480 消费者呢是从缓冲区里 209 00:08:30,520 --> 00:08:33,400 读一个数据并且把计数减1 210 00:08:33,440 --> 00:08:36,200 好 那我们在做这件事情之前 211 00:08:36,240 --> 00:08:39,640 各自需要一些什么准备呢 212 00:08:39,680 --> 00:08:42,280 首先我把它放到一个管程里头 213 00:08:42,320 --> 00:08:46,480 这是由管程进入的申请和释放 214 00:08:46,520 --> 00:08:48,000 那这是在管程内部 215 00:08:48,040 --> 00:08:50,360 你可以理解为这两个函数 216 00:08:50,400 --> 00:08:54,640 是构成我们管程的内部的代码 217 00:08:54,680 --> 00:08:57,160 我申请到了管程的 218 00:08:57,200 --> 00:08:58,920 互斥访问权之后 219 00:08:58,960 --> 00:08:59,600 那我在这里头呢 220 00:08:59,640 --> 00:09:00,840 我就可以往里写数据了吗 221 00:09:00,880 --> 00:09:04,320 不是 我得先看你是不是有空地 222 00:09:04,360 --> 00:09:07,600 那在这儿 就是去看是否有空地 223 00:09:07,640 --> 00:09:09,960 如果说没有 224 00:09:10,000 --> 00:09:11,600 就相当于我这里头 225 00:09:11,640 --> 00:09:14,040 N个缓冲区里都有数据了 226 00:09:14,080 --> 00:09:15,000 那么这时候呢 227 00:09:15,040 --> 00:09:17,560 我就等待一个条件变量上 228 00:09:17,600 --> 00:09:20,760 notfull 等在这个条件变量上 229 00:09:20,800 --> 00:09:22,000 有空地之后呢 230 00:09:22,040 --> 00:09:24,640 我再回来往里写数据 231 00:09:24,680 --> 00:09:26,480 这边呢跟我们前面的 232 00:09:26,520 --> 00:09:28,320 信号量做法类似的 233 00:09:28,360 --> 00:09:29,960 在消费者这头呢 234 00:09:30,000 --> 00:09:31,120 是读出一个数据之后 235 00:09:31,160 --> 00:09:34,960 它就释放一个条件变量 236 00:09:35,000 --> 00:09:38,000 说如果你在那里头有生产者 237 00:09:38,040 --> 00:09:42,160 等在需要空闲缓冲区的情况 238 00:09:42,200 --> 00:09:44,720 那么这时候它就把它唤醒了 239 00:09:44,760 --> 00:09:46,320 那这时候有一个问题 240 00:09:46,360 --> 00:09:48,920 我们用信号量解决的时候 241 00:09:48,960 --> 00:09:52,120 我必须先检查缓冲区的状态 242 00:09:52,160 --> 00:09:54,520 然后再申请互斥操作 243 00:09:54,560 --> 00:09:57,840 那现在在管程里头你把它倒过来 244 00:09:57,880 --> 00:10:01,120 这样就没问题了吗 245 00:10:01,160 --> 00:10:03,080 实际上这里的原因在于 246 00:10:03,120 --> 00:10:05,280 因为管程我在内部检查的时候 247 00:10:05,320 --> 00:10:08,080 如果不成功我还可以放弃 248 00:10:08,120 --> 00:10:11,040 管程的互斥访问权限 249 00:10:11,080 --> 00:10:14,080 而在信号量那个地方实现里头 250 00:10:14,120 --> 00:10:16,720 我进到那个临界区里头了 251 00:10:16,760 --> 00:10:18,600 好 我已经占用了 252 00:10:18,640 --> 00:10:20,920 对缓冲区的互斥访问 253 00:10:20,960 --> 00:10:22,760 那这时候别人就再进不来了 254 00:10:22,800 --> 00:10:24,080 因为我没有办法放弃 255 00:10:24,120 --> 00:10:26,840 所以这是这两者之间的区别 256 00:10:26,880 --> 00:10:28,160 也正是由于这个区别 257 00:10:28,200 --> 00:10:31,600 我可以把这个检查空满的判断呢 258 00:10:31,640 --> 00:10:34,680 放在管程的内部 259 00:10:34,720 --> 00:10:37,360 跟这相对应 260 00:10:37,400 --> 00:10:39,120 消费者呢读数据之前 261 00:10:39,160 --> 00:10:42,320 需要检查是否缓冲区里头有数据 262 00:10:42,360 --> 00:10:43,680 如果说没有 263 00:10:43,720 --> 00:10:45,280 就这个计数为0 264 00:10:45,320 --> 00:10:46,320 那么这时候呢 265 00:10:46,360 --> 00:10:49,040 它有放弃管程的使用权 266 00:10:49,080 --> 00:10:55,040 等在这个非空的这个条件变量上 267 00:10:55,080 --> 00:10:56,760 好 一旦里头有数据之后 268 00:10:56,800 --> 00:10:58,680 那有数据呢是生产者 269 00:10:58,720 --> 00:11:01,960 写完数据之后它做释放操作 270 00:11:02,000 --> 00:11:03,920 写完数据之后那这边释放 271 00:11:03,960 --> 00:11:06,280 好 它出来再检查成功之后 272 00:11:06,320 --> 00:11:08,720 它就可以往外读数据了 273 00:11:08,760 --> 00:11:11,960 好 那这是呢我们用管程来实现 274 00:11:12,000 --> 00:11:15,320 生产者消费者问题的解法 275 00:11:15,360 --> 00:11:18,280 我们比较基于信号量 276 00:11:18,320 --> 00:11:21,000 和管程的实现方案 277 00:11:21,040 --> 00:11:23,240 我们可以看到在管程里头呢 278 00:11:23,280 --> 00:11:25,720 它可以把PV操作都 279 00:11:25,760 --> 00:11:28,520 集中到一个模块里头 280 00:11:28,560 --> 00:11:32,080 从而呢简化和降低 281 00:11:32,120 --> 00:11:37,200 同步机制的实现难度 282 00:11:37,240 --> 00:11:37,880 在这里头我们 283 00:11:37,920 --> 00:11:39,920 还有一个问题需要来讨论 284 00:11:39,960 --> 00:11:43,040 管程里头条件变量 285 00:11:43,080 --> 00:11:46,400 到底是内部的线程优先执行 286 00:11:46,440 --> 00:11:49,720 还是正占用管程处于执行状态的 287 00:11:49,760 --> 00:11:54,400 这个线程有更优先权限来执行 288 00:11:54,440 --> 00:11:56,160 这两种不同的处理办法呢 289 00:11:56,200 --> 00:11:58,480 对应到我们这里的两种不同管程 290 00:11:58,520 --> 00:12:02,200 一种叫Hanson一种叫Hoare 291 00:12:02,240 --> 00:12:05,920 这两种做法的区别是这样的 292 00:12:05,960 --> 00:12:08,320 在Hanson管程里头呢 293 00:12:08,360 --> 00:12:12,000 一个线程T1等待条件变量 294 00:12:12,040 --> 00:12:13,840 那么它进入等待状态 295 00:12:13,880 --> 00:12:17,160 这时候允许进程T2开始执行 296 00:12:17,200 --> 00:12:19,520 在T2执行过程当中呢 297 00:12:19,560 --> 00:12:21,560 等待的条件成立了 298 00:12:21,600 --> 00:12:23,840 好 那这地方呢它在管程里头呢 299 00:12:23,880 --> 00:12:25,960 这T2给了一个释放 300 00:12:26,000 --> 00:12:30,920 允许x条件变量所对应的线程 301 00:12:30,960 --> 00:12:32,840 可以开始运行 302 00:12:32,880 --> 00:12:35,480 那在Hanson的做法里头呢 303 00:12:35,520 --> 00:12:36,760 它释放完了之后 304 00:12:36,800 --> 00:12:39,160 当前这个T2还会继续执行 305 00:12:39,200 --> 00:12:41,680 一直到它放弃 306 00:12:41,720 --> 00:12:44,320 管程的互斥访问权限 307 00:12:44,360 --> 00:12:48,440 然后T1才恢复执行 308 00:12:48,480 --> 00:12:49,280 这种做法呢 309 00:12:49,320 --> 00:12:53,960 是当前正在执行的这个线程更优先 310 00:12:54,000 --> 00:12:58,080 而Hoare的做法呢是倒过来 311 00:12:58,120 --> 00:13:00,200 第一个线程执行到 312 00:13:00,240 --> 00:13:02,120 等待条件变量的时候 313 00:13:02,160 --> 00:13:03,000 进入等待状态 314 00:13:03,040 --> 00:13:04,440 第二开始执行 315 00:13:04,480 --> 00:13:06,000 这跟前面都是一样的 316 00:13:06,040 --> 00:13:09,040 它们区别在于T2认为 317 00:13:09,080 --> 00:13:11,560 T1等待的事件已经出现了 318 00:13:11,600 --> 00:13:14,960 好 那这时候它唤醒T1 319 00:13:15,000 --> 00:13:16,040 唤醒完了之后这时候 320 00:13:16,080 --> 00:13:19,600 它立即放弃管程的互斥访问权 321 00:13:19,640 --> 00:13:23,040 好 这时候T1马上开始执行 322 00:13:23,080 --> 00:13:24,720 等它执行结束之后 323 00:13:24,760 --> 00:13:27,360 T2再继续执行 324 00:13:27,400 --> 00:13:29,400 如果说我们从通常的 325 00:13:29,440 --> 00:13:32,080 优先的角度来讲 326 00:13:32,120 --> 00:13:34,600 T1更优先是合理的 327 00:13:34,640 --> 00:13:37,520 这个呢在我们现在的 328 00:13:37,560 --> 00:13:39,280 基本原理介绍里头呢 329 00:13:39,320 --> 00:13:40,240 Hoare做法 330 00:13:40,280 --> 00:13:42,760 是更容易证明它的确定性 331 00:13:42,800 --> 00:13:44,520 而第一种呢 332 00:13:44,560 --> 00:13:47,520 它连续执行 它的效率会更高 333 00:13:47,560 --> 00:13:49,240 在这儿呢就少了一次切换 334 00:13:49,280 --> 00:13:50,880 所以这两种做法呢 335 00:13:50,920 --> 00:13:52,840 目前在真实系统里头呢 336 00:13:52,880 --> 00:13:55,560 一般用的是Hanson的做法 337 00:13:55,600 --> 00:13:58,160 而在教科书里头呢 338 00:13:58,200 --> 00:14:00,440 用的是Hoare的做法 339 00:14:00,480 --> 00:14:02,480 基于这两种不同的做法 340 00:14:02,520 --> 00:14:04,840 我们再来看 341 00:14:04,880 --> 00:14:08,880 它在生产者消费者问题当中的体现 342 00:14:08,920 --> 00:14:11,400 这是我们刚才生产者消费者 343 00:14:11,440 --> 00:14:15,200 问题当中基于管程的实现代码 344 00:14:15,240 --> 00:14:18,600 好 两种不同做法它们的区别在哪呢 345 00:14:18,640 --> 00:14:22,640 在这个地方 一个是while一个是if 346 00:14:22,680 --> 00:14:26,000 这两种区别怎么体现出来呢 347 00:14:26,040 --> 00:14:27,920 在这个地方 348 00:14:27,960 --> 00:14:30,040 条件变量等待的时候 349 00:14:30,080 --> 00:14:33,360 这种释放呢它仅仅是一个提示 350 00:14:33,400 --> 00:14:35,920 你回来之后你要重新去检查 351 00:14:35,960 --> 00:14:37,840 这是while的含义 352 00:14:37,880 --> 00:14:38,960 再次检查呢 353 00:14:39,000 --> 00:14:43,040 实际上相当于我要再重新去排队 354 00:14:43,080 --> 00:14:45,440 因为在前面那个释放操作里呢 355 00:14:45,480 --> 00:14:47,640 释放完之后它还会再继续执行 356 00:14:47,680 --> 00:14:49,520 有可能在这个执行过程当中呢 357 00:14:49,560 --> 00:14:51,880 这个地方的状态是会有变化的 358 00:14:51,920 --> 00:14:54,360 好 也正是由于这个原因 359 00:14:54,400 --> 00:14:56,560 在Hanson的做法里头呢 360 00:14:56,600 --> 00:15:00,160 当前正在执行那个线程优先级更高 361 00:15:00,200 --> 00:15:02,400 好 那这个地方呢你需要重新来检查 362 00:15:02,440 --> 00:15:04,480 所以这个地方是while 363 00:15:04,520 --> 00:15:07,240 而在Hoare的做法里头呢 364 00:15:07,280 --> 00:15:08,960 条件变量的释放操作 365 00:15:09,000 --> 00:15:11,200 表示条件变量的立即释放 366 00:15:11,240 --> 00:15:13,920 和放弃管程使用权 367 00:15:13,960 --> 00:15:15,440 那这时候呢 368 00:15:15,480 --> 00:15:17,280 它出来之后不再需要进行检查 369 00:15:17,320 --> 00:15:19,960 而直接开始后续的执行 370 00:15:20,000 --> 00:15:21,840 那这就是等待条件变量 371 00:15:21,880 --> 00:15:24,360 那个线程它的优先级更高 372 00:15:24,400 --> 00:15:26,000 这两种做法呢 373 00:15:26,040 --> 00:15:28,000 明显是第一个效率更高 374 00:15:28,040 --> 00:15:29,760 第二个呢效率会低一些 375 00:15:29,800 --> 00:15:31,640 因为它多了一次切换 376 00:15:31,680 --> 00:15:34,000 当然第二个行为的确定性 377 00:15:34,040 --> 00:15:35,760 会更好一些 378 00:15:35,800 --> 00:15:38,000 所以一个用在基本原理里头 379 00:15:38,040 --> 00:15:39,760 一个是在实际系统里头 380 00:15:39,800 --> 00:15:39,840