全文脉络思维导图如下:
1.什么是进程同步
在多道批处理系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到底的,而是走走停停,以不可预知的速度向前推进,这就是进程的「异步性」。
那么,「进程的异步性会带来什么问题呢」?举个例子,如果有A、B两个进程分别负责读和写数据的操作,这两个线程是相互合作、相互依赖的。那么写数据应该发生在读数据之前。而实际上,由于异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区还没有被写入数据,读进程A没有数据可读,因此读进程A被阻塞。
进程同步(synchronization)就是用来解决这个问题的。从上面的例子我们能看出,一个进程的执行可能影响到另一个进程的执行,「所谓进程同步就是指协调这些完成某个共同任务的并发线程,在某些位置上指定线程的先后执行次序、传递信号或消息」。
再举个生活中的进程同步的例子,你想要喝热水,于是你打了一壶水开始烧,在这壶水烧开之前,你只能一直等着,水烧开之后水壶自然会发生响声提醒你来喝水,于是你就可以喝水了。就是说「水烧开这个事情必须发生在你喝水之前」。
注意不要把进程同步和进程调度搞混了:
进程调度是为了最大程度的利用CPU资源,选用合适的算法调度就绪队列中的进程。进程同步是为了协调一些进程以完成某个任务,比如读和写,你肯定先写后读,不能先读后写吧,这就是进程同步做的事情了,指定这些进程的先后执行次序使得某个任务能够顺利完成。2.什么是进程互斥
同样的,也是因为进程的并发性,并发执行的线程不可避免地需要共享一些系统资源,比如内存、打印机、摄像头等。举个例子:我们去学校打印店打印论文,你按下了WPS的“打印”选项,于是打印机开始工作。你的论文打印到一半时,另一位同学按下了Word的“打印”按钮,开始打印他自己的论文。想象一下如果两个进程可以随意的、并发的共享打印机资源,会发生什么情况?
显然,两个进程并发运行,导致打印机设备交替的收到WPS和Word两个进程发来的打印请求,结果两篇论文的内容混杂在一起了。
进程互斥(mutualexclusion)就是用来解决这个问题的。当某个进程A在访问打印机时,如果另一个进程B也想要访问打印机,它就必须等待,直到A进程访问结束并释放打印机资源后,B进程才能去访问。
实际上,像上述的打印机这种「在一个时间段内只允许一个进程使用的资源」(这也就是互斥的意思),我们将其称为「临界资源」,对临界资源进行访问的那段代码称为「临界区」。
通俗的对比一下进程互斥和进程同步:
进程同步:进程A应在进程B之前执行进程互斥:进程A和进程B不能在同一时刻执行从上不难看出,「进程互斥是一种特殊的进程同步」,即逐次使用临界资源,也是对进程使用资源的先后执行次序的一种协调。
3.常见的进程同步与互斥机制
常见的进程同步与互斥机制有两种:
信号量与PV操作管程①信号量与PV操作
包交包会!看完下面这段解释你绝对能够明白PV操作是啥。
年,荷兰学者Dijkstra提出了一种卓有成效的实现进程同步和互斥的方法—信号量机制(Semaphore)。信号量其实就是一个变量,我们可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现进程互斥或同步。这一对原语就是PV操作:
1)「P操作」:将信号量值减1,表示「申请占用一个资源」。如果结果小于0,表示已经没有可用资源,则执行P操作的进程被阻塞。如果结果大于等于0,表示现有的资源足够你使用,则执行P操作的进程继续执行。
可以这么理解,当信号量的值为2的时候,表示有2个资源可以使用,当信号量的值为-2的时候,表示有两个进程正在等待使用这个资源。不看这句话真的无法理解V操作,看完顿时如梦初醒。
2)「V操作」:将信号量值加1,表示「释放一个资源」,即使用完资源后归还资源。若加完后信号量的值小于等于0,表示有某些进程正在等待该资源,由于我们已经释放出一个资源了,因此需要唤醒一个等待使用该资源(就绪态)的进程,使之运行下去。
我觉得已经讲的足够通俗了,不过对于V操作大家可能仍然有困惑,下面再来看两个关于V操作的问答:
问:「信号量的值大于0表示有临界资源可供使用,这个时候为什么不需要唤醒进程」?
答:所谓唤醒进程是从就绪队列(阻塞队列)中唤醒进程,而信号量的值大于0表示有临界资源可供使用,也就是说这个时候没有进程被阻塞在这个资源上,所以不需要唤醒,正常运行即可。
问:「信号量的值等于0的时候表示没有临界资源可供使用,为什么还要唤醒进程」?
答:V操作是先执行信号量值加1的,也就是说,把信号量的值加1后才变成了0,在此之前,信号量的值是-1,即有一个进程正在等待这个临界资源,我们需要唤醒它。
信号量和PV操作具体的定义如下:
实现进程互斥
两步走即可实现进程的互斥:
定义一个互斥信号量,并初始化为1把对于临界资源的访问置于P操作和V操作之间
「P操作和V操作必须成对出现」。缺少P操作就不能保证对临界资源的互斥访问,缺少V操作就会导致临界资源永远得不到释放、处于等待态的进程永远得不到唤醒。
实现进程同步
回顾一下进程同步,就是要各并发进程按要求有序地运行。
举个例子,以下两个进程P1、P2并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。假设P2的“代码4”要基于P1的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。
如果P2的“代码4”要基于P1的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。
使用信号量和PV操作实现进程的同步也非常方便,三步走:
定义一个同步信号量,并初始化为当前可用资源的数量在优先级较「高」的操作的「后」面执行V操作,释放资源在优先级较「低」的操作的「前」面执行P操作,申请占用资源
配合下面这张图直观理解下:
生产者和消费者问题
下面我们利用信号量和PV操作来解决经典的进程同步和互斥问题:生产者和消费者问题。
:系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。任何时刻,只能有一个生产者或消费者可以访问缓冲区。
由题可知,生产者、消费者共享一个初始为空、大小为n的缓冲区,我们从题目中提炼出同步与互斥关系:
同步关系1:只有缓冲区没满时(优先级高),生产者才能把产品放入缓冲区(优先级低),否则必须等待同步关系2:只有缓冲区不空时(优先级高),消费者才能从中取出产品(优先级低),否则必须等待互斥关系:缓冲区是临界资源,各进程必须互斥地访问。既然这个题目有两个同步关系和一个互斥关系,那么我们就需要两个同步信号量和一个互斥信号量:
empty:同步信号量(对应同步关系1),表示生产者还能生产多少,即还能放入缓冲区多少产品,该数量小于等于0,则生产者不能进行生产。初始化为n。full:同步信号量(对应同步关系2),表示消费者还能从缓冲区取出多少,即当前缓冲区已有产品的数量,该数量小于等于0,则消费者不能进行读取。初始化为0。mutex:互斥信号量,实现对缓冲区的互斥访问。初始化为1。代码如下,注意各个PV操作的配对:
②管程
管程有一个重要特性:「在一个时刻只能有一个进程使用管程」。进程在无法继续执行的时候不能一直占用管程,否则其它进程将永远不能使用管程。也就是说「管程天生支持进程互斥」。
其实使用管程是能够实现信号量的,并且也能用信号量实现管程。但是管程封装的比较好,相比起信号量来需要我们编写的代码更少,更加易用,这也就是「Java采用管程机制」的原因,synchronized关键字及wait()、notify()、notifyAll()这三个方法都是管程的组成部分。把管程翻译为Java领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。再详细的部分就不再深究了,溜了溜了。