进程同步经典例题

1.信号量机制

信号量机制即利用pv操作来对信号量进行处理。

什么是信号量信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。

当它的值大于0时,表示当前可用资源的数量;p(可理解占用)

当它的值小于0时,其绝对值表示等待使用该资源的进程个数。v(可理解生产,释放占用资源)

注意,信号量的值仅能由PV操作来改变。

     一般来说,信号量S³0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1;若S£0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。

2.PV操作

什么是PV操作?

p操作(wait):申请一个单位资源,进程进入

经典伪代码

[cpp] view plain copy
  1. wait(S){  
  2.     while(s<=0)"white-space:pre;">   //如果没有资源则会循环等待;  
  3.         ;  
  4.     S-- ;  
  5. }  

v操作(signal):释放一个单位资源,进程出来

[cpp] view plain copy
  1. signal(S){  
  2.     S++ ;  
  3. }  

p操作(wait):申请一个单位资源,进程进入
v操作(signal):释放一个单位资源,进程出来
PV操作的含义PV操作由P操作原语和V操作原语组成(原语是不可中断的过程),对信号量进行操作,具体定义如下:
    P
S):①将信号量S的值减1,即S=S-1
           
②如果S<=0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。
    V
S):①将信号量S的值加1,即S=S+1
           
②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。

PV操作的意义 :我们用信号量及PV操作来实现进程的同步和互斥。PV操作属于进程的低级通信。

使用PV操作实现进程互斥时应该注意的是:
    
1)每个程序中用户实现互斥的PV操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分支,要认真检查其成对性。
    
2PV操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。
    (3)互斥信号量的初值一般为1


三个同步问题

- 生产者-消费者问题
- 哲学家进餐问题
- 读者-写者问题


生产者-消费者问题

进程同步经典例题_第1张图片
注:所有的都是,当缓冲池满了,生产者就不可往进存放,必须等待;当缓冲池空了,消费者就不可从中取出,必须等待

1.记录型信号量方法

定义:

semaphore: 信号量

wait:(P操作)申请资源

signal:(V操作)释放资源

S:可用资源数目;当S<0, 表示有某些进程正在等待该资源,S的绝对值表示当前等待资源的进程数

mutex:互斥信号量(互斥锁)

empty: 表示缓冲池中空缓冲区数量

full:表示缓冲池中满缓冲区数量(这两个称为资源信号量

临界区:每个进程中访问临界资源的那段代码叫临界区

进入区:临界区前,用于检查临界资源是否正在被访问的那段代码,可进入临界区则改标志为被访问;作用:申请资源

退出区:临界区后,用于将临界区正在被访问的标志恢复为未被访问的那段代码;作用:释放资源

假定生产者与消费者中有n个缓冲区(临界资源包含缓冲区):利用mutex实现进程对缓冲区的互斥作用,利用信号量empty和full表示表示缓冲区的空的和满的数量。假定生产者消费者等效,缓冲区未满,生产者才能把消息送入缓冲池;缓冲区未空,消费者才能从缓冲区中取出一个消息。 伪代码如下:


Var mutex, empty, full: semaphore := 1, n, 0;    //设置各个初始值
    buffer: array[0, ..., n-1] of item;          //代表缓冲区,用来存放消息
    in, out: integer := 0, 0;                    //存入,取出地址初始值(类似指针)
    begin
        parbegin                                 //生产者
            proceducer: begin
                        repeat
                        ...
                        producer an item nextp;  //生产者“生产”一个消息
                        ...
                        wait(empty);             //生产者申请一个空缓冲区(即申请访问临界资源)
                        wait(mutex);             //进程进入临界区,加锁,使其他进程无法访问该临界资源
                        buffer(in) := nextp;     //把消息存入缓冲区(访问临界资源)
                        in := (in + 1) mod n;    //存入地址指向下一个缓冲区
                        signal(mutex);           //退出临界区,开锁
                        signal(full);            //释放一个满缓冲区
                        until false;
                    end
            consumer:   begin                    //消费者
                        repeat
                        wait(full);              //消费者申请一个满缓冲区(即申请访问临界资源)
                        wait(mutex);             //进入临界区,加锁
                        nextc := buffer(out);    //取出缓冲区中消息(访问临界资源)
                        out := (out + 1) mod n;  //取出地址指向下一个缓冲区
                        signal(mutex);           //退出临界区, 开锁
                        signal(empty);           //释放空缓冲区
                        consumer the item in nextc;
                        until false
                    end
        parend
    end

注: 每个程序中wait(mutex)和signal(mutex)必须成对存在,另外资源信号量也同理,且两个wait不能互相变换顺序; 当in=out时,缓冲区为空

2.AND信号量方法

利用Swait代替wait,用Ssignal代替signal

Var mutex, empty, full: semaphore := 1, n, 0;    
    buffer: array[0, ..., n-1] of item;          
    in, out: integer := 0, 0;                   
    begin
        parbegin                                 
            proceducer: begin
                        repeat
                        ...
                        producer an item nextp;  
                        ...
                        //空缓冲区和锁都获取到才能访问(防止死锁)
                        Swait(empty, mutex); 
                        buffer(in) := nextp;     
                        in := (in + 1) mod n;    
                        Ssignal(mutex, full);                     
                        until false;
                    end
            consumer:   begin                    
                        repeat
                        Swait(full, mutex);                          
                        nextc := buffer(out);    
                        out := (out + 1) mod n;  
                        Ssignal(mutex, empty);                   
                        consumer the item in nextc;
                        until false
                    end
        parend
    end

3.管程方法

由于信号量机制中wait和signal分散在各个进程中进行(每个进程必须自己有PV操作),比较麻烦,容易死锁。so,用管程(同步工具)。
管程简单的讲就是一个门(感觉就是一个带同步功能的接口),他把共享数据和对数据的操作关在里面,而每次只能进一个进程(实现同步),外面的进程不能访问到管程内,而且进入管程的进程只能调用管程提供的对数据操作的功能

  • 管程定义了公共的数据结构(进程是私有的)
  • 管程对数据结构的操作主要是同步操作和初始化(进程是顺序操作)
  • 管程的目的是为了解决共享资源的互斥访问问题(进程是为了解决并发),管程不能与调用者并发
  • 管程只能被调用(其实就是个接口)(进程是动态的,由创建而诞生,撤销而消亡)

PC:管程
count:缓冲池中已有的产品数目(count>=n时,表示缓冲池已满;count<=n时,表示缓冲池空了)
put(item):生产者把生产的产品投放到缓冲池
get(item):消费者从缓冲池中取出一个产品

producer: begin
            repeat
            produce an item int nextp;
            PC.put(item);
            until false;
            end
consumer: begin
            repeat
            PC.get(item);
            consumer the item in nextc;
            until false;
            end

哲学家进餐问题

进程同步经典例题_第2张图片

1.记录型信号量方法

桌上的筷子可视为临界资源,每个时间只允许一个哲学家使用。那么可用一个信号量表示一个筷子(这里有5个信号量),则

Var chopstick: array[0,...,4] of semaphore;
...                                       //把所有信号量初始化为1
repeat                                    //第i个哲学家活动如下
    wait(chepstick[i]);                   //获取左边的筷子
    wait(chepstick[(i + 1) mod 5]);       //获取下一个(右边)的筷子
    ...
    eat;                                  //都成功,进餐
    ...
    signal(chepstick[i]);
    signal(chepstick[(i + 1) mod 5]);
    ...
    think;
until false;

注:上述机制可能导致死锁(比如每个人都得到了左边的筷子,等着右边的筷子。。。)

2.AND信号量方法

每个哲学家先获得两个临界资源(一双筷子)才能进餐

repeat
    think;
    Sswait(chopstick[(i + 1) mod 5], chopstick[i]);
    eat;
    Ssignal(chopstick[(i + 1) mod 5], chopstick[i]);
until false;

读者-写者问题

特点:


  • 允许多个进程同时读一个共享对象(Reader)
  • 不允许一个进程同时与其他读进程或者其他写进程操作同一个共享对象(Writer进程与其他进程互斥)
  • 读者-写者问题常被用来测试新的同步原语

wmutex :写互斥信号量
rmutex :读互斥信号量
readcount:正在读的进程数目(当Readcount=0时,才需要执行Wait(Wmutex))
Reader:读进程
Writer:写进程

1.记录型信号量方法

Var rmutex, wmutex: semaphore :=1, 1;
    Readcount: integer := 0;                       //读进程数目初始化0
    begin
    parbegin
        Reader: begin                              //读操作
            repeat

                wait(rmutex);                      //申请读 进入临界区 加锁               
                //判断有没有读进程(目前为0则没有读进程,可以需要把写进程信号量改为“在写”,并不是真的在写,而是由于有进程申请了读,为了互斥,则要禁止写,则把写的信号量变为“在写”,使其他写进程不能进入临界区(去写))
                if readcount=0 then wait(wmutex); 
                    Readcount:= Readcount + 1;     //读进程加1,表示多一个读进程在读
                signal(rmutex);                    //释放锁 其他进程可以读

                ...
                perform read operation;            //读操作(上面是申请开始读,下面是申请读完)
                ...

                wait(rmutex);                      //进入临界区 加锁 (读完了)
                //判断读进程数目(为0,则要把写信号量释放,表示其他写进程可以申请写了)
                if readcount=0 then signal(wmutex);             
                signal(rmutex);                    //释放锁 其他进程可以读

            until false;
            end
        Writer: begin                              //写操作
            repeat
                wait(wmutex);
                perform write operation
                signal(wmutex);
            until false;
            end
        parend
    end         

2.信号量集方法

信号量集机制特点

  • 一次可获得多个,释放多个资源
  • 资源数目低于设定值,就不可分配(下限值)
  • S表示信号量 d为需求量 t为下限量;Swait(S, d, t, … , Sn, dn, tn)———- (每次都要判断资源量S是否大于t)
  • 特殊情况一: Swait(S, d, d);此时信号量集中只有一个信号量S,允许每次申请d个资源,当资源少于d则不予分配
  • 特殊情况一: Swait(S, 1, 1);此时信号量集蜕变为一般记录型信号量(S>1)或者互斥信号量(S=1)
  • 特殊情况一: Swait(S, 1, 0);当S>=1时允许多个进程进入特定区,当S=0阻止任何进程进入(like开关)

so,该方法与记录型信号量相比,增加了一个限制:最多只允许RN个读者同时读

L :可申请使用的读信号量 初始化为RN
mx:写信号量
wait(L, 1, 1): 使L的值减1,当有RN个读者,则L减为0,再有wait操作就失败而阻塞

Var RN integer;
    L, mx: semaphore := RN, 1;
        begin
            parbegin
                Reader: begin
                        repeat
                            Swait(L, 1, 1);          //查看读进程的数目是否小于设定值(这里是1),如果不小于则可以通过
                            Swait(mx, 1, 0);         //申请当mx为1,则可以申请到(mx>0),则可以进行读操作,表示没有写操作在进行
                            ...
                            perform read poreation;
                            ...
                            Ssignal(L, 1);           //释放读临界区
                        until false;
                    end
                Writer: begin
                        repeat
                            Swait(mx, 1, 1; L, RN, 0);//既当无写进程(mx=1),又无读进程(L=RN)进行时,才可以写操作
                            perform write operation;
                            Ssignal(mx, 1);
                        unitl false;
                    end
            parend
        end

进程同步之理发师问题

@(操作系统)[进程同步]

description

假设有一个理发店只有一个理发师,一张理发时坐的椅子,若干张普通椅子顾客供等候时坐。没有顾客时,理发师就坐在理发的椅子上睡觉。顾客一到,他不是叫醒理发师,就是离开。如果理发师没有睡觉,而在为别人理发,他就会坐下来等候。如果所有的椅子都坐满了人,最后来的顾客就会离开。 
在出现竞争的情况下问题就来了,这和其它的排队问题是一样的。实际上,与哲学家就餐问题是一样的。如果没有适当的解决方案,就会导致进程之间的“饿肚子”和“死锁”。 
如理发师在等一位顾客,顾客在等理发师,进而造成死锁。另外,有的顾客可能也不愿按顺序等候,会让一些在等待的顾客永远都不能理发。

解决方案

最常见的解决方案就是使用三个信号量(Semaphore):一个给顾客信号量,一个理发师信号量(看他自己是不是闲着),第三个是互斥信号量(Mutual exclusion,缩写成mutex)。一位顾客来了,他想拿到互斥信号量,他就等着直到拿到为止。顾客拿到互斥信号量后,会去查看是否有空着的椅子(可能是等候的椅子,也可能是理发时坐的那张椅子)。 
如果没有一张是空着的,他就走了。如果他找到了一张椅子,就会让空椅子的数量减少一张,这位顾客接下来就使用自己的信号量叫醒理发师。这样,互斥信号标就释放出来供其他顾客或理发师使用。如果理发师在忙,这位顾客就会等。理发师就会进入了一个永久的等候循环,等着被在等候的顾客唤醒。一旦他醒过来,他会给所有在等候的顾客发信号,让他们依次理发。

PV操作

顾客信号量 = 0  
理发师信号量 = 0  
互斥信号量mutex = 1 // 椅子是理发师和顾客精进程都可以访问的临界区 
int 空椅子数量 = N     //所有的椅子数量  

理发师(线程/进程)  
While(true){        //持续不断地循环  
  P(顾客)          //试图为一位顾客服务,如果没有他就睡觉(进程阻塞)  
  P(互斥信号量)     //如果有顾客,这时他被叫醒(理发师进程被唤醒),要修改空椅子的数量  
    空椅子数量++     //一张椅子空了出来  
  V(理发师)        //现在有一个醒着的理发师,理发师准备理发,多个顾客可以竞争理发师互斥量,但是只有一个顾客进程可以被唤醒并得到服务  
  V(互斥信号量)    //释放椅子互斥量,使得进店的顾客可以访问椅子的数量以决定是否进店等待 
  /* 理发师在理发 */ 
}  


顾客(线程/进程)  
while(true)
{   //持续不断地循环  
    P(互斥信号量)     //想坐到一张椅子上  
    if (空椅子数量 > 0) 
    { //如果还有空着的椅子的话  
        空椅子数量--        //顾客坐到一张椅子上了  
        V(顾客)           //通知理发师,有一位顾客来了  
        V(互斥信号量)     //顾客已经坐在椅子上等待了,访问椅子结束,释放互斥量  
        P(理发师)         //该这位顾客理发了,如果理发师还在忙,那么他就等着(顾客进程阻塞)  
        /* 竞争到了理发师则该顾客开始理发 */
    }
    else//没有空着的椅子  
        V(互斥信号标)     //不要忘记释放被锁定的椅子  
        /* 顾客没有理发就走了 */ 
    }  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

c++代码

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int chairs = 10;
int empty_chairs = chairs; // 空椅子数
int customers = 0;
int barbers = 1;
condition_variable cv_barbers; // 当有顾客到时需通知理发师
mutex chairs_mtx, cus_mtx, bar_mtx;

/**
* 理发师进程,阻塞理发师的请况
* 1. 没有顾客,则睡觉(阻塞)
* 2. 访问临界区受阻,此时临界区正在被顾客访问
* @param i [description]
*/
void barber(int i)
{
    while (true)
    {
        unique_lock lck(bar_mtx);
        cv_barbers.wait(lck, [] 
        {
            if (customers > 0)
            {
                cout << "有顾客,理发师被唤醒" << endl;
                return true;
            }
            else
            {
                cout << "没有顾客,理发师睡觉" << endl;
                return false; 
            }
        });
        unique_lock lck2(chairs_mtx);
        customers--;
        empty_chairs++;
        /* cut hair*/
        cout << "理发师给顾客理发" << endl;
        lck2.unlock();
        // 理发时不断有顾客进来
        this_thread::sleep_for(std::chrono::microseconds(10));
    }
}
/**
* 顾客进程,阻塞顾客进程的情况有两种
* 1. 访问临界区(检查是否有空闲的椅子)时发现理发师进程也在访问临界区,P(chairs_mtx)
* 没有多余的椅子并不是阻塞顾客进程,直至有空闲椅子,而是直接离开,即该顾客不排队理发
* @param i [description]
*/
void customer(int i)
{
    unique_lock lck(chairs_mtx);
    if (empty_chairs > 0)
    {
        empty_chairs--;
        customers++;
        cv_barbers.notify_one();
        cout << "顾客 " << i << " 等待理发" << endl;
        //this_thread::sleep_for(std::chrono::milliseconds(100));
        lck.unlock();
    }
    else
    {
        /* 进程退出,不再理发了 */
        cout << "顾客 " << i << " 没有位置,离开" << endl;
        lck.unlock();
        /* leave */
    }
}

int main()
{
    thread t1 = thread(barber, 1);
    vector v;
    for (size_t i = 0; i < 20; i++)
    {
        v.push_back(thread(customer, i + 1));
    }
    t1.join();
    for (size_t i = 0; i < v.size(); i++)
    {
        v[i].join();
    }
    system("pause");
    return 0;
}

转载于:https://blog.csdn.net/anyao112233/article/details/54025070




你可能感兴趣的:(OperatingSystem)