看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

在多道批处理系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。那么,进程的异步性会带来什么问题呢?

???? 尽人事,听天命。博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步

???? 本文已收录于 CS-Wiki(Gitee 官方推荐项目,现已累计 1.4k+ star),致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习

 

全文脉络思维导图如下:

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

1. 什么是进程同步

在多道批处理系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进,这就是进程的异步性

那么,进程的异步性会带来什么问题呢?举个例子,如果有 A、B 两个进程分别负责读和写数据的操作,这两个线程是相互合作、相互依赖的。那么写数据应该发生在读数据之前。而实际上,由于异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区还没有被写入数据,读进程 A 没有数据可读,因此读进程 A 被阻塞。

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

进程同步(synchronization)就是用来解决这个问题的。从上面的例子我们能看出,一个进程的执行可能影响到另一个进程的执行,所谓进程同步就是指协调这些完成某个共同任务的并发线程,在某些位置上指定线程的先后执行次序、传递信号或消息

再举个生活中的进程同步的例子,你想要喝热水,于是你打了一壶水开始烧,在这壶水烧开之前,你只能一直等着,水烧开之后水壶自然会发生响声提醒你来喝水,于是你就可以喝水了。就是说水烧开这个事情必须发生在你喝水之前

注意不要把进程同步和进程调度搞混了:

  • 进程调度是为了最大程度的利用 CPU 资源,选用合适的算法调度就绪队列中的进程。

  • 进程同步是为了协调一些进程以完成某个任务,比如读和写,你肯定先写后读,不能先读后写吧,这就是进程同步做的事情了,指定这些进程的先后执行次序使得某个任务能够顺利完成。

2. 什么是进程互斥

同样的,也是因为进程的并发性,并发执行的线程不可避免地需要共享一些系统资源,比如内存、打印机、摄像头等。举个例子:我们去学校打印店打印论文,你按下了 WPS 的 “打印” 选项,于是打印机开始工作。 你的论文打印到一半时,另一位同学按下了 Word 的 “打印” 按钮,开始打印他自己的论文。想象一下如果两个进程可以随意的、并发的共享打印机资源,会发生什么情况?

显然,两个进程并发运行,导致打印机设备交替的收到 WPS 和 Word 两个进程发来的打印请求,结果两篇论文的内容混杂在一起了。

进程互斥(mutual exclusion)就是用来解决这个问题的。当某个进程 A 在访问打印机时,如果另一个进程 B 也想要访问打印机,它就必须等待,直到 A 进程访问结束并释放打印机资源后,B 进程才能去访问。

实际上,像上述的打印机这种在一个时间段内只允许一个进程使用的资源(这也就是互斥的意思),我们将其称为临界资源,对临界资源进行访问的那段代码称为临界区

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

通俗的对比一下进程互斥和进程同步:

  • 进程同步:进程 A 应在进程 B 之前执行

  • 进程互斥:进程 A 和进程 B 不能在同一时刻执行

从上不难看出,进程互斥是一种特殊的进程同步,即逐次使用临界资源,也是对进程使用资源的先后执行次序的一种协调。

3. 常见的进程同步与互斥机制

常见的进程同步与互斥机制有两种:

  • 信号量与 PV 操作

  • 管程

① 信号量与 PV 操作

包交包会!看完下面这段解释你绝对能够明白 PV 操作是啥。

1965年,荷兰学者 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 操作具体的定义如下:

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

实现进程互斥

两步走即可实现进程的互斥:

  • 定义一个互斥信号量,并初始化为 1

  • 把对于临界资源的访问置于 P 操作和 V 操作之间

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

P 操作和 V 操作必须成对出现。缺少 P 操作就不能保证对临界资源的互斥访问,缺少 V 操作就会导致临界资源永远得不到释放、处于等待态的进程永远得不到唤醒。

实现进程同步

回顾一下进程同步,就是要各并发进程按要求有序地运行。

举个例子,以下两个进程 P1、P2 并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。假设 P2 的 “代码4” 要基于 P1 的 “代码1” 和 “代码2” 的运行结果才能执行,那么我们就必须保证 “代码4” 一定是在 “代码2” 之后才会执行。

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

如果 P2 的 “代码4” 要基于 P1 的 “代码1” 和 “代码2” 的运行结果才能执行,那么我们就必须保证 “代码4” 一定是在 “代码2” 之后才会执行。

使用信号量和 PV 操作实现进程的同步也非常方便,三步走:

  • 定义一个同步信号量,并初始化为当前可用资源的数量

  • 在优先级较的操作的面执行 V 操作,释放资源

  • 在优先级较的操作的面执行 P 操作,申请占用资源

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

配合下面这张图直观理解下:

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

生产者和消费者问题

下面我们利用信号量和 PV 操作来解决经典的进程同步和互斥问题:生产者和消费者问题。

【问题描述】:系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。任何时刻,只能有一个生产者或消费者可以访问缓冲区。

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

由题可知,生产者、消费者共享一个初始为空、大小为 n 的缓冲区,我们从题目中提炼出同步与互斥关系:

  • 同步关系 1:只有缓冲区没满时(优先级高),生产者才能把产品放入缓冲区(优先级低),否则必须等待

  • 同步关系 2:只有缓冲区不空时(优先级高),消费者才能从中取出产品(优先级低),否则必须等待

  • 互斥关系:缓冲区是临界资源,各进程必须互斥地访问。

既然这个题目有两个同步关系和一个互斥关系,那么我们就需要两个同步信号量和一个互斥信号量:

  • empty:同步信号量(对应同步关系 1),表示生产者还能生产多少,即还能放入缓冲区多少产品,该数量小于等于 0,则生产者不能进行生产。 初始化为 n。

  • full:同步信号量(对应同步关系 2),表示消费者还能从缓冲区取出多少,即当前缓冲区已有产品的数量,该数量小于等于 0,则消费者不能进行读取。初始化为 0。

  • mutex:互斥信号量,实现对缓冲区的互斥访问。初始化为 1。

代码如下,注意各个 PV 操作的配对:

看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

② 管程

管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程将永远不能使用管程。也就是说管程天生支持进程互斥

其实使用管程是能够实现信号量的,并且也能用信号量实现管程。但是管程封装的比较好,相比起信号量来需要我们编写的代码更少,更加易用,这也就是 Java 采用管程机制的原因,synchronized 关键字及 wait()notify()notifyAll() 这三个方法都是管程的组成部分。把管程翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。再详细的部分就不再深究了,溜了溜了。

???? 关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学硕士在读,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 基础和面试指南的相关原创技术好文。本公众号的目的就是让大家可以快速掌握重点知识,有的放矢。希望大家多多支持哦,和小牛肉一起成长 ????

  • 并推荐个人维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已累计 1.4k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~ ????

上一篇:16.2.1重学C++之【vertor基本概念、构造函数、赋值操作】


下一篇:《趣谈 Linux 操作系统》学习笔记(一):为什么要学 Linux 及学习路径