同步回顾
进程同步控制有多种方式:算法、硬件、信号量、管程
这些方式能够认为就是同步的工具(方法、函数)
好比信号量机制中的wait(S) 和 signal(S) ,就至关因而两个方法调用。
调用wait(S)就会申请这个资源,不然就会等待(进入等待队列);调用signal(S)就会释放资源(或一并唤醒等待队列中的某个);
在梳理同步问题的解决思路时,只须要合理安排方法调用便可,底层的实现细节不须要关注。
接下来以这种套路,看一下借助与不一样的同步方式“算法、硬件、信号量、管程”这一“API”,如何解决经典的进程同步问题
生产者消费者
生产者-消费者(producer-consumer)问题是一个著名的进程同步问题。它描述的是:
有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。
为使生产者进程与消费者进程能并发执行,在二者之间设置了一个具备 n 个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品去消费。
尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步
也就是即不允许消费者进程到一个空缓冲区去取产品,也不容许生产者进程向一个已装满产品且还没有被取走的缓冲区中投放产品。
记录型信号量
对于缓冲池自己,能够借助一个互斥信号量mutex实现各个进程对缓冲池的互斥使用;
生产者关注于缓冲池空位子的个数,消费者关注的是缓冲池中被放置好产品的满的个数
因此,咱们总共设置三个信号量semaphore:
mutex值为1,用于进程间互斥访问缓冲池
full表示缓冲区这一排坑中被放置产品的个数,初始时为0
empty表示缓冲区中空位子的个数,初始时为n
对于缓冲池以一个数组的形式进行描述:buffer[n]
另外还须要定义两个用于对数组进行访问的下标 in 和 out ,初始时都是0,也就是生产者会往0号位置放置元素,消费者会从0号开始取
每次的操做以后,下标后移,in和out采用自增的方式,因此应该是循环设置,好比in为10时,应该从头再来,因此求余(简言之in out序号一直自增,经过求余循环)
//变量定义
int in=0, out=0;
item buffer[n];
semaphore mutex=l,empty=n, full=0;
//生产者
void proceducer(){
do{
producer an item nextp;
......
wait(empty);//等待空位子
wait(mutex);//等待缓冲池可用
buffer[in] =nextp;//设置元素
in =(in+1)%n;//下标后移
signal(mutex);//释放缓冲池
signal(full);//“满”也就是已生产产品个数释放1个(+1)
}while(TRUE);
//消费者
void consumer() {
do{
wait(full);//等待已生产资源个数
wait(mutex);//等待缓冲池可用
nextc= buffer[out];//得到一个元素
out =(out+1) % n;//下标后移
signal(mutex);//释放缓冲池
signal(empty);//空位子多出来一个
consumer the item in nextc;//消费掉得到的产品
} while(TRUE);
}
//主程序
void main() {
proceducer();
consumer();
}
以上就是一个记录型信号量解决生产者消费者的问题的思路
对于信号量中用于实现互斥的wait和signal必须是成对出现的,尽管他们可能位于不一样的程序中,这都无所谓,他们使用信号量做为纽带进行联系
AND型信号量
对于生产者和消费者,都涉及两种资源,一个是缓冲池,一个是缓冲池空或满
因此能够将上面两种资源申请的步骤转换为AND型,好比
wait(empty);//等待空位子
wait(mutex);//等待缓冲池可用
转换为AND的形式的Swait(empty,mutex)
int in=0, out=0;
item buffer[n];
semaphore mutex=l, empty=n, full=O;
void proceducer() {
do{
producer an item nextp;
......
Swait(empty, mutex);
buffer[in] = nextp;
in =(in+1) % n;
Ssignal(mutex, full)
} while(TRUE);
}
void consumer() {
do{
Swait(full, mutex);
nextc= buffer[out];
out =(out+1) % n;
Ssignal(mutex, empty);
consumer the item in nextc;
......
} while(TRUE);
}
这个示例中,AND型信号量方案只是记录型信号量机制的一个简单升级
管程方案
管程由一组共享数据结构以及过程,还有条件变量组成。
共享的数据结构就是缓冲池,大小为n
生产者向缓冲池中放入产品,定义过程put(item)
消费者从缓冲池中取出产品,定义过程get(item)
对于生产者,非满 not full 就能够继续生产数据;
对于消费者,非空 not empty 就能够继续消费数据;
因此设置两个条件:notfull,notempty
若是数据个数 count>=N,那么 notfull 非满条件不成立
若是数据个数 count<=0,那么notempty 非空条件不成立
也就是说:
count>=N,notfull 不知足,生产者就会在 notfull 条件上等待
count<=0N,notempty 不知足,消费者就会在 notempty 条件上等待
//定义一个管程
Monitor procducerconsumer {
item buffer[N];//缓冲区大小
int in, out;//访问下标
condition notfull, notempty;//条件变量
int count;//已生产产品的个数
//生产方法
void put(item x) {
if(count>=N){
notfull.wait; //若是生产个数已经大于缓冲区大小,将生产进程添加到notfull条件的等待队列中
}
buffer[in] = x; //设置元素
in = (in+1) % N; //下标移动
count++;//已生产产品个数+1
notempty.signal //释放等待notempty条件的进程
}
//获取方法
void get(item x) {
if(count<=0){
notempty.wait; // 若是已生产产品数量为0(如下),消费者进程添加到notempty的等待队列中
}
x = buffer[out];// 读取元素
out = (out+1) % N; // 下标移动
count--; //已生产产品个数-1
notfull.signal; // 释放等待notfull条件的进程
}
//初始化数据方法
void init(){
in=0;out=0;count=0;
}
} PC;
生产者和消费者逻辑
void producer(){
item x;
while(TRUE){
produce an item in nextp;
PC.put(x);
}
}
void consumer( {
item x;
while(TRUE) {
PC.get(x);
consume the item in nextc;
......
}
}
void main(){
proceducer();
consumer();
}
管程的解决思路就是将同步的问题封装在管程内部,管程会帮你解决全部的问题
哲学家进餐
由Dijkstra提出并解决的哲学家进餐问题(The Dinning Philosophers Problem)是典型的同步问题。
该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。
平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。
进餐完毕,放下筷子继续思考。
灰色大圆桌,黄色凳子,每一个人左右各有一根筷子,小圆点表示碗。(尽管画的像乌龟,但这真的是桌子  ̄□ ̄||)
记录型信号量机制
放在桌子上的筷子是临界资源,同一根筷子不可能被两我的同时使用,因此每一根筷子都是一个共享资源
须要使用五个信号量表示,五个信号量每一个表示一根筷子
当哲学家饥饿时,老是先去拿他左边的筷子,即执行wait(chopstick[i]);
成功后,再去拿他右边的筷子,即执行wait(chopstick[(i+1)mod 5]);又成功后即可进餐。(i+1)mod 5 是为了处理第五我的右边的是第一个的问题 )
进餐完毕,又先放下他左边的筷子,而后再放右边的筷子。
//定义五个信号量
//为简单起见,假定数组起始下标为1
//信号量所有初始化为1
semaphore chopstick[5]={1,1,1,1,1};
do{
//按照咱们上面图中所示,第 i号哲学家,左手边为i号筷子,右手边是 (i+1)%5
wait(chopstick[i]);//等待左手边的,
wait(chopstick[(i+1)%5]);]);//等待右手边的
// 进餐......
signal(chopstick[i]);//释放左手边的
signal(chopstick[(i+1)%5])//释放右手边的
// 思考......
} while(TRUE);
经过这种算法能够保证相邻的两个哲学家之间不会出现问题,可是一旦五我的同时拿起左边的筷子,都等待右边的筷子,将会出现死锁
有几种解决思路
(1)至多只容许有四位哲学家同时去拿左边的筷子
能够保证确定会空余一根筷子,而且没拿起筷子的这我的的左手边的这一根,确定是已经拿起左手边筷子的某一我的的右手边,因此确定不会死锁
(2) 仅当哲学家的左、右两只筷子都可用时,才容许他拿起筷子进餐。 也就是AND机制,将左右操做转化为“原子”
(3) 规定奇数号哲学家先拿他左边的筷子,而后再去拿右边的筷子,而偶数号哲学家则相反。
如上图所示,1抢1号筷子,2号和3号哲学家竞争3号筷子,4号和5号哲学家竞争5号筷子,全部人都是先竞争奇数,而后再去竞争偶数
这一条是为了全部的人都会先竞争奇数号筷子,那么也就是最多三我的抢到了奇数号筷子,有两我的第一步奇数号筷子都没抢到的这一轮就至关于出局了
三我的,还有两个偶数号筷子,必然会有一我的抢获得
AND型信号量
哲学家进餐须要左手和右手的筷子,因此能够将左右手筷子的获取操做原子化,借助于AND型信号量
//定义五个信号量
//为简单起见,假定数组起始下标为1
//信号量所有初始化为1
semaphore chopstick[5]={1,1,1,1,1};
do{
//按照咱们上面图中所示,第 i号哲学家,左手边为i号筷子,右手边是 (i+1)%5
Swait(chopstick[i],chopstick[(i+1)%5]))
// 进餐......
Ssignal(chopstick[i],chopstick[(i+1)%5]);
// 思考......
} while(TRUE);
读者写者问题
一个数据文件或记录,可被多个进程共享,咱们把只要求读该文件的进程称为“Reader进程” ,其余进程则称为“Writer 进程” 。
容许多个进程同时读一个共享对象,由于读操做不会使数据文件混乱。
但不容许一个Writer 进程和其余Reader 进程或 Writer 进程同时访问共享对象,由于这种访问将会引发混乱。
所谓“读者—写者问题(Reader-Writer Problem)”是指保证一个 Writer 进程必须与其余进程互斥地访问共享对象的同步问题。
读者—写者问题常被用来测试新同步原语。
很显然,只有多个读者时不冲突
记录型信号量机制
读和写之间是互斥的,因此须要一个信号量用于读写互斥Wmutex
另外若是有读的进程存在,另外的进程若是想要读的话,不须要同步也就是Wait(Wmutex)操做;
若是当前没有进程在读,那么须要Wait(Wmutex)操做,因此设置一个变量记录写者个数Readcount,能够用来判断是否须要同步
另外Readcount 会被多个读者进程访问,因此也是临界资源,因此设置一个rmutex 用于互斥访问Readcount
//两个信号量,一个用于读者互斥 readcount ,一个用于读写互斥
semaphore rmutex=l,wmutex=1;
int readcount=0;//初始时读者个数为0
//读者
void reader() {
do{
wait(rmutex);//读者先获取 readcount
if(readcount==0){//若是一个读者没有,第一个读者须要与写者互斥访问
wait(wmutex);
}
readcount++;//读者个数+1
signal(rmutex);//读者个数+1后,能够释放readcount的锁,其余读者能够进来
//开始慢慢读书......
wait(rmutex);//读者结束时,须要获取readcount的锁
readcount--;//退出一个读者
if (readcount==0) {//若是此时一个读者都没有了,还须要释放与读写互斥的锁
signal(wmutex);
}
signal(rmutex);//释放readcount的锁
}while(TRUE);
}
void writer(){
do{
wait(wmutex);//写者必须得到wmutex
//执行写任务....
signal(wmutex);//写任务结束后就能够释放锁
}while(TRUE);
}
//主程序
void main() {
reader();
writer();
}
写者相对比较简单,得到锁wmutex以后,进行写操做,不然等待wmutex
读者也是须要先得到锁,读操做后释放锁,可是由于多个读者之间互不影响,因此使用readcount记录读者个数,只有第一个读者才须要竞争wmutex,只有最后一个读者才须要释放wmutex
readcount做为读者之间的竞争资源,因此对readcount进行操做的时候也须要进行加锁
信号量集机制
将读者写者的问题复杂化一点,它增长了一个限制,即最多只容许 N个读者同时读。
在上面的解决方法中,能够不使用rmutex控制对readcount的互斥,能够构造一个读者个数的信号量readcountmutex,初始值设置为N
每次新增一个读者时,wait(readcountmutex),一个读者离开时signal(readcountmutex)
也可使用信号量集机制
int N;//最大的读者个数,也就是至关于图书馆的空位子,初始时空位子为N
semaphore L=N, mx=1;//定义两个信号量资源L和mx,分别用于控制读者个数限制和读写(写写)
void reader() {
do{
Swait(L, 1, 1);//获取空位子L,每次获取1个,>=1时可分配
Swait(mx, 1, 0);//获取与写的互斥量mx,每次获取0个,>=1时可分配,若是mx为1,也就是没有写者,读者均可以进来,不然一个都进不来
//进行一些读操做
Ssignal(L, 1);//释放一个单位的资源L
}while(TRUE);
}
void writer() {
do{
Swait(mx,1,1; L,N,0);//得到资源mx,每次获取1个,>=1时分配,得到资源L,每次得到0个,>=N时便可分配
//进行一些写操做
Ssignal(mx, 1);//释放资源mx
}while(TRUE);
}
void main(){
reader();
writer();
}
Swait(L, 1, 1);用于获取读者空位子没什么好说的
Swait(mx, 1, 0);做为开关,只要mx知足条件>=1,那么就能够无限制的进入(此例中有L的限制),一旦条件不知足,则全都不能进入,知足多读者,有写不能读的状况
对于写者中的Swait(mx,1,1; L,N,0);
他会获取mx,>=1时,获取一个资源,而且当L>=N时,分配0个L资源,也就是说一个读者都没有的时候才行
Swait(mx, 1, 0); 与Swait( L,N,0);都是需求0个,至关于开关判断
总结
以上为借助“进程同步的API”,信号量,管程等方式完成进程同步的经典示例,例子来源于《计算机操做系统》
说白了,就是用 wait(S) Swait(S) signal(S) Ssignal(S)等这些“方法”描述进程同步算法
可能会以为这些内容乱七八糟的,根本没办法使用,的确这些内容全都没办法直接转变为代码写到你的项目中
可是,这些都是解决问题的思路
不论是信号量仍是管程仍是什么,不会须要你从头开始实现一个信号量,而后.......也不须要你从头开始实现一个管程,而后......
不论是操做系统层面,仍是编程语言层面,仍是具体的API,万变不离其宗
尽管这些wait和signal的确不存在,可是,可是,可是编程语言中极可能已经提供了语意相同的方法供你调用了
也就是说,你只须要理解同步的思路便可,尽管没有咱们此处说的wait(S),可是确定有对应物。