9.1背景知识
1、如果资源处理不当,可能会出现一些意想不到的情况,合作的风险
独立的线程:
不和其他线程共享资源或状态
确定性->输入状态决定结果
可重现->能够重现起始条件
调度顺序不重要
合作线程:
在多喝线程中共享状态
不确定性
不可重现(不可重复性)
这些不确定性和不可重复以意味着bug可能是间歇性发生的,也就是合作是有风险的。
2、为什么要合作
1)共享资源
资源是需要共享的,因为进程可能要访问同一个文件。
2)加速
通过并行和并发,可以提高系统的效率,实现更有效的资源的利用。相当于把一个大的任务,拆分成多个小的任务,每个任务通过并行的执行提高系统的性能。
3)模块化
在设计时将一个大的工作,变成一个小的工作,使之具有模块化,使系统便于扩展。
3、问题出现的原因
例子:
以上四条汇编指令的意思是:
1)把next_pid赋值给寄存器1(Reg1)
2)再把这个寄存器1存到了new_pid这个内存单元的去。此时new_pid就具有了next_pid这个值。
3)寄存器1加一操作。
4)完成next_pid的值增加了一个1的操作。
总的实现过程:
先把new_pid = next_pid,然后next_pid再加1.
但是,如果这时有两个进程,就会出现意想不到的情况:
问题产生的原因:
在第二次进程的上下文切换时候,进程1的寄存器恢复之后依然100的值,是的next的值无法更新称为102。最终产生了切换使得最终的结果不是想要的结果。这是一种典型的异常现象。
9.2一些概念part1
由于上述产生的异常现象(称之为竞态条件Race Condition),这就是为什么要引入同步互斥这些机制的原因,就是要解决这种不确定性的问题。
1、系统缺陷:结果依赖于并发执行或者事件的顺序/时间
- 不确定性
- 不可重现
2、怎样避免竞态?
让指令不被打断(比如上述的四条机器指令不被打断)
3、不被打断的方法:原子操作(Atomic Operation)—不可被打断操作
原子操作是指一次不存在任何中断或者失败的执行
- 该执行成功结束
- 或者根本没有执行
- 并且不应该发现任何部分执行的状态
实际上操作往往不是原子的
- 有些看上去是原子操作,实际不是
- 连x++这样简单的语句,实际上是由3条指令造成的
- 有时候甚至连条单条机器指令都不是原子的
例子:
所以需要后续的同步机制,确保或者是A赢或者是B赢。
4、一些基本概念
- 临界区(Critical section)
临界区是指进程中的一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会被执行的代码区域。简单来说,就是访问共享资源的那段代码就是临界区。
- 互斥(Mutual exclusion)
当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并且访问任何相同的共享资源。
- 死锁(Dead lock)
两个或以上的进程,在互相等待完成特定任务,而最终没法将自身任务进行下去。
- 饥饿(Starvation)
一个可执行的进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行
9.3一些概念part2
1、一个有趣的类比:
2、解决的方法和概念
3、更好的解决方法(轻量级)
但是由于进程上下文切换的原因,问题还是会存在。
如果只是将Note往前面提简单的挪动一下还是不会解决问题,变成谁都不会去买面包了。
9.4一些概念part3
1、再换一个方法
结果还是有问题的。
需要确保在任何情况下,只有一个进程在临界区中执行,其他的进程需要在外面等待。
一个更加合理的方案解析过程:
程序是有效的,但是导致代码不一样了。
最终的解决方案:
为每一个线程保护了一段“临界区”代码。使用临界区的思想,问题就可以较好的解决。其是讲前诉方法的一个抽象。
有了临界区的代码之后,就可以确保任何时候只有一个进程在临界区中执行,切其他进程在外面等待,知道临界区中的进程离开,其他进程中的一个会进入临界区去执行。这个是比较合理的一个实现。
9.5临界区
在临界区中执行所拥有的属性:
1、互斥:同一个时间临界区中最多存在一个线程
2、前进(Progress):如果一个线程想要进入临界区,那么它最终会成功,不会一直的死等。
3、有限等待:如果一个线程i处于入口区,那么在i的请求被接受之前,其他线程进入临界区的时间是有限制的。如果是无限等待,就会出现饥饿状态,是Progress前进状态的一种进一步补充。
4、忙等(可选属性):如果一个进程在等待进入临界区,那么在它可以进入之前会被挂起。
基于这些属性,设计一些方法对临界区进行保护:
方法一:禁用硬件中断
方法二:基于软件的解决方法
方法三:更高级的抽象(基于硬件原子操作的指令)
9.6禁用硬件中断
一、基本实现
没有中断,也就是没有了上下文切换,因此没有并发。
- 硬件将中断处理延迟到中断被启用之后
- 大多数现代计算机体系结构都提供指令来完成
进入临界区时禁用中断,离开临界区时开启中断。这个方法是可以解决问题的。
二、缺点
1、一旦中断被禁用,线程就无法被停止
2)整个系统都会为你停下来
3)可能导致其他线程处于饥饿状态
2、要是临界区可以任意长
无法限制响应中断所需的时间(可能存在硬件影响)
需要注意:
执行这种课屏蔽中断的指令,只是把自身的响应中断的能力屏蔽了,并不意味着也将其他cpu的响应中断能力屏蔽,所以其实其他的cpu还是可以继续产生中断,所以在多cpu的情况下是无法解决互斥问题的。
9.7基于软件的解决方案
一、思考方案
1、思考方案一:
某一个进程,它想进入临界区,其有一个顺序(次序),根据这个次序决定谁会进入这个临界区。
方法如下所示:
假设这个线程的次序是0,那么当turn=0时,才去继续下面的执行临界区代码,否者在while循环中一直打转。条件满足时,改变使得turn=1。
而进程1的代码也是类似的,只是while循环中的判断条件是不等于0,下面的turn=0.
这个程序的弊端是:
必须进程1执行一次临界区,进程0执行一次临界区,然后两个交替执行,才能保证两继续的执行。一旦其中的一个进程不愿意再做这个事情,那按照之前的属性,其他进程先进去就应该能够进去,但是在这种模式下,就无法完成这个前进的属性。
2、思考方案二:
前面表示了一个turn是不够表示,所以接下来使用一个小数组flag[2]来表示这个进程是否想进入临界区。
- flag[0] = 1 //表示进程0想进入临界区执行
- flag[0] = 1 //表示进程1想进入临界区执行
方法1如下所示:
但是这个代码是有问题的,不能满足互斥这个属性。
因为在初始的时候,两个进程都不会想进入临界区,所以两个flag都会赋值为0,表面没有这个需求。这样就是的两个进程都会跳出这个循环,然后都会将直接复制为,想要进入临界区,也就出现了多买面包的想象。
方法2如下所示:
满足了互斥,但是倘若两个线程都赋值了1,出现上下文切换的时候,都无法跳出这个循环,也就是出现是死锁的情况。
可见,互斥的解决并没有想象的那么简单~~~
二、正确实现
1、正确的接法
将以上的两种思考都综合起来使用。三个变量共同的作用。
算法如下:
该算法可以满足互斥,前进和有限等待三个属性。
反证法来证明:
假定,现在两个进程都进入了临界区,都在执行临界区代码,但是turn只是一个值的,所以总会有一个线程会跳出循环的。
2、另外一种算法
所需的变量空间相同,但是更加的复杂
三、拓展
1、n进程解决方法1 (E&M算法)
除了了针对两个进程之外,还可以拓展到n个进程如何保证互斥。
大致的思路:
对于进程i而言,对于其前面的进程而言,如果有进程想进入临界区,或者已经进入了临界区,那么i进程就必须等待。而对于i进程后面的进程,必须要等待i进程执行之后在进入临界区。这样就可以通过一个循环的方式完成n个进程有序的进入临界区。
2、n进程解决方法2(Bakery算法)
大致思路如下:
四、总结
1)即使是针对两个进程的解决竞态的实现还是比较复杂的。
2)需要忙等待,浪费cpu时间。
3)没有硬件包装的情况下无真正的软件解决方案。对硬件的1需求比较低(只需要load操作和store是原子操作即可)
9.8更高级的抽象 — 基于原子操作
软件的处理比较复杂有没有更加简单的实现方法?
一、基础
使用了一些特殊的操作:
1、Test-and-Set测试和置位
(一条机器指令,但是完成了读写操作两条指令的功能)
从内存中读取值
测试该值是否为1(然后返回真或假)
内存值设置为1
2、交换
(交换内存中的两个值)
只要计算机系统提供了这两条的其中一条指令就可以很容易的完成互斥的问题。
二、解决的方法
1、Test-and-Set方式
解决忙等的情况:先让其睡眠,在加一步唤醒操作
两者的区别:
- 忙等:不需要上下文切换,但是利用率低,适用与临界区执行时间短的情况。
- 不忙等:需要上下文切换,上下文切换开销比较大大,适用于临界区很长,远远大于上下文切换所需要的开销。
2、交换的方式
解析:
1)当一个进程想要进入临界区的时候,key=1,而且lock的初始值是0,所以当执行到while循环的时候,由于执行了交换,交换执行的过程不会被打断进行上下文切换的操作,而后lock的变成了1,而key变成了0.所以会退出循环,执行临界区的代码。
2)需要注意的是,当进入临界区的时候,load已经是1,当其他进程进入临界区执行的时候,load是1,而key也是1,交换之后还是1,一直会循环的等待,进入不了临界区。知道进入临界区的进程,退出临界区之后,完成一个将load变成0的操作。其他等待的进程才会继续执行exchange。
三、采用这种原子操作的特点
1、优点
1)简单并且容易证明
2)适用于单处理器或者共享主存的多处理器中任意数量的进程
3)可以很容易拓展n个进程,可以用于支持多临界区
4)开销比较小
2、缺点
1)忙等待消耗处理器时间
2)当进程离开临界区并且多个进程在等待的时候可能导致饥饿现象
3)出现死锁的情况(例子:如果一个低优先级的进程拥有临界区并且一个高优先级进程也需求,那么高优先级进程会获得处理器并且等待临界区 — 需要用优先级反转的方式进行处理)
四、总结
1、锁是更高级的编程抽象
- 互斥可以使用锁来实现
- 通常需要一定等级的硬件支持
2、常用的三种实现方法
- 禁用中断(仅限于单处理器)
- 软件方法(复杂)
- 原子操作指令(但处理器或多处理器均可)—更常用
3、可选的实现内容
- 有忙等待
- 无忙等待
参考链接:https://www.bilibili.com/video/av6538245