我这儿还用着呢,你先等会吧。
并发的出现是为了提高系统的效率,但是由此产生的问题却也不少。
我们知道,并发的程序是交叉执行的,随机性强,故而无法确定程序执行到何处时会转去执行另一个进程的任务,这一点和顺序程序有着根本性的不同。但并发程序却也不仅仅是针对多个程序而言的外部并发性,同时也针队一个程序中多条指令而言的内部并发性。
并发程序
从上述种种原因,我们可以看出,并发程序每次运行的结果不能保证都相同,这是由于并发程序的非封闭性造成的,而这也是我们需要解决的问题——只有可在现的结果才是正确的。
如何才能保持程序的可再现性呢?
我们说,当两段程序之间的读写集合互不相交,其写集合也互不相交时,就能保证可再现性。
进程互斥
进程互斥在并发编程时是特别需要注意的,否则就会因为并发程序的非封闭性而造成无可挽救的损失。
进程互斥是进程之间所发生的一种间接相互作用,是进程本身所不希望的,也是运行进程感受不到的。
在继续向下讲述前,先解释几项概念:
-
共享变量
多个进程均需访问的变量。
可以近似的理解为高级语言中的全局变量或静态变量(因为这两种变量类型是多个程序段或函数均可访问的)。
另外需要注意:共享变量既可能属于操作系统空间,也可能属于用户进程空间。
-
临界区
访问共享变量的程序段。 -
进程互斥
多个进程不能同时进入关于同一组共享变量的临界区,否则可能发生与时间有关的错误。
由于互斥是操作系统乃至并发程序设计中十分重要的概念,故需要准确的理解互斥的概念。
不允许多个进程同时进入关于同一组共享变量的不同临界区
不同临界区不允许多个进程同时进入关于同一组共享变量的相同临界区
相同临界区
实现进程互斥
实现互斥,就是保证同一时刻最多只有一个进程处于临界区内,也即实现对于临界区的管理。
需要满足如下几个正确性原则:
-
互斥性原则
任意时刻之多只能有一个进程处于关于同一组共享变量的临界区之中。 -
进展性原则
临界区空闲时,只有执行了临界区入口及出口部分代码的进程参与下一个进入临界区的决策,该决策不可无限期延迟。 -
有限等待性原则
请求进入临界区的进程应该在有限的等待时间内获得进入临界区的机会。
Dekker互斥算法
算法核心思想就是:
- 设置一个可以表示进程是否将要进入或已经处于临界区的标志位。
- 某一进程要进入临界区,需要查看其它进程是否处于临界区(检查对应进程号的标志位是否为1),做出等待或进入临界区的动作。
高中的时候家里才买了电脑,我和我哥都想玩,电脑就那么一个,我和我哥就只有一个人可以霸占着电脑玩游戏,毕竟高中的学业压力还是比较大的,所以我哥玩的时候我都在做作业,做一会儿作业,看一下我哥是不是还在玩,做一会儿,看一下,就这么循环,最后好不容易我哥准备去睡觉了,一看时间都11点多了,还玩个屁啊......
Peterson互斥算法
算法核心思想为:
- 只要第二个进程在临界区,第一个进程就等待。
Peterson算法相较于Dekker算法简单
Lamport面包店算法
算法核心思想为:
- 对每个进程增设一个“摇号”状态,以及摇出的号码。
- 进程处于“摇号”状态时不被获准进入临界区。
- 对于“摇号”结束的进程进行所持有号码的比较,小号先行原则。
算法思想源于面包店,但是这种情况确实经常会经历的。
现在支付宝,微信用的比较多,以前去银行存钱,每次都得在门口的柜台机取一个号码,等柜员叫号,有时候银行里明明没什么人,取号的时候却显示有xx人在我前面,那叫一个气哦,所以在银行等的时候就很无聊了,时不时的要注意一下柜员有没有叫到自己。
所以每次看到那种拿金卡去后面办业务的人好羡慕......
银行取号排队的这个过程可能是Lamport算法最好的一个实例(天知道这些算法是不是哪个人在买面包的时候想到的)。
Eisenberg-Mcguire算法
个人觉得该算法是Dekker算法的加强版本,这算法的核心思想是:
- 设置一个标志位表示空闲,准备进入,已进入三种状态。并标记当前正在临界区的进程号。(是不是很像Dekker算法呢)
- 某一进程想要进入临界区时,判断是否存在其他进程已在临界区。
- 退出临界区时找到下一个非空闲的进程。
小时候亲戚家办席,不懂规矩,大大咧咧就要跑去坐上席(年少不懂事),被老妈扯回来了,回家那是一顿训。
长辈得先落座,然后才能是晚辈上座,如果碰到座位不够,一些懂事的小辈通常都会跑去厨房帮忙。在厨房就可以开吃了......
放在这里,我们可以看到,长辈和晚辈可以看做是一个个的进程,一些处于所谓want_in状态,先让辈分最高的人落座(检测所有人的辈分),然后才能指定下一个落座的人。
我们发现,在Lamport和Eisenberg算法中出现了一条循环语句,可以让进程处于“等待”状态,但这并不是让进程从运行态变为等待态,只是让活动进程执行循环直到条件满足才能跳出而已,并不涉及到进程的状态改变。
这种等待状态也称“忙式等待”。
“测试并设置”指令
前文所介绍的都是软件方法实现进程的互斥,但硬件实现进程互斥通常会比采用软件方法要简单,不需要过多的动作,不过这需要以硬件支持为前提。
“测试并设置”指令的定义如下:
int test_and_set(int *target)
{
int temp;
temp = *target;
*target = 1;
return temp;
}
这条指令是原子的,即在指令执行时是不可分割的。
基于ts指令的算法思想与Dekker算法较为类似,为:
- 查看现在是否有进程处于临界区。
左边的算法非常简单,但是却不满足有限等待性,即公平性。
当某一进程退出临界区时,该算法没有指定下一个可以进入临界区的进程,如此便可造成一些进程长时间处于等待状态,甚至出现饿死的情况,即为不公平的算法。
右边的算法则稍显复杂,但主要只增设了两个标志位,waiting表示是否处于等待态,key表示是否被获准进入临界区。
由于出现了waiting这一标志位,因而现在可以得到多个进程的状态用于判断是否可以指定下一个可以进入临界区的进程。
“交换”指令
交换指令也是原子的,其定义为:
void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
交换和测试并设置指令的算法都是套路......
交换指令的算法
小结
要记住,无论是对于操作系统,还是并发编程,互斥这个概念都是非常重要的,我们必须非常准确的理解并掌握。
实现互斥的算法还有很多,就比如硬件上还有简单的开关中断就能实现,但我们需要掌握的是算法思想的核心内容,毕竟千变万变,本质是不会变的嘛!
网友评论