一、进程互斥的实现方式
1.1软件实现
1.1.1单标志法
代码实现:假设此时我们有两个进程P0与P1要使用互斥的临界资源。
int turn = 0;
//p0进程
while ( turn ! = 0 ); //进入区
critical section; //临界区
turn = 1; //退出区表示谦让
remainder section; //剩余区
//
//
//
//p1进程
while ( turn ! = 1 ); //进入区
critical section; //临界区
turn = 0; //退出区表示谦让
remainder section; //剩余区
算法思想:首先turn的初始化值为0,当我们想执行P0的时候由于不满足进入while循环的要求,故P0会跳出while循环进入临界区,但是反观P1,此时turn的值为0,满足P1的while循环要求,故P1这个进程就会卡死在while循环中进行死循环,直到turn==1才可以进入临界区。这种通过一个变量标志来为临界资源上锁的方法就叫做单标志法。
优缺点:该算法可以满足在同一时间只有一个进程可以访问临界资源,但是我们继续分析代码会发现,当P0进程运行完毕后 turn==1,也就是P0表达了谦让的动作,下个进程只允许P1上CPU运行,那么如果此时P1进程并不需要使用临界资源,而P0突然又想使用临界资源了,由于此时turn==1,就导致P0也进不去临界区,就导致了临界资源空闲但是不让P1以外的进程进入,这就违反了我们进程互斥四大原则中的空闲让进原则。
1.1.2双标志先检查法
代码实现:
#1 bool flag[2];
#2 flag[2] = {false,false}; //boll数组,用来记录进程的状态,false为进程不想进入临界区
#3 //P0进程
#4 while (flag[1]); //检查进程1是否想进入临界区
#5 flag[0] = true; //将自己的状态改为true表示我要进入临界区
#6 critical section; //临界区
#7 flag[0] = false; //将自己的状态改为false表示谦让
#8 remainder section; //剩余区
#9 //
#10 //
#11 //
#12 //P1进程
#13 while (flag[0]); //检查进程0是否想进入临界区
#14 flag[1] = true; //将自己的状态改为true表示我要进入临界区
#15 critical section; //临界区
#16 flag[1] = false; //将自己的状态改为false表示谦让
#17 remainder section; //剩余区
算法思想:我们来分析代码,首先给定了一个bool型的数组,使用false来表示进程不想进入临界区,使用ture表示进程想进入临界区。我们先看进程P0,进程P0执行之前会先检查P1是否想要进入临界区,如果P1想进入临界区,那么P0就会进入死循环,否则P0将进入临界区。此时P0进入临界区后flag[0]==true,那么此时P1检测到P0想要进入临界区,那么P1就会被卡在while循环中形成死循环用来等待P0进程,当P0进程出临界区后flag[0]==false,此时P1才可以进入临界区,这样就可以达到互斥的目的。
优缺点:由于这种算法可以在自己用完临界资源后,让出临界资源允许其他任何进程包括他自己再次使用临界资源,故这种算法解决了单标志法的空闲等待的问题。 但是我们都知道进程是存在运行时间片的,每个进程的时间片由操作系统分配,如果操作系统分配的时间片使得代码的执行过程为:#4->#13->#5->#14,我们来看一下运行效果。首先P0会检查P1是否想要进入临界区,答案是否定的,故P0会跳出循环向下运行,然后P1检查P0是否要进入临界区,由于此时#5还没有执行所以答案也是否定的。故两个进程都会进入临界区,这就导致了两个进程同时使用互斥的共享资源,显然这是大忌。而这种问题的原因就来源于检查与改变自己的状态两句代码不能一气呵成的完成,后面我会在信号量那里解决这个问题。
1.1.3双标志后检查法
代码实现:双标志后检查法就是将检查与改变状态的两句代码互换位置。
#1 bool flag[2];
#2 flag[2] = {false,false}; //boll数组,用来记录进程的状态,false为进程不想进入临界区
#3 //P0进程
#4 flag[0] = true; //将自己的状态改为true表示我要进入临界区
#5 while (flag[1]); //检查进程1是否想进入临界区
#6 critical section; //临界区
#7 flag[0] = false; //将自己的状态改为false表示谦让
#8 remainder section; //剩余区
#9 //
#10 //
#11 //
#12 //P1进程
#13 flag[1] = true; //将自己的状态改为true表示我要进入临界区
#14 while (flag[0]); //检查进程0是否想进入临界区
#15 critical section; //临界区
#16 flag[1] = false; //将自己的状态改为false表示谦让
#17 remainder section; //剩余区
算法思想:算法思想其实是与双标志先检查法一样的。
优缺点:这种算法我们依旧按照#4->#13->#5->#14,来运行。首先P0表达了自己想要进入临界区的意愿,然后P1也表达了自己想要进入临界区的意愿,此时#5发现进程1想要进入临界区,故进程P0会卡死在while循环中,#14此时也会检查发现P0想要进入临界区,故P1也会卡死在while循环中,两个进程都在谦让对方,发生了死锁的问题。显然又是不能一气呵成造成的问题。违背了空闲让进以及有限等待。
1.1.4Peterson算法
代码实现:
#1 bool flag[2]
#2 int turn = 0;
#3 //P0进程
#4 flag[0] = ture;
#5 turn = 1;
#6 while (flag[1] && turn==1);
#7 critical section;
#8 flag[0] = false;
#9 remainder section;
#10 //
#11 //
#12 //
#13 //P1进程
#14 flag[1] = ture;
#15 turn = 0;
#16 while (flag[0] && turn==0);
#17 critical section;
#18 flag[1] = false;
#19 remainder section;
算法思想:首先P0与P1的flag都为false,先让P0上CPU运行,到#6时,此时由于CPU没有上处理器,故#14没有运行,故#6的while循环虽然满足了turn==1,但是不满足flag[1]==true,故P0会进入循环执行结束后,P0再变为false接触对临界资源的上锁。
这是比较好的情况,没有发生时间片的轮转,现在我们使时间片轮转,看看这种算法会不会发生前面几种算法出现的问题。首先我们按照#4->#14->#5->#15的方式运行,我们将这几串代码运行完毕后发现此时flag[0]==true,flag[1]==true,但是由于turn为一种特殊的全局变量值唯一,故此时的turn==0,那么我们看两个进程的while循环的要求就会发现,P1的while满足要求,故P1此时会死循环进行等待,但是P0却可以跳出循环,避免了死锁。
我们换一种方式来运行#4->#14->#15->#5,这种运行方式后flag[0]==true,flag[1]==true,turn=1,这样一来P0被阻塞,P1可以运行,那这种算法似乎真的可以解决前面算法的所有问题。
优缺点:事实上这种代码还是存在着一个问题,那就是运用了while循环,我们前面分析了,当一个进程被卡在while循环中时,该进程会进入死循环来等待临界资源的释放,这样一来进程在循环的过程中就已经在消耗CPU。这样会导致只要进程进不了临界区,那么该进程就会在时间片用完前一直霸占CPU,故不满足让权等待原则。
1.2硬件实现
1.中断屏蔽法
实现思想:这种方法是用关中断指令和开中断指令来实现的,也就是在访问临界区前加上一个关中断指令,在访问临界区后加上一个开中断指令。这样一来就可以满足访问临界区前上锁,出临界区后开锁的目的。
优缺点:显然,关/开中断指令属于内核指令,只能适用于内核进程而不适用于用户进程,而且如果存在多台处理机公用一个临界资源的情况下,由于一个关/开中断指令只对应一个处理机,那么如果此时有其他处理机上的进程想要访问该临界资源,那么此关/开中断指令是管不了这台处理机上的进程的。说白了也就是前朝的剑斩不了本朝的官。
2.TestAndSet指令:该指令是用硬件实现的,功能与关/开中断指令相同,不过与其不同的是,该指令可以适用于多处理机,可谓是全能王。
二、信号量机制(408重点)
2.1信号量是什么
信号量其实是一种能够反应系统资源的变量,信号量分为整形信号量与记录型信号量。信号量这种变量可以通过操作系统给我们的一对原语wait(S)和signal(S)原语来实现,我们可以将这两个原语想象成是一个库函数即可,中间的S是进程,可以将其看成是函数调用需要传的参数。
2.2信号量的实现(整形信号量)
wait与signal代码实现:
int S = 1; //整形信号量
void wait (int S){
while(S<=0);
S=S-1;
}
void signal (int S){
S=S+1
}
首先我们可以看到S作为一个全局变量初始值为1,先看wait函数,wait函数需要做的就是判断该进程此时可不可以进入临界区,如果可以就需要给临界区上锁;signal函数需要做的就是给临界区解锁,那么是如何做到的呢? 我们可以将S想象成临界资源的数量即整形信号量,这个代码中,临界资源的数量为1。当wait函数执行的时候,先用while来判断临界资源是否不足1个,如果临界资源不足一个,那么就会将进程阻塞,如果临界资源足够,那么就会使临界资源的数量减去该进程消耗的数量,此处为1。这样就达到了即给临界资源上锁,如果临界资源较多还不影响其他进程的使用。再看signal函数,运行完毕后直接将临界资源数加一即可。
由于这种方式使用了原语使检查与上锁一气呵成,故避免了异步性带来的问题。但是这种算法由于引入了while循环,如果某个进程卡在了while循环中,那么该进程会占用处理机盲等,这里我认为还有一个致命的问题,由于这个循环在原语中,不会被时钟中断,如果得不到临界资源就会无限循环下去,这个行为很危险,但是似乎这里各大教材并没有说明这个问题,我不晓得为啥。
2.3信号量的实现(记录型信号量)
代码实现:
typedef struct {
int value; //临界资源数量
struct process *L; //等待临界资源的阻塞队列
} semaphore;
void wait (semaphore S) {
S.value--; //占用一个临界资源
if (S.value < 0){ //如果此时临界资源小于0
block (S.L); //该进程没得临界资源可用,让他进入临界资源等待队列中阻塞
}
}
void signal (semaphore S) {
S.value++; //用完了释放1个临界资源
if (S.value <= 0) { //如果此时临界资源在释放完后还是小于等于0证明等待队列中有进程阻塞
wakeup(S.L); //唤醒等待队列中的进程
}
}
这个代码的难点在于理解为什么临界资源的数量判断后小于等于0还要唤醒等待队列中的进程,我们顺着代码逻辑可以发现,我们假设初始临界资源只有1个,且被某个进程所占用了,那么此时value=0,那么如果此时还有一个进程想使用临界资源,那么value会再次减一;此时还有一个进程想要占用临界资源,那么value还会减一,我们发现,value为负数时,value绝对值就代表着等待临界资源的阻塞队列中有多少个进程被阻塞,那么此时如果某个进程归还一个临界资源,使value加一,那么此时虽然value为负数,但实际上此时是有一个临界资源可用的,那么就需要唤醒阻塞队列中的队头进程,让其使用该临界资源。
优缺点:显然,由于block与wakeup函数的加入,以及value变化机制可以判断出,当一个进程无法被分配临界资源的时候,会进入阻塞队列进行阻塞等待,而不是占用CPU循环等待。这样就完成了让权等待。显然这种方法实现的进程互斥是十分完美的。
三、信号量的应用
3.1用信号量机制来完成进程的同步、互斥
同步:我们可以创建一个mutex变量用来记录进程进入临界区的名额,那么在临界区的前后对mutex进行PV操作,就可以实现这种临界区的互斥访问。
互斥:我们可以定义一个系统资源S,进程P1共三句代码,进程P2也三句代码,如果我们不加以信号量机制,那么由于时钟管理,那么这两个进程的运行速度便是我们不可预知的。但是此时我们将P1的第二句代码后加上一个V操作,然后再进程P2执行前加上一个P操作,那么此时由于进程P2前有一个P操作,故导致我们定义的系统资源不足,故不能运行,只有当P1的第二句代码执行完毕后执行V操作增加一个系统资源后,P2才能运行,这就可以使代码朝着我们期望的速度运行。
3.2生产者-消费者问题
问题是这样的:生产者用来向临界资源中生产产品,而消费者需要从临界资源中拿走产品,但问题是临界资源空间是有限的,当临界资源中存满了产品的时候,那么我们就要组阻止生产者继续向里面生产产品,同时如果临界资源的产品为空的时候,我们也要组织消费者从中拿走产品。此外当临界资源有空闲空间的时候,我们就可以唤醒生产者,同时当临界资源有产品的时候我们也可以唤醒消费者。此外两个生产者不能同时向临界资源中存放数据,做到进程间互斥。
代码实现:
semaphore mutex = 1; //用来实现进程互斥
semaphore empty = n; //用来表示初始时缓冲区空闲位置这种资源为满的
semaphore full = 0; //用来表示初始时缓冲区产品这种资源为空的
//生产者
producer (){
while(1){
生产一个产品:
P(empty) //消耗一个空位
P(mutex); //临界区上锁实现进程互斥
把产品放入缓冲区;
V(mutex); //为临界区解锁
V(full) //生产一个产品
}
}
//消费者
consumer (){
while(1){
P(full) //消耗一个产品
P(mutex); //临界区上锁实现进程互斥
从缓冲区取出一个产品;
V(mutex); //为临界区解锁
V(empty) //生产一个空位
使用产品;
}
}
代码分析:其实代码的底层逻辑我已经用注释表明了,假如此时生产者由于缓冲区存满被阻塞再P中,但如果消费者执行了V(empty)操作就可以生产一个空位,进而唤醒生产者进程,这就是该代码的底层逻辑。 还有一个问题需要我们思考,如果我们将代码中两个进程的两个P操作交换位置,也就是将临界区上锁的P操作与消耗一个资源的P操作进行交换,我们来看一下效果:首先生产者给临界区上锁,但如果此时缓冲区已满,那么生产者就会被卡在P(empty)这个代码处无法继续运行,需要等待消费者生产一个空位才行,但是此时我们再看消费者进程,由于生产者给临界区上锁,导致消费者进程也无法执行,那这样就发生了生产者在等待消费者生产一个空位,而消费者又在等待生产者给临界区解锁,哦豁,发生了死锁问题。
这里要注意,我们需要让临界区代码尽可能的短,由于用原语上锁的缘故,导致进程访问临界区的过程操作系统无法进行其他进程访问该临界区的进程调用,故尽量让代码长度短一些,才能尽量缩短进程访问临界区的时间。
3.3多生产者-多消费者问题
问题是这样的:有两个生产者分别是父亲和母亲;有两个消费者分别是儿子和女儿;有一个临界资源是盘子,每次只能存放一个产品,父亲每次向盘子中放入一个苹果,由女儿负责吃掉,母亲每次向盘子中放入一个橘子,由儿子吃掉。而盘子一次只能装1个水果,当盘子中已经有一个水果那么父亲和母亲的放入水果过程就要被阻塞,而当盘子为空的时候,儿子和女儿吃水果的过程就要被阻塞。
代码实现:
semaphore mutex = 1; //实现访问盘子互斥
semaphore apple = 0; //初始苹果数量为0
semaphore orange = 0; //初始橘子数量为0;
semaphore plate = 1; //初始盘子可以放1个水果。
//父亲
dad (){
while(1){
准备一个苹果;
P(plate) //消耗一个盘子空间
P(mutex)
把苹果放入盘子;
V(mutex)
V(apple) //生产一个苹果
}
}
//母亲
mom (){
while(1){
准备一个橘子;
P(plate) //消耗一个盘子空间
P(mutex)
把橘子放入盘子;
V(mutex)
V(orange) //生产一个橘子
}
}
//女儿
daughter (){
while(1){
P(apple) //消耗一个苹果
P(mutex)
从盘子中取出苹果;
V(mutex)
V(plate) //生产一个盘子空间
吃掉苹果;
}
}
//儿子
son (){
while(1){
P(orange) //消耗一个橘子
P(mutex)
从盘子中取出橘子;
V(mutex)
V(plate) //生产一个盘子空间
吃掉橘子;
}
}
代码分析:这里的代码逻辑并不难理解,需要注意一点的就是由父亲占用的空间只能由女儿释放,由母亲占用的空间只能由儿子释放。 这里我们尝试将mutex这个信号量去掉,分析代码后发现各个进程依然可以做到互斥访问,原因在于缓冲区的空间仅为1,如果父亲已经访问过缓冲区放入苹果,那么母亲就无法再放入橘子。可是如果我们将缓冲区的大小设置为2呢,父亲放入一个苹果,母亲此时发现缓冲区还有空间,那么母亲就会向缓冲区放入橘子,而现实中我们的缓冲区如果被两个进程同时访问并写入数据,可能会发生数据覆盖的情况,故mutex在某些情况下,没有这个信号量也能实现互斥,但是大部分情况还是要加上mutex这个信号量。
3.4吸烟者问题
问题是这样的:有三个吸烟者进程,进程1拥有烟草,进程2拥有卷纸,进程3拥有胶水,而吸烟者想要吸烟就必须用胶水粘好卷了烟草的卷纸,也就是必须同时拥有烟草,卷纸和胶水才可以吸烟。现在有一个供应者,每次可以向桌子上放一个组合:A组合 卷纸+胶水,B组合 烟草+胶水,C组合 烟草+卷纸。这些组合分别对应着1个进程,这样就可以使三个进程轮流执行。
代码实现:
semaphore offer1 = 0; //桌子上组合1的数量
semaphore offer2 = 0; //桌子上组合2的数量
semaphore offer3 = 0; //桌子上组合3的数量
semaphore finish = 0; //用于判断吸烟完成
int i = 0; //用于实现循环
//供应者
provider (){
while(1){
if(i == 0){
将组合1放桌上;
V(offer1);
} else if(i == 1){
将组合2放桌上;
V(offer2);
} else if(i == 2){
将组合3放桌上;
V(offer3);
}
i = (i + 1) % 3;
P(finish);
}
}
//吸烟者进程1
smoker1 (){
while(1){
P(offer1);
从桌子上拿走组合1;
完成吸烟;
V(finish);
}
}
//吸烟者进程2
smoker2 (){
while(1){
P(offer2);
从桌子上拿走组合2;
完成吸烟;
V(finish);
}
}
//吸烟者进程3
smoker3 (){
while(1){
P(offer3)
从桌子上拿走组合3;
完成吸烟;
V(finish);
}
}
代码分析:这个代码类似于向盘子中放水果,由于缓冲区的大小为1,故每次只能有1个进程顺利的访问缓冲区,故不需要专门设置一个mutex信号量来给临界区上锁。
3.4读者-写者问题
问题是这样的:一个文件可以被写数据,也可以被读数据。但是要遵循某种规律:1.该文件只能由一个写者进程访问。 2.该文件可以被多个读者进程访问。 3.在写者进程写数据之前,需要停止其他进程对文件的访问。 4.在写者进程写数据完成前,禁止其他进程对文件访问。 造成这种问题的原因是:读者读数据的时候并不会更改文件中的数据内容,写者写数据的时候会更改文件中的数据内容,甚至是覆盖上一个写者在文件中写的内容。
代码实现:
semaphore rw = 1; //用于实现对共享文件的互斥访问
int count = 0;
semaphore mutex = 1;
writer (){
while(1){
P(rw);
写文件...;
V(rw);
}
}
reader (){
while(1){
P(mutex); //使后面的代码一气呵成
if(count == 0) //判断自己是不是第一个访问文件的读者
P(rw); //实现与写者的互斥
count++; //读文件前,使count++,代表有一个读者在读文件
V(mutex);
读文件...;
P(mutex);
count--; //读完文件,使count--,代表该读者访问完文件了
if(count == 0) //判断自己是不是最后一个访问完文件的读者
V(rw); //最后一个读者负责解锁文件
V(mutex);
}
}
代码分析:读者-写者的代码分析在代码中已经写成了注释,不过这里有几个问题需要思考,这段代码是如何实现读者和读者可以同时访问文件的呢?首先第一个读者对count进行判断发现自己是第一个进来的读者,那么就给文件上个锁,此处的上锁只能针对写者,因为第二个读者对count进行判断的时候已经不满足if语句,故会直接跳过P(rw)这句代码直接访问文件,这样就达成了读者可以同时访问文件,还满足了读者与写者的互斥。
这段代码的实现还有一个问题,如果有源源不断的读者进入,那么写者就永远不能被解锁,导致写者饥饿。那么如何解决这个问题呢?怎么在代码中加入信号量才能解决?需要思考。
3.5管程
由于信号量在解决一些很麻烦的问题的时候,编写难度较大,我们将一系列较复杂的信号量机制封装在一起就形成了管程。
管程中定义了一种数据结构,如生产者-消费者问题的缓冲区;还定义了各种函数用来实现生产和消费的功能,而一个管程只允许一个进程在某一时刻对其进行访问,该进程只需要调用管程中所包含的函数即可实现访问管程的效果,这种只允许一个进程访问管程就可以实现进程之间的互斥,这种互斥是由编译器完成的,程序员无需关心。
四、死锁
4.1死锁的概念
死锁就是两个进程都要使用对方进程所占用的临界资源而相互等待,只要一方没放弃这个临界资源,那么两个进程都会被阻塞,无法向前推进。简单来讲就是室友A和室友B都需要对方早上起床来叫醒自己上早八,但是室友A和室友B都没醒,但是对方又都在等待对方叫醒自己,这就发生了死锁。一般来讲死锁的形成需要四个必要条件,这四个条件缺一个都不能发生死锁。
1.互斥条件:一个临界资源只允许一个进程访问就会导致需要该临界资源的其他进程阻塞,就有可能发生死锁,一个临界资源如果允许多个进程访问,那么也就不会发生上述出现的问题。 2.不剥夺条件:前面说的两个进程都在等待对方手中的临界资源,而且不能将对方手中的临界资源剥夺让自己使用,所以只能两个进程都被阻塞。那么如果可以剥夺,自然也就没有了死锁。 3.请求和保持条件:一个进程在占有了一个资源后又请求了一个临界资源,而该临界资源正在被其他进程所使用。那么就会导致该进程由于资源不足无法向前推进,而该进程又占有这个临界资源不放,就导致了其他想使用这个临界资源的进程也被阻塞,发生死锁。 4.循环等待条件:我们将两个进程换成三个进程,每个进程都在等待对方手中的临界资源,进程A需要进程B,进程B需要进程C,而进程C又需要进程A,这样就形成了一个循环。只要我们打破了某一个环节,循环被打破,死锁也就可以解除。
总之,对资源分配的不合理就会导致死锁,而我们解决死锁有三种办法:1.预防死锁,2.避免死锁(银行家算法),3.检测死锁并解除。
4.2预防死锁
1.破坏互斥条件:将互斥临界资源在逻辑上改为共享临界资源。这种方法的缺点是某些互斥临界资源是必须保持互斥的功能才能使进程不出错的运行。
2.破坏不剥夺条件:第一种办法就是当进程申请完所有临界资源之前,某个临界资源被其他进程占用,那么就要该进程主动放弃所有已经申请的临界资源。第二种办法就是优先级高的进程可以剥夺优先级低的进程的临界资源。这种方法的缺点就是当某个进程马上就要运行完毕的时候,临界资源一旦被剥夺,就会导致该进程前面执行的结果功亏一篑,效率低下。
3.破坏请求和保持条件:使某个进程一次性的请求完所有的临界资源,否则就不投入运行,这样一来进程一旦运行起来就不会在保持临界资源的同时申请新的临界资源。这种方法的缺点就是容易导致饥饿,如果某个进程A需要资源A,进程B需要资源B,进程C需要资源A,B。进程A和进程B都只需要申请一个资源即可运行,而进程C在申请资源的时候需要保证进程A和进程B都没有占用资源,如果有源源不断的进程A和进程B投入运行,那么进程C就会饥饿。
4.破坏循环等待条件:我们可以将所有的临界资源进行编号,而进程申请资源的时候只能按照编号从小到大的顺序来进行申请。这样就可以保证拥有小编号的进程只会等待拥有大编号的进程,而拥有大编号的进程不可能会等待拥有小编号的进程,这样就破坏了循环。
4.3避免死锁(银行家算法)
假设我是一个成功的银行家,手中拥有100亿的美金。接下来我将会投资三家公司,分别为字节跳动向我请求70亿美金,淘宝向我请求40亿美金,而京东向我请求50亿美金。而这投资公司有一个规定就是在满足公司请求的最大投资金额之前,公司是不会将投资金额归还给我的。首先我分别给字节跳动,淘宝,京东投资了20亿,10亿,30亿,如图:
最大投资 | 已经投资 | 还需投资 | |
字节跳动 | 70 | 20 | 50 |
淘宝 | 40 | 10 | 30 |
京东 | 50 | 30 | 20 |
此时字节又给我发消息说,能不能将你手中剩下的40亿美金都投给我?按照投资规则来看,如果我将这40亿全部投给了字节跳动,由于不满足其70亿投资的投资金额,那么字节跳动就可以将我仅剩的40亿全部霸占,并且有权利不还给我。这样一来我由于手上没钱了,那么淘宝和京东投资的40亿也要不回来了,那么就导致我变成了穷光蛋,这就是死锁的原因。由于我投资金额的分配不当导致我变成了穷光蛋。
那么如果此时字节给我发消息说,让我先投20亿行不行?按照投资规则,我将20亿给字节,剩下的20亿我可以投给京东,然后京东就可以将50亿全部归还给我,然后我就可以用剩下的50亿投字节和淘宝,这样我就可以拿回所有的财产,这就是临界资源的合理分配。
那么如果我们将钱想象成临界资源,而临界资源分很多种,每个进程又需要多种临界资源,我们也可以用这种方法进行比较,如果我们模拟出了一个安全的投资路径,那么我们就可以按照这种分配方式进行临界资源的分配,而这种分配方式被称为安全序列。