进程同步概念简介 多线程上篇(四)
### 进程同步概念#### 临界资源
一旦有对资源的共享,就必然涉及竞争限制
比如尽管有两个人去水井打水,但是水井却只有一个;合理安排的话刚好错开,但是如果安排不合理,那就会出现冲突,出现冲突怎么办?总有一个先来后到,等下就好了。
这个水井就是一个临界资源
$\color {red} {临界资源用来表示一种公共资源或者说是共享数据,可以被多个线程使用。}$
但是每一次,只能有一个线程使用它,一旦临界资源被占用,其他线程要想使用这个资源,就必须等待。
当多进程访问临界资源时,比如打印机。
假设A进程和B进程轮流获得CPU时间片执行,A打印数学,B打印英语,如果不进行任何的控制与限制,可能会打印出来一张纸上一道数学计算题,接下来是一段英语的情况。
所以尽管A进程和B进程轮流获得时间片运行,但是当需要访问临界资源时,一旦有一个进程已经开始使用,另外的进程就不能进行使用,只能等待。
计算机就是那个计算机,硬盘就是那个硬盘,一段代码中的某个变量(共享变量)就是那个变量.....所有的一切都是只有一份,如果对于某个点多进程同时访问,必然要做一定的限制
进程同步的主要任务是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效地共享资源和相互合作,从而使程序的执行具有可再现性
#### 两种制约关系
既然资源访问有限制,到底有哪些场景是需要同步处理的?也就是何时会出现资源冲突?
!(data/attachment/forum/202207/17/143706c1x6t5mzzgtmcmm8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
看得出来,其实同步要解决的问题根本就是竞争
间接关系是赤裸裸的的竞争,共享同一个I/O就是一种竞争,尽管他们看似好像没有什么关系
直接的制约关系,源于进程间的合作,某种程度上来说也是一种竞争
只不过是有条件的竞争,他们共享缓冲区,当缓冲区满时只能是消费者可以运行,生产者需要阻塞,这可以认为缓冲区满这种情况下,消费者独占了缓冲区,生产者不能使用了,不过这种情况下还是说成合作比较容易理解
**要么是因为共享资源带来的竞争,要么就是相互合作带来的依赖**。
#### 临界区
有了临界资源的概念,就很容易理解临界区的概念,在程序中,所有的操作都是通过代码执行的,访问临界资源的那段代码就是临界区
以打水为例,所以在还没到井口,就要画一个大圈,不允许第二个人进入范围,“请站在安全黄线内”这句话熟悉么?
这就是临界区。
!(data/attachment/forum/202207/17/143841nvv8674krs8fav13.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
#### 同步规则
如何才能够合理处理竞争或者合作依赖导致的制约?
* 空闲让进
* 忙则等待
* 有限等待
* 让权等待
空闲让进和忙则等待很好理解,对于临界资源,如果空闲没有被使用,谁来了之后都可以使用;如果临界资源正在被使用,那么其他后来者就需要进行等待
有限等待是指,要求访问临界资源的进程,应保证有限时间内能进入自己的临界区,自己不能傻傻的等,傻傻等受伤的是自己
让权等待是指,如果无法进入自己的临界区时,应立即释放处理机,而不能占着CPU死等,你死等就算了,别人却也不能用了
有限等待和让权等待是两个维度
你不能为了一件事情不顾一切代价等个天荒地老,太伤身了;
如果你非要花五块钱去苏宁买一台电视(等待事件发生),人家不卖给你(无法进入临界区),你就赖着不走(忙等),你就耽误别人做生意了(别的进程无法获得CPU)
有限等待和让权等待的共同特性是必须保证有条件的退出以给其他进程提供运行的机会
简单说就是有限时间内你就要走开,你得不到更要走开,你即使能得到但是时间太久也得先让一下别人
!(data/attachment/forum/202207/17/144508a0rwilxl1seosr1o.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
临界区的设置就是安全黄线的设置,同步规则其实就是临界区两条黄线进出规则
对于临界区,还可以进一步细分出来进入区和退出区以及剩余区
!(data/attachment/forum/202207/17/144529t13v66uw667i3wwc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
### 临界区算法
Peterson算法
所以临界区方式解决同步问题就是借助于算法,合理的控制对于临界区的进入与退出,并且保障能够做到:空闲让进、忙则等待、有限等待、让权等待
一种有名的算法为 **Peterson**
Peterson算法适用于两个进程在临界区与剩余区间交替执行。假设两个进程分别为p0 和 p1
为了表示方便,使用pi表示其中一个进程时,pj表示另外一个,显然有i = 1-j(j = 1-i)
!(data/attachment/forum/202207/17/144557n3q0gnbnnd0zkk6n.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
使用一个int类型变量turn 表示可以进入临界区的线程,如果turn == i,表示pi可以进入临界区
使用boolean 类型数组flag,总共两个进程,所以flag,用来表示哪个进程想要进入临界区,如果flag = true;表示进程pi想要进入临界区
上图红框内为进入区,蓝框内为退出区
为了进入临界区,进程pi首先设置flag为true;并且设置turn为j;
显然,根据while的条件,只有flag == false 或者turn == i 时,pi可以进入临界区
也就是如果我想进入的话,当对方不想进入或者当前允许我进入时,我就可以进入临界区
显然,如果只有一个进程想要进入,那么如上所述,对方不想进入时,可以进入临界区,符合空闲让进
如果两个进程都想进入,不管经过多么激烈的竞争,当执行到while时flag 和 flag 都是true,也就是while内部的两个条件,条件1始终是true
但是turn只会有一个值,要么0 要么1,也就是说while的第2个条件决定了谁会被while阻塞,谁能够继续执行下去
这种情况下必然能够有一个进程会进入临界区,另外一个被while循环阻塞,所以符合空闲让进、忙则等待
当临界区内的进程执行结束后,会设置flag[] 标志位,如果此时另外的进程在等待,一旦设置后,其他进程就可以进入临界区(刚才已经说了,如果pi想进入,flag == false 或者turn == i 时可以进入)也就是说当前进程结束后,下一个进程就能够进入了,所以满足有限等待
##### 小结:
上面的算法,满足了通过进入区和退出区代码的设置,可以做到同步的规则
如果只有一个想要进入临界区,可以直接进入,如果两个竞争进入,只有一个能够进入,另一个会被while循环阻塞
Peterson只是一种临界区算法,还有其他的
### 同步方式之信号量
1965年,荷兰学者Dijkstra 提出的信号量(Semaphores)机制是一种卓有成效的进程同步工具。
临界区算法的原理可以让多进程对于临界区资源的访问串行化;
信号量机制允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
#### 整型信号量
最初信号量机制被称之为整型信号量
最初由Dijkstra 把整型信号量定义为一个用于表示资源数目的整型量 S,它与一般整型量不同,除初始化外,仅能通过两个标准的原子操作(AtomicOperation)wait(S)和 signal(S)来访问。
这两个操作一直被分别称为P、V操作(据说,据说因为Dijkstra是荷兰人。在最初论文以及大多数文献中,用P代表wait。用V代表signal。是荷兰语中高度 proberen 和增量verhogen)
Wait(S)和 signal(S)操作可描述为:
```c
wait(S):while (S<=0);
S:=S-1;
signal(S):S:=S+1;
```
wait表示资源申请:如果S小于等于0(资源不足)等待,如果满足那么将会进行S-1,也就是申请资源
signal表示资源释放:每释放一次资源,资源数目 S+1
P、V操作也称之为操作原语,就是指原子操作
**原子性**是指一个操作是不可中断的,要么全部执行成功要么全部执行失败(具体怎么保证的,此处可以不用关注)
!(data/attachment/forum/202207/17/144706dqaqkkj1s6rfq2v1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
多个线程并发的对资源进行访问时,借助于PV原语操作,可以有效地做到共享资源的限制访问。
但是,对于整型信号量,P操作也就是 wait(S)
```c
wait(S):while (S<=0);
S:=S-1;
```
**如果获取不到资源,将会持续while (S<=0);,将会永远等待,进程处于忙等状态**
#### 关于原语
wait(S)和 signal(S)这一原子操作叫做原语,原语是操作系统概念的术语,是由若干条指令组成的,用于完成一定功能的一个过程
是由若干个机器指令构成的完成某种特定功能的一段程序,具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。
原语是操作系统的核心部分组成,原语有不可中断性。它必须在管态(内核态,系统态)下执行,并且常驻内存,而个别系统有一部分不在管态下运行。
可以简单的理解是具有指定功能的操作系统提供的一个API性质的一种东西
#### 记录型信号量
鉴于整型信号量机制中的“忙等”情况,演化出来记录型信号量
如果进程无法进入临界区,那么进入等待释放CPU资源,并且通过一个链表记录等待的进程。
记录型信号量机制在整形信号量机制的基础上增加了进程链表指针L,这也是记录型信号量名称的由来
记录型信号量 semaphore 的结构如下:
```c
semaphore {
value:int value;
L:进程等待链表(集合);
}
```
value相当于整型信号量中的S,L就是一个链表(集合)
简言之,将整形信号量中的整型S,演化为一个结构,这个结构包括一个整型值,还有一个等待的进程链表
!(data/attachment/forum/202207/17/144903pk9dth8brt8w6yk9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
相对应整型信号量中的wait(S) 和 signal(S)可以描述为:
```c
wait(S):
var S = semaphore;
S.value=S.value-1;
if S.value<0 then block(S.L);
signal(S):
var S = semaphore;
S.value=S.value+1;
if S.value<=0 then wakeup(S.L);
```
上面的操作中,均定义了一个semaphore类型的变量S
* 如果执行 wait 操作,先执行资源减一,如果此时S.value<0,说明在申请资源之前(S.value-1),原来的资源就是<=0,那么该进程阻塞,加入等待队列L中 * 如果执行 signal 操作,先执行资源加一,如果此时S.value<=0,说明在释放资源之前(),原来的资源是<0的,那么将等待链表中的进程唤醒
上面逻辑的关键之处在于:
* 当申请资源时,先进行S.value-1,一旦资源出现负数,说明需要等待,S.value的绝对值就是等待进程的个数,也就是S.L的长度
* 当资源恢复时,先进行S.value+1,已经有人释放资源了然而资源个数还是小于等于0,说明原来就有人在等待,所以应该去唤醒
block 和 wakeup 也都是原语,也就是原子操作。
block原语,进行自我阻塞,放弃处理机,并插入到信号量链表S.L 中
wakeup原语,将S.L链表中的等待进程唤醒
如果 S.value的初值为 1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量,用于进程互斥。(效果就如同Peterson算法了)
#### AND 型信号量
针对于临界区算法或者是整型信号量或者是记录型信号量是针对各进程之间只共享一个临界资源而言的。
但是有些场景下,一个进程需要先获得两个或更多的共享资源后方能执行其任务。
假设A,B两个进程,均需要申请资源D,E
```
process A: process B:
wait(D); wait(E);
wait(E); wait(D);
```
假设交替执行顺序如下
```
process A: wait(D); 于是D=0
process B: wait(E); 于是E=0
process A: wait(E); 于是E=-1 A阻塞
process B: wait(D); 于是D=-1 B阻塞
```
最终A,B都被阻塞,如果没有外力作用下,两者都无法从阻塞状态释放出来,这就是死锁
#### 相关概念
#### 锁
对于一个水井,你在打水,另外的人就要等一等,这是人类大脑意识做出来的很基本的反应(人眼识别,大脑解析并且做出反应)
但是计算机程序并没有这么智能,你需要对他进行某些处理,以限制别的线程的访问,比如你可以将“安全黄线”变成一个安全门,比如厕所,进去了之后把门关上。。。
**这种概念就是锁,锁就是对资源施加控制,锁指的是一种控制权**
当进入临界区时,我们称之为获得锁,获得锁之后就可以访问临界资源;
其他线程想要进入临界区,也需要先获得锁,显然,他们获取不到,因为此时,锁被当前正在执行的线程持有
当前线程结束后,将会释放锁,别得线程就可以获取这个资源的锁,然后....
#### 死锁
锁表示一种控制权,对临界资源的访问权限,如果临界资源不止一个,就可能出现这种情况:
需要先后访问两种临界资源A和B,thread1获得了A线程的锁之后,等待获得B的锁,但是thread2获得了资源B的锁,在等待A资源的锁,这就出现了互相等待的情况
比如一条窄桥,同一时刻仅仅允许一辆车通过,如果一旦出现两辆车都走到桥的一半处,而且互不相让,怎么办?这就是死锁
#### 解决方案
AND型信号量机制就是用于解决这种多共享资源下的同步问题的
AND 同步机制的基本思想:
> 将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。
> 只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。
> 也就是对若干个临界资源的分配,采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。
这种思维就是通过对“若干个临界资源“的原子分配,逻辑上就相当于一份共享资源,要么得到,也么得不到,所以就不会出现死锁
在 wait 操作中,增加了一个“AND”条件,所以被称为AND 同步,也被称为同时wait操作,即Swait(Simultaneous wait),相应的signal被称为Ssignal(Simultaneous signal)
!(data/attachment/forum/202207/17/145244ynoonoqhevs9dnqk.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
Swait(S) 和 Ssignal(S)可以描述为:
```c
Swait(S1,S2,…,Sn)
if(Si>=1 and …and Sn>=1){
for( i=1 to n){
Si=Si-1;
}
}else{
将进程插入到第一个资源<1 的对应的S的队列中,并且程序计数器设置到Swait的开始(比如S1 S2 都大于等于1,但是 S3<1,那么就插入到S3.L中;从头再来就是为了整体分配)
}
Ssignal(S1,S2,…,Sn)
for(i=1 to n){
Si=Si+1;
将与Si关联的所有进程从等待队列中删除,移入到就绪队列中
}
```
AND型信号量机制借助于对于多个资源的“批量处理”的原子操作方式,将多资源的同步问题,转换为一个“同一批资源”的同步问题
#### 信号量集
记录型信号量机制中,只是对信号量加1(S+1 或者S.value+1) 或者 减1(S-1 或者S.value-1)操作,也就是只能获得或者释放一个单位的临界资源
如果想要一次申请N个呢?
如果进行N次的资源申请怎么办,一种方式是可以使用多次Wait(S)操作,但是显然效率较低;
另外有时候当资源数量低于某一下限值时,就不进行分配怎么办?需要在每次分配资源前检查资源的数量。
为了解决这两个问题,对AND信号量机制进一步扩展,形成了一般化的“信号量集”机制。
Swait操作可描述如下
**其中S为信号量,d为需求值,而 t为下限值。**
```c
Swait(S1,t1,d1,…,Sn,tn,dn)
if( Si>=t1 and …and Sn>=tn){
for(i=1 to n){
Si=Si-di;
}
}else{
将进程插入到第一个资源Si<ti 的对应的S的队列中,并且程序计数器设置到Swait的开始(比如S1 S2 都大于等于t1,t2,但是 S3<t3,那么就插入到S3.L中;从头再来就是为了整体分配)
}
Ssignal(S1,d1,…,Sn,dn)
for(i=1 to n){
Si=Si+di;
将与Si关联的所有进程从等待队列中删除,移入到就绪队列中
}
```
信号量集就是AND型信号量机制将资源限制扩展到Ti
* Swait(S,d,d)。此时在信号量集中只有一个信号量 S,但允许它每次申请 d 个资源,当现有资源数少于d时,不予分配。
* Swait(S,1,1)。此时的信号量集已蜕化为一般的记录型信号量(S>1时)或互斥信号量(S=1 时)。
* Swait(S,1,0)。这是一种很特殊且很有用的信号量操作。当 S≥1 时,允许多个进程进入某特定区;当 S 变为 0 后,将阻止任何进程进入特定区。换言之,它相当于一个可控开关。
上面的格式为(s,t,d)也就是第一个为信号量,第二个为限制,第三个为需求量
所以Swait(S,1,0)可以用做开关,只要S>=1,>=1时可分配,每次分配0个,所以只要S>=1,永远都进的来,一旦S<1,也就是0往后,那么就不满足条件,就一个都进不去
#### 小结
临界区机制通过算法控制进程串行进入临界区,而信号量机制则是借助于原语操作(原子性)对临界资源进行访问控制
按照各种信号量机制对应的规则以及相应的原语操作,就可以做到对资源的共享同步访问,而不会出现问题
信号量机制总共有四种
!(data/attachment/forum/202207/17/145619oz495hjgur8j0gyh.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
整型信号量机制可以处理同一共享资源中,资源数目不止一个的情况
记录型信号量对整型信号量机制的“忙等”进行了优化,通过block以及weakup原语进行阻塞和通知
AND型信号量机制解决了对于多个共享资源的同步
信号量集是对AND的再一次优化,既能够处理多个共享资源同步的问题,还能够设置资源申请的下限,是一种更加通用的处理方式
`本文作者:程序员潇然 疯狂的字节X https://crazybytex.com/`
### 信号量的应用
#### 实现资源互斥访问
为使多个进程能互斥地访问某临界资源,只须为该资源设置一互斥信号量 mutex,并设其初始值为1
然后将各进程访问该资源的临界区 CS置于 wait(mutex)和 signal(mutex)操作之间即可。
这样,每个欲访问该临界资源的进程在进入临界区之前,都要先对 mutex 执行wait操作,若该资源此刻未被访问,本次wait操作必然成功,进程便可进入自己的临界区,否则进程将会阻塞。
步骤:
```c
.......
wait(mutex);
临界区
signal(mutex);
剩余区
.......
```
实现前趋关系
前驱关系就是指执行顺序,比如要求语句S1执行结束之后才能执行语句S2
在进程P1中:
S1;
signal(S);
在进程P2中
wait(S);
S2;
显然,初始时将资源S设置为0,S2需要获取到资源才会执行,而S1执行后就会释放资源
对于一个更加复杂的前驱关系图,如何实现?
!(data/attachment/forum/202207/17/145714clcr9brmiyzyd8lb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
从图中可以看得出来,有S2和S3依赖S1
S4 和 S5依赖S2,而S6依赖S3、S4、S5
所以,S1应该提供两个信号量,提供给S2和S3 使用
S2 应该等待S1的信号量,并且提供两个信号量给S4 和 S5
S3 应该等待S1的信号量,并且提供一个信号量给S6
S4应该等待S2的信号量,并且提供一个给S6
S5应该等待S2的信号量,并且提供一个给S6
S6应该等待S3、S4、S5
所以总共需要2+2+1+1+1 6个信号量,我们取名为a,b,c,d,e,f,g
!(data/attachment/forum/202207/17/145727v51j6bxqbb50wqlx.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
那么过程如下:
```c
Var a,b,c,d,e,f,g:semaphore: =0,0,0,0,0,0,0;
P1{
S1;
signal(a);
signal(b);
}
P2{
wait(a);
S2;
signal(c);
signal(d);
}
P3{
wait(b);
S3;
signal(e);
}
P4{
wait(c);
S4;
signal(f);
}
P5{
wait(d);
S5;
signal(g);
}
P6{
wait(e);
wait(f);
wait(g);
S6;
}
```
有人可能会疑惑,这里的wait和signal又是什么?他就是信号量机制中的 wait 和 signal,他的内容是相当于下面的这些
!(data/attachment/forum/202207/17/145904a2d2y3j2bbaybdzn.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
### 同步方式之管程
虽然信号量机制是一种既方便、又有效的进程同步机制
**但每个要访问临界资源的进程都必须自备同步操作 wait(S)和 signal(S),这就使大量的同步操作分散在各个进程中。**
这不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。
在解决上述问题的过程中,便产生了一种新的进程同步工具——管程(Monitors)。
所以管程也可以这么理解:它能够确保临界资源的同步访问,并且还不用将大量的同步操作分散到各个进程中。
#### 管程的定义
> 系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性
比如一个IO设备,有状态(空闲还是忙时?),以及可以对他采取的操作(读取还是写入?)以及等待该资源的进程队列来描述,所以就可以从这三个维度抽象描述一个IO设备,而不关注他们的内部细节
!(data/attachment/forum/202207/17/150010yu8hwd9zcaxcd5ai.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
又比如,一个集合,可以使用集合大小,类型,以及一组可执行操作来描述,比如Java中的ArrayList,有属性,还有方法
!(data/attachment/forum/202207/17/150020mkggeg9lnazegsgq.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
**可以把对该共享数据结构实施的操作定义为一组过程**
比如资源的请求和释放过程定义为request 和 release
进程对共享资源的申请、释放和其它操作,都是通过这组过程对共享数据结构的操作来实现的
类比到JavaBean的话,这些操作就如同setter和getter方法,所有对于指定对象的操作访问都需要通过getter和setter方法 ,类似,所有对共享数据结构实施的操作,都需要借助于这一组过程。
!(data/attachment/forum/202207/17/150043pwmbl0o6lfeaz57k.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
这组过程还可以根据资源的情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现进程互斥
还是类比到JavaBean,就是相当于又增加了几个方法,这些方法提供了更多的逻辑判断控制
代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,共同构成了一个操作系统的资源管理模块,我们称之为管程。
Dijkstra于1971年提出:
> 把所有进程对某一种临界资源的同步操作都集中起来,构成一个所谓的秘书进程。
> 凡要访问该临界资源的进程,都需先报告秘书,由秘书来实现诸进程对同一临界资源的互斥使用。
> 管程的概念经由Dijkstra提出的概念演化而来,由Hoare和Hanson于1973年提出。
定义如下:
一组相关的数据结构和过程一并称为管程。
Hansan的定义:一个管程定义了一个数据结构和能为并发进程在该数据结构上所执行的一组操作,这组操作能同步进程和改变管程中的数据。
所以管程的核心部分是对共享数据抽象描述的数据结构以及可以对该数据结构实施操作的一组过程。
使用数据结构对共享资源进行抽象描述,那么必然要初始化数据,比如一个队列Queue,有属性size,这是一个抽象的数据结构,那么一个具体的队列到底有多大?你需要设置size 的值,所以对于管程还包括**初始化**
!(data/attachment/forum/202207/17/150114k2v4mi996w28mawa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
一个基本的管程定义如下:
```
Minitor{
管程内部的变量结构以及说明;
函数1(){
}
......
函数N(){
}
init(){
对管程中的局部变量进行初始化;
}
}
```
#### 管程特点
管程就是管理进程,管程的概念就是设计模式中“依赖倒置原则”,依赖倒置原则是软件设计的一个理念,IOC的概念就是依赖倒置原则的一个具体的设计
管程将对共享资源的同步处理封闭在管程内,需要申请和释放资源的进程调用管程,这些进程不再需要自主维护同步。
有了管程这个大管家(门面模式?)进程的同步任务将会变得更加简单。
管程是墙,过程是门,想要访问共享资源,必须通过管程的控制(通过城墙上的门,也就是经过管程的过程)
而管程每次只准许一个进程进入管程,从而实现了进程互斥
!(data/attachment/forum/202207/17/150209opoxclu0potucdtc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
**管程的核心理念就是相当于构造了一个管理进程同步的“IOC”容器。**
> 管程是一个语言的组成成分(非操作系统支持部分),管程的互斥访问完全由编译程序在编译时自动添加上,无需程序员关心,而且保证正确
一般的 monitor 实现模式是编程语言在语法上提供语法糖,而如何实现 monitor 机制,则属于编译器的工作
比如
Java中使用synchronized时,这是不是一种管程理念?你只是写了一个synchronized关键字(语法糖),多线程的共享同步完全不用你操心了
(注意:并不是所有的语言都支持管程的概念)
!(data/attachment/forum/202207/17/150246vzti9ylzuqatrudg.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")
#### 条件变量
管程可以保证互斥,同一时刻仅有一个进程进入管程,所以他必然需要同步工具,如两个同步操作原语 wait和 signal,他还需要互斥量用以控制管程进入的同步
当某进程通过管程请求获得临界资源而未能满足时,管程便调用 wait 原语使该进程等待,并将其排在等待队列上
仅当另一进程访问完成并释放该资源之后,管程调用signal原语,唤醒等待队列中的队首进程
但是,仅仅这个互斥量是不够的
比如,如果需要处理之前提到过的“执行顺序控制”,如何控制前驱关系?
当一个进程调用了管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,而在此期间,如果该进程不释放管程,则其它进程无法进入管程,被迫长时间地等待。
所以还需要其他的信号量用于针对其他条件进行同步,这些就是条件变量,所以一个完整的管程定义为:
```c
Minitor{
管程内部的变量结构以及说明;
condition条件变量列表;
函数1(){
}
......
函数N(){
}
init(){
对管程中的局部变量进行初始化;
}
}
```
条件变量就是当调用管程过程的进程无法运行时,用于阻塞进程的一种信号量
管程中对每个条件变量都须予以说明,其形式为:Var x,y:condition。
对条件变量的操作仅仅是wait和signal,条件变量也是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时提供的两个操作即可表示为 x.wait和x.signal,其含义为:
① x.wait:正在调用管程的进程因 x 条件需要被阻塞或挂起,则调用 x.wait 将自己插入到x条件的等待队列上,并释放管程,直到x条件变化。此时其它进程可以使用该管程。
② x.signal:正在调用管程的进程发现 x 条件发生了变化,则调用 x.signal,重新启动一个因 x 条件而阻塞或挂起的进程。如果存在多个这样的进程,则选择其中的一个,如果没有,则继续执行原进程,而不产生任何结果。这与信号量机制中的 signal操作不同,因为后者总是要执行s:=s+1操作,因而总会改变信号量的状态。
如果有进程Q因x条件处于阻塞状态, 当正在调用管程的进程P执行了x.signal操作后,进程Q 被重新启动,此时两个进程 P和Q,如何确定哪个执行,哪个等待,可采用下述两种方式之一进行处理:
(1)P等待,直至Q 离开管程或等待另一条件。
(2)Q等待,直至P离开管程或等待另一条件。
采用哪种处理方式,当然是各执一词。
Hoare 采用了第一种处理方式
而 Hansan 选择了两者的折衷,他规定管程中的过程所执行的signal操作是过程体的最后一个操作,于是,进程P执行signal操作后立即退出管程,因而进程Q马上被恢复执行。
### 总结
进程控制是操作系统的一种硬性管理,是必须要有的,如果没有进程控制,就没办法合理安排运行进程, 根本无法完成状态的切换维护等。
进程的同步是一种软逻辑上的,如果不做,并不会导致系统出问题或者进程无法运行,但是如果不进行同步,结果却很可能是错误的,所以也是必须做的
类比装修的话,进程控制就是硬装,不装没法住,总归要水电搞搞好,进程同步就是家具家电和软装,硬装后住进去不会出现“生存问题”(至少有水喝有电用),但是你要是连个热水壶都没有是打算要喝凉水么
**进程同步的概念多很复杂抽象,因为毕竟是概念表述,没有涉及到具体的实现细节。**
> 进程同步的核心是对于临界资源的访问控制,也就是对于临界区的访问。
> 不管是临界区算法还是信号量机制还是管程机制,终归也都是控制进入临界区的不同的实现思路。
每种不同的算法、机制都各自有各自的特色,场景。
信号量机制将临界资源的访问的互斥,演化为可以多个进程访问资源(整型信号量),记录型信号量对整型信号量机制进行优化,处理了忙等的问题
然后继续演化出AND型,可以对不同的资源进行同步,而不仅仅是同一种资源
最后发展为信号量集的形式,可以对不同的资源、不同的个数、不同的限制进行处理,变得更为通用。
管程更多的是一种设计思维,管程就是管理进程的程序,进程对于资源的同步操作全都依赖管程这一“大管家”,管程是编程语言级别的,不需要程序员进行过多的处理,一般会提供语法糖
需要注意并不是所有的语言都有管程的概念(Java是有的),管程让你从同步的细节中解放出来,可以在很多场景下简化同步的实现。
管程的概念是“线程同步”的“IOC”,大大简化了同步的代价。
不管临界区算法还是信号量机制还是借助于管程,他们都是一种同步工具,可以认为他们就是一组“方法”,“方法”的逻辑就是本文前面介绍的原理
在需要进程同步问题的解决思路中,可以直接使用“封装好的方法”
以上,尽管都是在说操作系统关于进程的同步的处理
其实,在同步问题上进程和线程的设计理念是相通的
因为这本质上都是在说并发----多道程序运行的操作系统,通常使用轮转时间片的方式并发的运行多个进程
!(data/attachment/forum/202206/16/141330jha7st9soow8772i.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "common_log.png")
`转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-57-1-1.html `
页:
[1]