• 进程同步、互斥机制


    一、进程的并发执行

    1. 并发是所有问题产生的基础。

    2. 进程的特征:

    并发:进程执行时间断性的,执行速度是不可预测的;

    共享:进程/线程之间的制约性;

    不确定性:进程执行的结果和执行的相对速度有关,所以是不确定的;

    3. 举例:

    1) 银行业务系统:进程的关键活动出现交叉;

    2) get-->copy-->put;

    并发环境下执行,时间上,速度上都不确定,执行次序的不同会导致不同的结果。

    4. 并发环境的制约关系:进程前趋图

    二、进程互斥(MUTUAL EXCLUSIVE)

    1. 竞争条件:两个或多个进程在读写某些共享数据时,而最后的结果取决于进程运行的精确的时序;

    2. 进程互斥:由于各个进程要求使用共享资源,而这些资源需要排他性的使用,各个进程之间竞争使用这些资源,这一关系称为进程互斥;

    3. 临界资源:系统中某些资源一次只希望允许一个进程使用,称这样的资源为临界资源或者互斥资源或共享变量;

    4. 临界区(互斥区):多个进程中对某个临界资源实施操作的程序片段;

    5. 临界区的使用原则:

    1)如果没有进程在临界区,想进入临界区的进程就可以进入;

    2)不允许两个进程同时处于临界区内;

    3)临界区外运行的进程不得阻塞其他进程进入临界区;

    4)不得使进程无限期等待进入临界区;

    6. 实现进程互斥的方法:软件方案、硬件方案;

     

    、进程互斥的软件解法

    软件方法:保护临界区

    正确算法:

    1. DEKKER算法

    2. PETERSON算法(更好)

    四、进程互斥的硬件解法

    用特殊指令来达到保护临界区的目的;

    1. 开关中断指令:

    1)简单、高效

    2)代价高,限制CPU的并发能力

    3)不适用于多处理器

    4)适用于操作系统本身,不适用于用户程序

    2. 测试并加锁指令:

    3. 交换指令: 

    4. 忙等待:进程在得到临界区访问权限之前,持续做测试而不做其他事情;(单CPU不提倡)

        自旋锁:(多处理器情况),忙等待就比较好,因为切换的开销是很大的;

    五. 进程同步

    1. 进程同步(synchronization):指系统中多个进程发生的时间存在某种时序关系,需要相互合作,共同完成某一项任务;

    进程之间的协作关系;

     

    2. 生产者消费者问题(又称为缓冲区问题):

    生产者进程-->缓冲区-->消费者进程

    只能由一个生产者或者消费者对缓冲区进行操作;

    避免忙等待:

    睡眠与唤醒操作(原语):sleep() 与 wakeup()  操作

    3. SPOOLing系统:生产者消费者问题

    六. 信号量及P、V操作(一种经典的进程同步机制):

    1. 1965荷兰学者Dijkstrat提出;P与V分别是荷兰语 test (proberen) 和 increment (verhogen);

     信号量:

     一个特殊变量,用于在进程间传递信息的一个整数值;

     定义如下:
    struct semaphore{
        int count;

        queueType  queue;

    }

    信号量说明:semaphore a;

    对信号量可以实施的操作:初始化、P操作和V操作;

    2. P(down,semWait)、V(up,semSignal)操作

    P操作相当于申请资源,而V操作相当于释放资源。所以要记住以下几个关键字:

    P操作----->申请资源

    V操作----->释放资源

    P操作:信号量值减1;

                然后判断信号量值是否小于0,如果小于0,则将该进程设置为阻塞状态;将该进程插入相应的等待队列s.queue末尾;

               否则实施P操作的进程就继续执行;

    V操作:信号量值加1;

                如果信号量值<=0,则说明原来信号量上有进程在等待,所以唤醒s.queue中的第一个等待进程;改变其为就绪态,并将其插入就绪队列;

               否则,实施v操作的进程就继续执行;

    3. 说明:

    • P操作和V操作是原语操作;
    • 信号量上定义了三个操作:初始化(非负数)、P操作和V操作;
    • 最初提出的是二元信号量(解决互斥),最后推广到一半信号量(多值)或计数信号量解决同步;

    4. 在理解了PV操作的的含义后,就必须讲解利用PV操作可以实现进程的两种情况:互斥和同步。 

    1)一个生产者,一个消费者,公用一个缓冲区。

    可以作以下比喻:将一个生产者比喻为一个生产厂家,如伊利牛奶厂家,而一个消费者,比喻是学生小明,而一个缓冲区则比喻成一间好又多。

    第一种情况,可以理解成伊利牛奶生产厂家生产一盒牛奶,把它放在好又多一分店进行销售,而小明则可以从那里买到这盒牛奶。只有当厂家把牛奶放在商店里面后,小明才可以从商店里买到牛奶。所以很明显这是最简单的同步问题。

    解题如下:

    定义两个同步信号量:

    empty——表示缓冲区是否为空,初值为1。

    full——表示缓冲区中是否为满,初值为0。

    生产者进程

    while(TRUE){

        生产一个产品;

        P(empty);

         产品送往Buffer;

         V(full);

    }

    消费者进程

    while(TRUE){

       P(full);

       从Buffer取出一个产品;

       V(empty);

       消费该产品;

    }

    2)一个生产者,一个消费者,公用n个环形缓冲区。

     第二种情况可以理解为伊利牛奶生产厂家可以生产好多牛奶,并将它们放在多个好又多分店进行销售,而小明可以从任一间好又多分店中购买到牛奶。同样,只有当厂家把牛奶放在某一分店里,小明才可以从这间分店中买到牛奶。
    不同于第一种情况的是,第二种情况有N个分店(即N个缓冲区形成一个环形缓冲区),所以要利用指针,要求厂家必须按一定的顺序将商品依次放到每一个分店中。缓冲区的指向则通过模运算得到。

    解题如下:

      定义两个同步信号量:

      empty——表示缓冲区是否为空,初值为n。

      full——表示缓冲区中是否为满,初值为0。

      设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指针,指向下一个可用的缓冲区。

    生产者进程

    while(TRUE){

         生产一个产品;

         P(empty);

         产品送往buffer(in);

         in=(in+1)mod n;

         V(full);

    }

    消费者进程

    while(TRUE){

       P(full);

       从buffer(out)中取出产品;

       out=(out+1)mod n;

       V(empty);

       消费该产品;

    }

    3)一组生产者,一组消费者,公用n个环形缓冲区

    第三种情况,可以理解成有多间牛奶生产厂家,如蒙牛,达能,光明等,消费者也不只小明一人,有许许多多消费者。不同的牛奶生产厂家生产的商品可以放在不同的好又多分店中销售,而不同的消费者可以去不同的分店中购买。当某一分店已放满某个厂家的商品时,下一个厂家只能把商品放在下一间分店。所以在这种情况中,生产者与消费者存在同步关系,而且各个生产者之间、各个消费者之间存在互斥关系,他们必须互斥地访问缓冲区。

    解题如下:

    定义四个信号量:

    empty——表示缓冲区是否为空,初值为n。

    full——表示缓冲区中是否为满,初值为0。

    mutex1——生产者之间的互斥信号量,初值为1。

    mutex2——消费者之间的互斥信号量,初值为1。

    设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指针,指向下一个可用的缓冲区。

    生产者进程

    while(TRUE){

         生产一个产品;

         P(empty);

         P(mutex1);

         产品送往buffer(in);

         in=(in+1)mod n;

         V(mutex1);

         V(full);

    }

    消费者进程

    while(TRUE){

       P(full);

       P(mutex2);

       从buffer(out)中取出产品;

       out=(out+1)mod n;

       V(mutex2);

       V(empty);

    }

    5. 用P、V操作解决进程间互斥问题:

    • 分析并发进程的关键活动,划定临界区;
    • 设置信号量mutex,初值为1;
    • 在临界区之前实施P(mutex);
    • 在临界区之后实施V(mutex);

    七. 生产者消费者问题

    生产者、缓冲区、消费者

    生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区。

    其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。

    问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。

    同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。

    听起来好像蛮对的,无懈可击似的,但其实在实现时会有一个竞争条件存在的。为了跟踪缓冲区中的消息数目,需要一个变量 count。

    如果缓冲区最多存放 N 个消息,则生产者的代码会首先检查 count 是否达到 N,如果是,则生产者休眠;否则,生产者向缓冲区中放入一个消息,并增加 count 的值。

    消费者的代码也与此类似,首先检测 count 是否为 0,如果是,则休眠;否则,从缓冲区中取出消息并递减 count 的值。同时,每个进程也需要检查是否需要唤醒另一个进程。

    代码可能如下:

    // 缓冲区大小
    #define N 100               
    int count = 0;                // 跟踪缓冲区的记录数
    
    /* 生产者进程 */
    void procedure(void)
    {
            int item;                // 缓冲区中的数据项
            
            while(true)                // 无限循环
            {               
                    item = produce_item();                // 产生下一个数据项
                    if (count == N)                                // 如果缓冲区满了,进行休眠
                    {
                            sleep();
                    }
                    insert_item(item);                        // 将新数据项放入缓冲区
                    count = count + 1;                        // 计数器加 1
                    if (count == 1)                         // 表明插入之前为空,
                    {                                                        // 消费者等待
                            wakeup(consumer);                // 唤醒消费者
                    }
            }
    }
    
    /* 消费者进程 */
    void consumer(void)
    {
            int item;                // 缓冲区中的数据项
            
            while(true)                // 无限循环
            {
                    if (count == 0)                                // 如果缓冲区为空,进入休眠
                    {
                            sleep();
                    }
                    item = remove_item();                // 从缓冲区中取出一个数据项
                    count = count - 1;                        // 计数器减 1
                    if (count == N -1)                        // 缓冲区有空槽
                    {                                                        // 唤醒生产者
                            wakeup(producer);
                    }
                    consume_item(item);                        // 打印出数据项
            }
    }
    

     看上去很美,哪里出了问题,这里对 count 的访问是有可能出现竞争条件的:缓冲区为空,消费者刚刚读取 count 的值为 0,而此时调度程序决定暂停消费者并启动执行生产者。生产者向缓冲区中加入一个数据项,count 加 1。

    现在 count 的值变成了 1,它推断刚才 count 为 0,所以此时消费者一定在休眠,于是生产者开始调用 wakeup(consumer) 来唤醒消费者。但是,此时消费者在逻辑上并没有休眠,所以 wakeup 信号就丢失了。

    当消费者下次运行时,它将测试先前读到的 count 值,发现为 0(注意,其实这个时刻 count 已经为 1 了),于是开始休眠(逻辑上)。而生产者下次运行的时候,count 会继续递增,并且不会唤醒 consumer 了,所以迟早会填满缓冲区的,

    然后生产者也休眠,这样两个进程就都永远的休眠下去了。

    使用信号量解决生产者-消费者问题

    首先了解一下信号量吧,信号量是 E.W.Dijkstra 在 1965 年提出的一种方法,它是使用一个整型变量来累计唤醒的次数,供以后使用。在他的建议中,引入了一个新的变量类型,称为信号量(semaphore).

    一个信号量的取值可以为 0(表示没有保存下来的唤醒操作)或者为正值(表示有一个或多个唤醒操作)。

    并且设立了两种操作:down 和 up(分别为一般化后的 sleep 和 wakeup,其实也是一般教科书上说的 P/V 向量)。对一个信号量执行 down 操作,表示检查其值是否大于 0,如果该值大于 0,则将其值减 1(即用掉一个保存的唤醒信号)并继续;

    如果为 0,则进程休眠,而且此时 down 操作并未结束。另外,就是检查数值,修改变量值以及可能发生的休眠操作都作为单一的,不可分割的 原子操作 来完成。

    下面开始考虑用信号量来解决生产者-消费者问题了,不过在此之前,再次分析一下这个问题的本质会更清晰点:问题的实质在于发给一个(尚)未休眠进程(如上的消费者进程在只判断了 count == 0 后即被调度出来,还未休眠)的 wakeup 信号丢失

    (如上的生产者进程在判断了 count == 1 后以为消费者进程休眠,而唤醒它)了。如果它没有丢失,则一切都会很好。

    #define N 100                                // 缓冲区中的槽数目
    typedef int semaphore;                // 信号量一般被定义为特殊的整型数据
    semaphore mutex = 1;                // 控制对临界区的访问
    semaphore empty = N;                // 计数缓冲区中的空槽数目
    semaphore full         = 0;                // 计数缓冲区中的满槽数目
    
    /* 生产者进程 */
    void proceducer(void)
    {
            int item;
            
            while(1)
            {
                    item = procedure_item();        // 生成数据
                    down(&empty);                                // 将空槽数目减 1
                    down(&mutex);                                // 进入临界区
                    insert_item(item);                        // 将新数据放入缓冲区
                    up(&mutex);                                        // 离开临界区
                    up(&full);                                        // 将满槽的数目加 1
            }
    }
    
    /* 消费者进程 */
    void consumer(voi)
    {
            int item;
            
            while(1)
            {
                    down(&full);                                // 将满槽数目减 1
                    down(&mutex);                                // 进入临界区
                    item = remove_item();                // 从缓冲区中取出数据项
                    up(&mutex);                                        // 离开临界区
                    up(&empty);                                        // 将空槽数目加 1
                    consumer_item(item);                // 处理数据项
            }
    }
    

    该解决方案使用了三个信号量:一个为 full,用来记录充满的缓冲槽的数目,一个为 empty,记录空的缓冲槽总数,一个为 mutex,用来确保生产者和消费者不会同时访问缓冲区。

    mutex 的初始值为 1,供两个或者多个进程使用的信号量,保证同一个时刻只有一个进程可以进入临界区,称为二元信号量(binary semaphore)。

    如果每一个进程在进入临界区前都执行一个 down(...),在刚刚退出临界区时执行一个 up(...),就能够实现互斥。

    另外,通常是将 down 和 up 操作作为系统调用来实现,而且 OS 只需要在执行以下操作时暂时禁止全部中断:测试信号量,更新信号量以及在需要时使某个进程休眠。

    这里使用了三个信号量,但是它们的目的却不相同,其中 full 和 empty 用来同步(synchronization),而 mutex 用来实现互斥。

    七. 用信号量解决读者/写者问题:

  • 相关阅读:
    node错误: primordials is not defined
    单片机TTL转RS232模块DB9数据线接口排针接口多接口方便连接
    单片机串口自适应电平5V TTL电平兼容转换3.3V电平串口转换NMOS管
    USB串口转RS485转换器工业级usb串口转RS485模块转换器串口通讯
    Kafka丢数据、重复消费、顺序消费的问题
    纪念第一次做的拉花拿铁
    《奢侈的理由》总结
    【算法框架套路】回溯算法(暴力穷举的艺术)
    svg中矩形旋转问题
    性能测试工具集锦
  • 原文地址:https://www.cnblogs.com/Allen-rg/p/7171524.html
Copyright © 2020-2023  润新知