操作系统(5)互斥与同步

1.计算机内部的同步与互斥的现象 

在单CPU、多CPU以及分布式系统中,有多个进程并发甚至并行执行。对资源的共享和竞争使得并发进程之间相互制约,因为该原因通常产生意想不到的错误,且在程序逻辑上体现不出来。

原因:①由于共享某些资源,(如变量、文件、设备)等,一个进程的执行可能影响其他进程的执行结果②与同一共享资源有关的程序段分散在各进程中,而且各进程的相对执行速度不可预知③由于每次并发执行速度顺序不同,并发进程的执行结果将不确定(无法再现),甚至可能导致错误;但由于这不可再现的原因,导致bug很难定位排除。

并发相关术语

1.原子操作:一个函数(原语)或动作的指令序列不可分割,要么作为一个整体执行(不可中断),要么都不执行

2.临界资源:一次仅允许一个进程独占使用的不可剥夺的资源

3.临界区:进程访问临界资源的那段程序代码。一次仅允许一个进程在临界区中执行

4.互斥:当一个进程正在临界区中访问临界资源时,其他进程不能进入临界区

5.同步:合作的并发进程需要按先后次序执行,例如:一个进程的执行依赖于合作进程的消息或者信号,当一个进程没有得到来自于合作进程的消息或者信号时需要阻塞等待,直到消息或者信号到达后才被唤醒

6.死锁:多个进程全部阻塞,形成等待资源的循环链

7.饥饿:一个就绪进程被调度程序长期忽视、不被调度执行;一个进程长期得不到资源

2.并发原理

并发编程的关键是对于进程调度的理解,在一些常见的错误例子中,往往都是未对临界资源进行保护,对临界资源的访问到中途某一句代码即被终止,此时另一进程在CPU上进入运行态再次修改了临界资源,当前一个资源恢复为运行态时,此时的临界资源已经发生了变化,而执行还是在断点处恢复重新执行的,因此发生了错误。

2.1 竞争条件

竞争条件:多个线程或者进程在读写共享数据时,最终结果依赖于它们的指令执行顺序

2.2 OS应该关注的问题

并发进程间应该实现同步和互斥

①跟踪不同进程,管理进程状态

②为每个活跃进程分配和释放各种资源

③保护每个进程数据和物理资源,避免其他进程的有意无意干涉

④一个进程的功能和输出结果正确与否,应与其他并发进程的相对执行速度无关

2.3 进程交互

①进程间的资源竞争:进程间互不知道对方的存在,竞争进程间没有任何的信息交换但执行过程可能受到其它竞争者的影响,如试图使用已分配的设备时将阻塞,竞争进程互斥访问临界资源,一次只允许一个进程在临界区中执行,其它试图进入临界区的进程将被阻塞

②进程间通过共享的合作:进程间接知道对方的存在,进程之间共享某些资源,但是无法确切知道对方的存在,多个进程可以同时读一个数据项,但必须互斥“写”(将在读者写者问题中详细谈到),多个进程读写共享数据时,需要保持数据的一致性

③进程间通过通信的合作:进程直接知道对方的存在,多个进程通过PID互相通信(发送消息和接收消息),实现同步和协调各种活动,传递消息时进程间未共享资源,则不需要互斥,但可能导致死锁和饥饿

2.4 互斥的要求

①对相关进程执行速度和处理器数目没有限制

②强制互斥(忙则等待):一次只允许一个进程进入临界区。有进程正在临界区中执行时,其他请求进程等待,待该进程退出后,从多个请求进程中选择一个进入临界区

③有限等待:请求进程应该在有限的等待时间内进入临界区,不能造成进程死锁或者饥饿

④有空让进:当临界区空闲时,请求进程可立即进入

⑤让权等待:当进程不能进入临界区时,应该立即释放CPU,避免忙等

1、下列属于临界资源的是(B) 。
A.磁盘存储介质 B. 全局的公共队列
C. 局部变量 D. 可重入的代码(指的是可同时被多个进程执行的纯代码)
2个正在访问临界资源的进程由于申请等待I/O操作而被中断时,它 (D)。
A. 可以允许其它进程进入该进程的相关临界区
B. 不允许其它进程进入任何临界区
C. 不允许其它进程抢占处理器
D. 可以允许其它进程抢占处理器,但不能进入相关临界区

3.互斥

3.1 使用硬件实现互斥

中断禁用:单CPU系统中,“关中断”可以实现临界区互斥。不应该让用户关中断,长时间关中断导致串行执行进程、系统的执行效率较低,不适合多处理器系统。每个CPU有各自的中断开关,禁止中断仅有对执行关中断指令的那个CPU有效,其他CPU上的进程仍可继续运行并访问共享资源

    while(true){
        /*关中断*/
        /*临界区*/
        /*开中断*/
        /*其余部分*/
    }

专用机器指令:两种机器指令(原子地在一个指令周期内执行):

#include <iostream>
#include <cmath>

using namespace std;
int bolt = 0;
/*比较word和testVal,相等时word赋值为newVal。返回word值*/
int compare_and_swap(int *word,int testVal,int newVal){
    int oldVal;
    oldVal = *word;
    if(oldVal == testVal){
        *word = newVal;
    }
    return oldVal;
}
/*可用compare_and_swap实现互斥*/
void P(int i){/*进程Pi代码,共n个进程*/
    while(ture){
        while(compare_and_swap(bolt,0,1)){
            //此处不做任何事情
            //解释:bolt是门栓的意思,在此处用来指代进程是否执行代码的钥匙
            //bolt初值为0,0代表是开锁,资源可用,为bolt为1时,代表资源被占用,进程等待
            //进入临界区时bolt设置为1,退出时设置为0
        }//可能进程在此处刚好它的时间片就完了,实际上因为资源被占用它啥也没干,也就是导致了忙等
        /*临界区代码*/
        bolt = 0;//执行完临界代码,复原bolt为可用状态
        /*其余部分*/
    }
}



int main(){

}

#include <iostream>
#include <cmath>

using namespace std;
int bolt = 0;
/*比较word和testVal,相等时word赋值为newVal。返回word值*/
int compare_and_swap(int *word,int testVal,int newVal){
    int oldVal;
    oldVal = *word;
    if(oldVal == testVal){
        *word = newVal;
    }
    return oldVal;
}
/*可用compare_and_swap实现互斥*/
void P0(){/*进程Pi代码,共n个进程*/
    while(ture){
        while(compare_and_swap(bolt,0,1));
        /*临界区代码*/
        bolt = 0;//执行完临界代码,复原bolt为可用状态
        /*其余部分*/
    }
}
/*可用compare_and_swap实现互斥*/
void P1(){/*进程Pi代码,共n个进程*/
    while(ture){
        while(compare_and_swap(bolt,0,1));
        /*临界区代码*/
        bolt = 0;
        /*其余部分*/
    }
}


int main(){
    p0();
    p1();
}

试着分析上述程序。
P0先运行,此时P0将bolt设置为1,进入临界区,此时p1并发执行,试图执行临界区代码,在while循环中不断自旋,直到结束时间片,p0临界区代码执行完成,将bolt设置为0,p1得到CPU后开始执行临界区代码,退出后又将bolt设置为0.

完全类似地,有:

int bolt = 0;

void exchange(int *register,int *memory){
    int temp;
    temp = *memory;
    *memory = *register;
    *register = temp;
}

void P0(){
    int key0 = 1;
    while(key0 == 1){
        exchange(&key0,&bolt);
    }
    /*临界区代码*/
    bolt = 0;
    /*其余部分*/
}

void P1(){
    int key1 = 1;
    while(key1 == 1){
        exchange(&key1,&bolt);
    }
    /*临界区代码*/
    bolt = 0;
    /*其余部分*/
}

那么这两种办法确实可以实现互斥,优点有:

①进程数量任意,适合于共享内存(要访问全局变量bolt)的单CPU和多CPU系统

②可支持多个临界区互斥,各临界区资源有自己的bolt

缺点有:

①忙等(自旋等待):导致CPU利用效率较低

②可能饥饿:多个进程进入临界区时,选择哪个进程是随机的

③可能死锁:如按优先级调度CPU时,设低优先级的P1入临界区后被中断,高优先级的P2抢占了CPUP2试图使用同临界资源时被拒绝且开始忙等循环;P1优先级低,无法被调度执行及退出临界区。此时P1P2进入死锁状态。

4.使用信号量实现同步互斥

4.1 信号量含义以及使用

信号量定义:用于进程间传递信号的一个整数值,在信号量上可以执行三种操作,即初始化,递减和递增,这三种操作都是原子操作。递减操作用于阻塞一个进程,递增操作用于接触一个进程的阻塞,信号量也称为计数信号量或者一般信号量,信号量的值可以用来表示可用资源的个数

使用分析:

当用来互斥时,s的初值为1,取值为1~ -(n-1),表示为当s=1时,有一个临界资源可用,一个进程可进入临界区,s=0时,临界资源已经分配,一个进程已经进入临界区执行代码,s<0时临界区已被占用,有s的绝对值个阻塞进程正等待进入

当用来同步时,s的初值将为>=0的值,表示可供进程使用的临界区资源个数(或者理解为可进入临界区的进程个数),s<0该资源的等待队列长度

4.2 信号量的访问和使用

信号量s只能被semWait(s)和semSignal(s)两个原语访问

semWait(s):本进程请求分配一个资源进行使用

semSignal(s):本进程释放一个资源

semWait和semSignal必须成对出现

用于互斥时,位于同一进程内,临界区前/后

用于同步时,交错出现在两个合作进程内

多个semWait( )的次序不能颠倒,否则可能导致死锁。用于同步的semWait(s1)应出现在用于互斥的semWait(s2)之前,多个semSignal( )操作的次序可任意。

4.3 信号量原语的定义

struct semaphore{
    int count;//信号量的值,实际上指代的是可用资源个数
    queueType queue;//因该信号量而阻塞的进程队列
}

semWait(semaphore s){
    s.count--;//进入该函数即马上减1
    if(s.count <0 ){//当最后一个可用的时候,s.count==1的,减1之后就到0了,此时该进程不阻塞,其后所有进程阻塞
        s.queue.push(pi);//加入阻塞队列,记录
        block(pi);//阻塞进程
    }
}

semSignal(semaphore s){
    s.count++;
    if(s.count <=0){//当小于等于0的时候,此时还有进程被阻塞着,假如>1了就意味着有多个可用资源
        process pi = s.queue.pop();//强信号量方式,弱信号量是随机选择的
        readyQueue.push(p1);//插入就绪队列
    }
    //当>0时,进程有资源可供直接使用而且没有进程被阻塞
}

同样的,我们需要思考如何保证semWait和semSignal被原子化地执行?

方案1:使用开/关中断方法

semWait(semaphore s){
    关中断;
    s.count--;//进入该函数即马上减1
    if(s.count <0 ){//当最后一个可用的时候,s.count==1的,减1之后就到0了,此时该进程不阻塞,其后所有进程阻塞
        s.queue.push(pi);//加入阻塞队列,记录
        block(pi);//阻塞进程
    }
    开中断;
}

semSignal(semaphore s){
    关中断;
    s.count++;
    if(s.count <=0){//当小于等于0的时候,此时还有进程被阻塞着,假如>1了就意味着有多个可用资源
        process pi = s.queue.pop();//强信号量方式,弱信号量是随机选择的
        readyQueue.push(p1);//插入就绪队列
    }
    //当>0时,进程有资源可供直接使用而且没有进程被阻塞
    开中断;
}

方案2:使用compare_and_swap或者exchange以及自旋等待的方法,只需要把核心代码放到临界区就行,这里省略

4.4 用信号量实现互斥

对每一临界资源设一个信号量s,初值=1,实现互斥:每个进程在进入临界区前semWait,退出后semSignal

临界区内不应有可能引起阻塞或死锁的因素。
const int n = N;//N是进程个数
semaphore s = 1;//初始化

void P(int i){
    while(true){//while是压力测试,不管他
        semWait(s);
        /*临界区*/
        semSignal(s);
        /*其他部分*/
    }
}
void main(){
    parbegin(P(1),P(2),...,P(n));//n个进程并发
}

两个进程实现互斥的例子

void P0(){
    semWait(s);
    /*临界区*/
    semSignal(s);
    /*其他部分*/
}

void P1(){
    semWait(s);
    /*临界区*/
    semSignal(s);
    /*其他部分*/
}
void main(){
    P0();
    P1();
}
试着分析上述代码.
P0先执行,经过semWait(s)该进程获得一个资源,信号量--,资源被上锁
,对p1不可用,p0执行结束后,p0使得信号量++,p1解除阻塞
,注意,semWait不是循环,因此被进程被唤醒后不会再次执行semWait,
而是直接进入临界区执行代码,执行结束后,释放资源,s恢复为1,
对于s的值:
p0:semWait(s) -> s =    0
p1:semWait(s) -> s =   -1   (有1个进程正被阻塞)
p0:semSignal(s) -> s =  0   (进程p1)被唤醒
p1:semSignal(s) -> s =  1   恢复临界区资源为可用状态

 一个比较直观的图

4.5 用信号量实现同步

同步时,对每类资源信号量s,初值>=0,表示可用资源个数
资源:可以是同物理资源的不同状态,如缓冲的空和满。对空缓冲和满缓冲分别设置信号量。
要求实现:P1已执行过A后,P2才能开始执行B信号量s,初值为0
void p0(){
    //代码段A
    semSignal(s);
}

void p1(){
    semWait(s);
    //代码段B
}

设有原始进程代码,考虑以下代码

void p0(){
    n++;//1
}

void p1(){
    printf("%d",n);//2
    n=0;//3
}

实现123顺序

semaphore s = 0;//初始化,123的需求

void p0(){
    n++;//1
    semSignal(s);
}

void p1(){
    semWait(s);
    printf("%d",n);//2
    n=0;//3
}

实现231顺序

semaphore s = 0;//初始化,231的需求

void p0(){
    semWait(s);
    n++;//1
}

void p1(){
    printf("%d",n);//2
    n=0;//3
    semSignal(s);
}

以上是同步的实现方法,若要求实现互斥,则:

semaphore s = 1;//初始化,互斥要求

void p0(){
    semWait(s);
    n++;//1
    semSignal(s);
}

void p1(){
    semWait(s);
    printf("%d",n);//2
    n=0;//3
    semSignal(s);
}

注意:semWait的顺序不能颠倒

例如:S和Q是两个初值为1的信号量,若颠倒了顺序


void p0(){
    semWait(S);
    semWait(Q);
    ...
    semSignal(Q);
    semSignal(S);
}

void p1(){
    semWait(Q);
    semWait(S);
    ...
    semSignal(S);
    semSignal(Q);
}
设想p0先执行,先申请了信号量S的资源,S.count--,S不可用
此时换p1执行,p1申请Q的资源,Q.count--,Q不可用,p0再执行
它因为Q不可用而被阻塞,在换p1执行,它因为S不可用而被阻塞
两个进程都被阻塞,发生死锁。

5. 利用信号量实现生产者/消费者问题

5.1 生产者/消费者问题模型

一组生产者进程产生数据,将数据输送到缓冲区

一组消费者进程从缓冲区中取出数据使用。有限缓冲:假设缓冲池大小固定,包含k个缓冲区。生产者和消费者共用一个循环缓冲池。无限缓冲:缓冲区的数量没有限制

5.2 有限缓冲

一组生产者进程和一组消费者进程共用一个sizeofbuffer个缓冲区的缓冲池来交换数据

资源、约束条件以及信号量设置

1.缓冲池一次只能让一个进程访问。(互斥):设一信号量s,初值为1

2.生产者需要空缓冲来发送数据。(同步):设一信号量为empty,初值为sizeofbuffer,表示有多少个空缓冲

3.消费者需要满缓冲来获取数据。(同步):设以信号量为full,初值为full

const int n = sizeofbuffer;

semaphore empty,full,s;

//1.信号量的初始化
void init(semaphore &empty,semaphore &full,semaphore &s){
    empty.count = n;//空缓冲有n个
    full.count =0;//满缓冲有0个
    s.count =1;//互斥资源可访问
}

//2.生产者进程
void producer(){
    生产一个数据;
    //接下来要做的是将数据送入缓冲池的空缓冲去,需要资源:空缓冲,故申请
    semWait(empty);
    //接下来生产者进程要访问临界区资源,故需要互斥保护
    semWait(s);
    将数据送入缓冲区
    semSignal(s);//signal的顺序任意
    semSignal(full);//得到了一个满缓冲
}

//3.消费者进程
void consumer(){
    //注意并不是完全对称的,因为消费数据涉及到访问临界区资源
    semWait(full);
    semWait(s);
    取出数据;
    semSignal(empty);
    semSignal(s);
    消费数据;
}

注意需要分清楚semWait的作用到底是用来互斥的还是来同步的,我们认为semWait(empty)用来申请空缓冲资源,semWait(s)用来申请可用资源权限,按照这个理解安排semWait的顺序就不会导致死锁。

来看一个导致死锁的例子

#define sizeofbuffer 8

const int n = sizeofbuffer;

semaphore s,empty,full;

void init(){
    s.count = 1;
    full.count = 0;
    empty.count = n;
}

void consumer(){
    生产数据;
    semWait(s);
    semWait(empty);
    向缓冲区送数据;
    semSignal(s);
    semSignal(full);
}

void producer(){
    semWait(full);
    semWait(s);
    在缓冲区取出数据;
    semSignal(s);
    semSignal(empty);
    消费数据;
}

试分析上述程序是否有可能产生死锁,以及产生的过程。
上述程序可能产生死锁,首先是生产者进程申请访问临界区资源,为临界区资源上锁,s->0,
同时生产者进程生产数据,填满缓冲区,empty->0,full->7
生产者时间片刚好超时,换消费者获取CPU,执行代码
此时消费者使得:s -> -1,消费者阻塞,消费者时间片超时
换生产者获取CPU,生产者从断点处继续执行,由于semWait(empty)使得empty->-1,生产者阻塞
当再次调度进程使用CPU时,此时消费者仍然无法获取临界区资源,继续阻塞,造成死锁。 

5.3 无限缓冲 

无限缓冲依然需要对临界区资源进行互斥访问保护,与有限缓冲不同的是,由于缓冲是无限的,因此不需要empty信号量供给生产者,但消费者依然需要full信号量来获取满缓冲

semaphore s,full;

void init(){
    s.count = 1;
    full.count = 0;
}

void consumer(){
    生产数据;
    semWait(s);
    向缓冲区送数据;
    semSignal(s);
    semSignal(full);
}

void producer(){
    semWait(full);
    semWait(s);
    在缓冲区取出数据;
    semSignal(s);
    消费数据;
}

练习:

设有三个进程:进程get读数并送到buf1,进程 copy复制buf1buf2,进程put打印buf2中的数据。用信号量实现getcopyput的同步。
思路分析:做这个题首先需要分析需要的资源是哪些?资源的状态怎么转移?
需要的资源有buf1,buf2
get进程使得empty的buf1变为full的buf1(输入empty的buf1,输出full的buf1)
copy进程使得empty的buf2变为full的buf2(输入full的buf1输出full的buf2)
put进程(输入full的buf2)
4个信号量代表4种资源:S1buf1S2buf1S3buf2S4buf2,初值 1010
理清思路后写出以下代码:

semaphore emptyBuf1,emptyBuf2,fullBuf1,fullBuf2;

void init(){
    emptyBuf1.count =1;
    emptyBuf2.count =1;
    fullBuf1.count  =0;
    fullBuf2.count  =0;
}
//get进程使得empty的buf1变为full的buf1(输入empty的buf1,输出full的buf1)
void get(){
    semWait(emptyBuf1);
    输入数据到buf1;
    semSignal(fullBuf1);
}
//copy进程使得empty的buf2变为full的buf2(输入full的buf1输出full的buf2)
void copy(){
    semWait(fullBuf1);
    semWait(emptyBuf2);
    把buf1的内容输入到buf2中
    semSignal(fullBuf2);
    semSignal(emptyBuf1);
}
//put进程(输入full的buf2)
void put(){
    semWait(fullBuf2);
    I/O;
    semSignal(emptyBuf2);
}

6.读者/写者问题

6.1 读者/写者 问题模型

有一个共享的数据对象,①允许多个读者进程同时读②一次只允许一个写者进程写,当一个写者正在写时,不允许其它任何读者或者写者同时访问该共享对象。

6.2 读者优先

当至少已有一个读者正在读时,随后的读者直接进入,开始读数据对象。但写者将等待。当一个写者正在写时,随后到来的读者和写者都将等待。

信号量设置:①一次只能让一个写者或一群读者访问数据。设一互斥信号量wsem,初值为1 ②正在读数据的读者数目由全局变量readcount表示(初值为0),它被多个读者互斥访问。(第1个读者需对数据加锁,最后一个读者对数据解锁),为readcount设一互斥信号量x,初值为1

代码如下:

int readCount = 0;

semaphore x,wsem;

void init(){
    x.count = 1;
    wsem.count = 1;
}

void reader(){
    //1.读者到来,读者的数量+1,为保证互斥,对readCount用信号量x进行保护
    semWait(x);
    readCount ++ ;
    if(readCount == 1){
        //2.第一个人对数据加锁
        semWait(wsem);
    }
    semSignal(x);
    //3.对readCount的操作结束,释放资源
    读数据对象;
    //4.读者离开,保护readCount变量
    semWait(x);
    readCount--;
    if(readCount == 0){
        //5.最后一个人解锁数据
        semSignal(wsem);
    }
    semSignal(x);
}

void writer(){
    semWait(wsem);
    写数据对象;
    semSignal(wsem);
}

6.3 写者优先

当一个写者声明想写时,不允许新的读者进入数据对象,只需等待已有的读者读完即可开始写,可以避免写者饥饿。

int readCount = 0,writeCount = 0;
semaphore rsem,wsem,s1,s2,s3;

//对于读进程, 还需要一个额外的信号量。在rsern上不允许建造长队列, 否则写进程将无法跳过这
//个队列, 因此只允许一个读进程在rsern上排队, 而所有其他读进程在等待rsern前, 在信号量z上排队

//所以,也就是说s1用来保证队列中只有一个进程正在排队
//s3用来保证互斥访问readCount

void init(){
    rsem.count = 1;
    wsem.count = 1;
    s1.count = 1;
    s2.count = 1;
    s3.count = 1;
}


void reader(){
    semWait(s1);
    semWait(rsem);
    semWait(s3);
    readCount ++ ;
    if(readCount == 1){
        semWait(wsem);
    }
    semSignal(s3);
    semSignal(rsem);
    semSignal(s1);
    读者读数据;
    semWait(s1);
    readCount -- ;
    if(readCount == 0){
        semSignal(wsem);
    }
    semSignal(s1);
}

void writer(){
    semWait(s2);//s2用来保护writeCount
    writeCount++;
    if(writeCount == 1){
        semWait(rsem);//当有写者声明想写的时候,等待所有的读者离开
    }
    semSignal(s2);

    semWait(wsem);//申请写的权限
    写数据;
    semSignal(s2);
    writeCount--;
    if(writeCount == 0){
        semSignal(rsem);//没有写者的时候,开放读的权限
    }
    semSignal(wsem);//释放写权限
    semSignal(s2);
}

练习1:

设有两个机器人拣黑、白棋子。 P1拣白子,P2拣黑子,交替拣,互斥拣。 用信号量实现P1P2的同步。
思路解析:也是类似于读者写者互斥的问题,首先也是要分析需要说明资源,资源的状态有几种?
资源:白子、黑子,资源的状态:白子可被捡起来(1,0),黑子可被捡起来(1,0)
semaphore w,b;

void init(){
    w.count = 1;
    b.count = 0;
}

void p1(){
    semWait(b);
    捡起来;
    semSignal(w);
}

void p2(){
    semWait(w);
    捡起来;
    semSignal(b);
}

 练习2:

桌上有空盘,允许放只水果。爸爸可向盘中放桔子,也可放苹果。儿子专等吃盘中的桔子,女儿专等吃盘中的苹果。规定当盘空时次只能放只水果供吃者取用。 试用信号量实现爸爸、儿子、女儿三个并发进程的同步。
思路分析:在本题中,充当生产者的是爸爸进程,充当消费者的是儿子女儿进程,资源是爸爸进程产生的苹果、桔子,资源的状态是盘中是否有苹果?盘中是否有桔子?爸爸进程需要的是空盘子,儿子女儿进程需要的装有苹果、桔子的盘子,根据以上分析,可知应该设置三个信号量,Sa表示盘中苹果的情况,So表示盘中桔子的情况,S表示盘子的情况,写出以下代码

semaphore So,Sa,S;

void init(){
    So.count = 0;
    Sa.count = 0;
    S.count = 1;
}

int placeFruit(){
    ...
    return fruitId;
}

void father(){
    semWait(s);
    int id = placeFruit();
    if(id == 1){//放的是苹果
        semSignal(Sa);
    }else if(id == 2){//放的是桔子
        semSignal(So);
    }
}

void daughter(){
    semWait(Sa);
    取苹果;
    semSignal(S);
}

void son(){
    semWait(So);
    取桔子;
    semSignal(S);
}

void main(){
    parbegin(father(),daughter(),son());
}

7. 管程 

7.1 管程的提出及其定义

在多种程序设计语言中实现了管程,允许用管程锁定任何对象,如变量、数组、链表甚至数组中的每一个元素

管程将共享对象以及能对它进行的所有操作(原本分散在各进程的临界区)集中(封装)在一个模块中。管程本身结构保证各操作的互斥执行。各进程调用相关操作,可防止进程有意/无意的互斥/同步操作

7.2 管程的组成

Monitor monitor{
    声明局部数据和条件变量;
    //只有管程内部的进程才能访问它们,临界资源

    procedure P1(...){
        ...
    }
    procedure P2(...){
        ...
    }
    ...
    procedure Pn(...){
        ...
    }
    /*一组对共享的局部数据和条件变量进行操作操作的过程*/
    /*一个进程通过调用某个过程,进入管程*/
    /*管程本身保证互斥,故管程中只能有有一个活跃进程*/

    {初始化代码部分;}//初始化局部数据
} 

7.3 管程 -- 条件变量

条件变量:表示进程正在等待的资源或原因。只用于维护等待队列,但没有相关联的值

有两个原语可以操作条件变量:

cwait(c):条件不满足时,调用cwait(c)的进程被放到条件变量c的等待队列中

csignal(c):唤醒一个条件变量c的阻塞进程

如果没有可用的资源,调用cwait(c)的进程将被阻塞,直到另一进程释放资源时,调用csignal(c)将其唤醒

7.4 管程 -- 内部队列

 

7.5 实现互斥与同步原理

互斥:管程内总是只有一个活跃进程,实现对共享数据/资源的互斥使用。不能进入管程的进程排在入口等待队列中。其它已经进入管程中的进程处于阻塞状态

同步:不能满足进程Q请求时,cwait(x)使Q进入x的条件队列(阻塞)。另一进程P释放资源时,调用csignal(x)将Q唤醒。然后Q继续执行而P进入紧急队列(名为阻塞,实为就绪),紧急队列中的进程优先于入口等待队列的进程

7.6 使用管程解决消费者/生产者问题

//1.定义管程
monitor boundedBuffer;
char buffer[N];
int in,out,count;//count是缓冲池中的有效缓存数目
cond notFull,notEmpty;//条件变量

//生产者操作
void append(char x){
    if(count == N){
        cwait(notFull);//缓冲池满,阻塞
    }
    buffer[in]=x;
    in = (in+1)%N;
    count++;
    csignal(notEmpty);
}

//消费者操作
void take(char x){
    if(count == 0){
        cwait(notEmpty);
    }
    buffer[out] = x;
    out = (out+1)%N;
    count --;
    csignal(notFull);
}

int main(){
    in = out =0;
    count =0;
    return 0;
}

然后在用户程序中使用该操作

void producer(){
    char x;
    produce(x);
    append(x);
}

void consumer(){
    char x;
    take(x);
    consume(x);

}

8.消息传递

8.1 消息传递模型

进程间通过“消息传递”交换信息

消息传递的两个原语:

send(P,message):给进程P发送消息

receive(Q,message):接受来自进程Q的消息

8.2 消息传递 -- 同步

同步:发送者发出消息后,接收者才能接收

当一个进程执行send()时,该进程可以:①不阻塞,继续执行②被阻塞,直到这个消息被接收者接收

当一个进程执行receive()时,该进程可以:①不阻塞,接收已发来的消息或放弃接收,继续执行②被阻塞,直到所等待的消息到达

无阻塞式的send和阻塞式的receive最常用

8.3 消息传递 -- 寻址

直接寻址方式:指明目标进程或者源进程的标识ID,公共服务进程使用receive()时,不指明源进程(隐式)

间接寻址方式:通过信箱发送和接收消息

 8.4 消息传递 -- 消息格式

8.5 消息传递 -- 排队原则

消息队列:先入先出

可指定消息优先级,高级消息先被接收

接受者可检查消息队列,并选择接收哪个消息

8.6 使用消息传递机制解决生产者/消费者问题

mayconsume信箱:用作有界缓冲池,存放消息
mayproduce信箱:空消息邮箱。最初填满空消息。 空消息数=空缓冲数。只用于同步。
void producer(){
    message pmsg;//生产者产生的数据
    receive(mayProduce,pmsg);//接收空消息(空缓冲数-1),无空消息时进程阻塞
    pmsg = produce();
    send(mayComsume,pmsg);//将pmsg消息放到缓冲池中
}

void consume(){
    message pmsg;
    receive(mayComsume,pmsg);
    consume(pmsg);
    send(mayProduce,null);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值