进程同步与通信

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Mr_zhuo_/article/details/70257162
学完了进程同步与通信,总结一下。

对于线程的执行,我们既需要互斥,又需要并行。
互斥可以使得线程的执行具有可再现性,并有一个确定的执行结果。
而并行可以让我们多线程存在时执行效率提高,并且共享了资源。


那么要使得互斥和并行同时存在,就引入了线程间的同步。
同步使得协调多线程对共享数据的互斥访问。

(p.s.同步也是互斥的别名。那为什么叫同步呢?因为同步使得多线程协调访问共享资源。打个比方,线程1和2同时来了,都需要资源A和B。假如线程1必须在2获取资源B之后才能去获取资源A,B,那自然操作系统就让线程2先按2的需要去执行。然后线程2获取B后执行了一个结果,使得线程1可以获取资源A,那这时1获取A,2等待A,然后2获取A,1获取B,这样子线程1,2既互斥又同步。
互斥是从微观上说的,如果不互斥的话,共享资源在多个线程同时享用的时候是有很多不确定性的 ; 而同步是从宏观上讲的,总的来看,经过协调,多线程是近似同步完成的。

那实现同步就需要实现两个事情:1.共享资源时的互斥 2.共享资源时的通信交流


这里引入几个概念:
临界资源:一次只允许一个线程(进程)访问的资源。
临界区:正在访问临界资源的一段代码。一个线程在临界区执行时其他线程不能进入临界区。
进入区:进入临界区前,检查临界资源闲忙的一段代码。
退出区:退出临界区前,释放临界资源的一段代码。
(这三个区都是代码段的一段代码)

另外,线程同步有四个原则:
空闲让进,忙则等待,有限等待,让权等待

实现:
1.共享资源时的互斥
方法有三个:
(1)禁用中断(实现临界区管理的 硬件设施
(2)原子操作指令锁(实现临界区管理的 硬件设施

(3)软件方法

分析比较三者的实现原理和优劣之处。
(1)禁用中断
:在访问临界资源时关中断,访问完了再开中断,这样访问的这个过程就不会被其他线程打断,也就实现了互斥访问。
那关中断期间来的所有中断请求都会等到开中断以后再执行。
优点:简单易行,执行过程中没有中断,没有上下文切换,当前进程独占
缺点:1.只适用于单处理机。如果是多处理机?
            2.线程无法停止:如果系统执行过程中出现问题了,没有中断,它就一错到底了,系统会崩溃的 ;如果执行中没有问题,但是只要不开中断其他线程可能会饥饿。
            3.临界区可能很长,这样关中断时间会很久。


(2)原子操作指令锁主要就是TS指令和Swap指令,自旋锁和互斥锁

TS指令:设置s【表示临界区是否空闲】为全局bool变量,利用Test&Set指令实现对临界区的加锁和解锁。

s=true :没有进程在临界区。资源可用

s=false :有进程占用临界区,资源不可用

bool TS(bool &x)
{
    if(x)
    {
        x=false;
        return ture;
    }else{
        return false;
    }
}
while(true)  
{  
    while(!TS(s)); //s =false时,有其他进程在占用临界区,当前进程等待执行临界区.直到s=true时,退出循环。  
    /*临界区*/
    lock=false; //释放临界区  
    ...
}  

Swap指令:设lock【表示是否上锁】为全局布尔变量(初值为假),每个进程设一个局部布尔变量key。利用Swap指令,可实现对临界区的加锁与解锁。

void Pi( )
{
    ...
    boolean key = true;
    do{
        Swap (key, lock);
    }while(key) //循环等待lock值为false,这时交换了lock和key,循环结束且lock=ture ,上锁   
    critical section //临界区
    Swap(key,lock);  //开锁
    ...
}

这时不同的等待方式可以实现两种锁:
自旋锁:忙等待。
互斥锁:无忙等待。设置了一个阻塞队列。


优点:1.简单易行
2.适用于多临界区(每个临界区设置一个锁)
3.适用于 单处理机共享主存的多处理机中任意多个线程(进程)的访问。

缺点:可能会死锁饥饿忙等也消耗CPU时间。


(3)软件方法:主要就是Dekker算法和Peterson算法来实现。

Dekker算法:(Dekker算法的详解在下一篇文章)

问题可以得到解决的原因是:turn的唯一性。和谦让又不过分谦让准则。如果你说你想上厕所并且排队轮到你了,那你去,我就不再说我想上厕所了,省得你想连着上厕所或者啥的时候得一直问你想不想上厕所,别有压力呀哥们。你完事了并且轮到我了我再说我想上厕所。我完事后,下次又轮到你了哥们。

flag[i] = true;//派大星!我准备好了!
while (flag[j]) //你也准备好了?
{
    if (turn != i) //还没到我哦...
    {
       flag[i] = false;//那我睡会儿
       while (turn != i);//到我啦!
       flag[i] = true;//派大星!我又准备好啦!
     }
}
/* critical section code */
... 
turn = j;//派大星,你来!
flag[i] = false;//我睡会儿
/* remainder section */

这样出现一个问题:会不会我们俩必须得严格按照你-我-你-我-你-我这样的顺序呢?

不用。我们俩上厕所得有两个条件:(1)我说我想上厕所 (2)该我上厕所了,其中一个不满足我就没法上厕所。比方说,我上完厕所了(P0执行完第一次),这时turn=j,可是在进入P1之前,P0又一次抢先,这个时候,若恰好flag[1]=0,他没来得及说他想上厕所,那我就当仁不让喽。这样我不是连着上了两次厕所吗?

Peterson算法:

flag用于标记我想不想进(或者我现在有没有准备好进)。

turn用于标记该谁进了。

问题可以得到解决的原因是:在某个时刻turn只能区一个值,要么1,要么0,(要么轮到你,要么轮到我),则while中的&&保证了双方“while是否退出循环”这件事情结果的唯一性。也就保证了每次只有一个进程的while退出循环,进入临界区。

do {
    flag[i] = true; //派大星!我准备好了!
    turn = j;       //你先来?
    while ( flag[j] && turn == j); //你准备好了吗?准备好的就快去,我等你。啥?你说你完事儿了?
    CRITICAL SECTION//那我进去喽
    flag[i] = false;//我完成了!我还没准备下一次呢,我要休息一会儿
    REMAINDER SECTION
} while (true);
Peterson算法就是每次在我想进的时候(flag[i]=true)都谦让一下别人(turn=j),如果你也想进那你就先进吧,我等等你。如果你不想进入那我就进入了。
这样就不会出现我们俩同时进去的情况了。
缺点:很复杂,并且只能实现互斥,不能实现同步。且需要忙等待,占用CPU(一致判断临界资源的使用情况)。



2.共享资源时的通信交流
两种办法:借助信号量或管程。

信号量
分两种:
1. 互斥信号量 mutex,初值设为1
2. 资源信号量 可以有多个,根据资源的数量。用于 协作
一般来说先判断资源信号量再判断互斥信号量。
注意,信号量归操作系统管理,设置的不要太多,尽可能少。

总结一点:
1.由P(),V()操作,判断是否可以进入临界区。能进就进,不能进就等。 可以忙等也可以阻塞
2.信号量和锁是一样的,都是控制线程是否进入临界区,实现方法都是三种:禁用中断,软件方法和原子操作指令。不同的地方就是,如果只有一个互斥信号量,也就是mutex时,就相当于锁了。
所以和锁相比,信号量一个特点就是可以用于多线程间协作(利用资源信号量)

值得注意的是:信号量同步应用问题分析步骤
*根据题目描述 抽象出问题涉及的几类进程有几种活动对象就需要几类进程),给出进程执行的伪码;
*分析并 标注每个进程与其他进程之间的制约关系
*定义 资源信号量:资源信号量的不同状态数一般就是需要的信号量数;
*定义 互斥信号量:有共享的临界资源存在时需要该信号量;
*删去多余的信号量( 保证并发进程执行结果可再现性的前提下,信号量越少越好);
* 对信号量值进行初始化
*将信号量的P、V操作添加到进程的伪代码中。

信号量集:将线程在运行中所需要的临界资源 一次性全部分配给该线程,如果有一个资源不能到位,所有其它资源也都将不再分配给该线程。 用完后所有的临界资源一次性释放
酱紫就避免了死锁,但是资源利用率降低。

管程
 管程作为一个模块,它的类型定义如下:
    monitor_name = MoNITOR; 
       共享变量说明; 
       define 本管程内部定义、外部可调用的函数名表; 
       use 本管程外部定义、内部可调用的函数名表; 
       内部定义的函数说明和函数体 
       { 
         共享变量初始化语句; 
       }
 
从语言的角度看, 管程主要有以下特性
  (1) 模块化。管程是一个基本程序单位,可以单独编译; 
  (2) 抽象数据类型。管程是中 不仅有数据而且有对数据的操作
  (3) 信息掩蔽。管程外 可以调用管程内部定义的一些函数,但函数的具体实现外部不可见; 
对于管程中定义的共享变量的所有操作都局限在管程中,外部只能通过调用管程的某些函数来间接访问这些变量。因此管程有很好的封装性。(类)
为了保证共享变量的数据一致性,管程应互斥使用
。 管程通常是用于管理资源的,因此管程中有进程等待队列和相应的等待和唤醒操作。在 管程入口有一个等待队列,称为入口等待队列。当一个已进入管程的进程等待时,就释放管程的互斥使用权;当已进入管程的一个进程唤醒另一个进程时,两者必须有一个退出或停止使用管程。在管程内部,由于执行唤醒操作,可能存在多个等待进程(等待使用管程),称为紧急等待队列,它的优先级高于入口等待队列。 
因此,一个进程进入管程之前要先申请,一般由管程提供一个enter过程;离开时释放使用权,如果紧急等待队列不空,则唤醒第一个等待者,一般也由管程提供外部过程leave。
管程内部有自己的等待机制。管程可以说明一种特殊的条件型变量:var c:condition;实际上是一个指针,指向一个等待该条件的PCB队列。对条件型变量可执行wait和signal操作:
    wait(c):若紧急等待队列不空,唤醒第一个等待者,否则释放管程使用权。执行本操作的进程进入C队列尾部;
    signal(c):若C队列为空,继续原进程,否则唤醒队列第一个等待者,自己进入紧急等待队列尾部。

总结一点:
1. 管程一个特点就是在管程内部执行的线程可以临时放弃互斥访问,让给其他线程
2.管程和信号量相比: 信号量机制功能强大,但 使用时对信号量的操作分散,而且难以控制,读写和维护都很困难。因此后来又提出了一种 集中式同步进程——管程。其基本思想是将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成。这样模块之间联系清晰,便于维护和修改,易于保证正确性. 


猜你喜欢

转载自blog.csdn.net/Mr_zhuo_/article/details/70257162