os之PV
本文将从定义,如何实现互斥,同步,pv实操三个部分讲解。
后补现代cpp的channal
信号量
要了解pv,首先要了解信号量。
人们为了实现进程同步互斥,为了机器知道运行状态(进程通常分为三个状态,就绪,运行,阻塞),定义了一个变量来表示系统种某种资源的数量。
- 如果信号量S>0,说明有资源可以使用
- 如果信号量S<0,说明有进程正在等待这个资源
- 如果信号量S=0,说明资源刚好用完,没有进程在等待
那么如何改变信号量,就是PV操作了
定义理解
| 原语 | 全称(荷兰语) | 含义 | 本质动作 |
|---|---|---|---|
| P | Proberen | 测试 | 申请资源(可能阻塞) |
| V | Verhogen | 增加 | 释放资源(唤醒等待者) |
| pv原语是操作系统用于实现进程同步和互斥的原子操作,由荷兰计算机科学家Dijkstra提出。 | |||
| 讲一下个人的通俗理解 |
- p是申请占用一个资源,将信号量-1,如果信号量小于0,就进入阻塞,否则继续进行
- V是释放一个资源,将信号量+1,我理解为通知,因为释放一个资源后,如果有另一个等待该资源的进程,就可以通知他运行了。简答来说,释放是操作,通知是结果。
可以这么理解,当信号量的值为 2 的时候,表示有 2 个资源可以使用,当信号量的值为 -2 的时候,表示有两个进程正在等待使用这个资源。
如何实现互斥
总结一下:互斥等于进程共享一个初值为1的信号量,进入临界区前p一下,出来后v一下。
第一步:定义一个互斥信号量mutex
mutex是一个约定俗成的命名,看到就知道是用来做互斥保护临界区的。是 Mutex(Mutual Exclusion,互斥锁)的缩写
1 | semaphore mutex=1//初始化互斥信号量 |
(semaphore表示信号量类型,有的教材写作 sem_t)
为什么mutex要初始化为1?
反过来想,初始化为0会发生什么?
第一个进程执行p(mutex),s-1后s<0直接阻塞自己了
第二步:在临界区前后进行PV
也就是说把对临界资源的访问置于PV操作之间
1 | semaphore mutex=-1; |
也许你会疑惑p1p2不是重复的吗,但是这是一个重要思想
pv操作必须成对出现
如何实现同步
进程同步就是要各并发进程有序进行。比如司机售票员问题。需要售票员先关门,司机才能开车。而不能司机开车了售票员再关门。
同步 = 在先行进程里V一下(通知),在后行进程里P一下(等待),信号量初值0。
第一步:定义变量
| 同步关系 | 等待方 | 通知方 | 信号量命名 | 初值 |
|---|---|---|---|---|
| 关车门 → 启动 | 司机 | 售票员 | start |
0 |
| 停车 → 开车门 | 售票员 | 司机 | open |
0 |
| 为什么初值是0? | ||||
| 如果初值是1的话,不管售票员管没关门,司机都可以开车,同步就失效了。 | ||||
| 设置为0,那么先跑的人一定会被阻塞,从而实现“后行进程必须等待先行进程” |
1 | semaphore event=0; |
第二步:对每个对象进行思考
比如司机,需要进行开车停车,售票员则需要进行开门关门。关系为:
停车后->开门
关门后->开车
所以对于司机,他需要等待关门P,然后开车,车停后通知售票员开门V。
对于售票员,他需要通知司机可以启动V,然后等司机关门后停车P。
1 | 司机() { |
为什么 V(start) 要放在 售票() 之前,而不是之后?
因为V 应该尽早发(条件满足就立刻发通知),不需要等把本进程所有事做完。
这样可以提高并发度。
PV实例题目
1.家人吃水果(同步)
题目
v桌上有一空盘,最多允许存放一只水果。爸爸可向盘中放一个苹果或放一个桔子,儿子专等吃盘中的桔子,女儿专等吃苹果。
试用P、V操作实现爸爸、儿子、女儿三个并发进程的同步。
思路
- 设置信号量表示不同操作对象的状态
- 思考每个主体需要做的事
这道题目来说,盘子有一个,盘子有三个状态(空盘,桔子,苹果)。那我设置三个信号量S,So,Sa,表示是否空盘。能否取桔子和苹果。
然后对于爸爸来说,他需要等盘子空(p(s)),然后放水果。如果是苹果就要通知女儿(V(Sa))。反之亦然。
对于女儿来说,她需要等待苹果(p(Sa)),如果等到了她拿走,就要通知爸爸盘子空了(V(S))
代码
1 | semaphore S = 1; // 空盘信号量(初值1:一开始是空盘) |
2.读者登记表(互斥)
题目
有一阅览室,读者进入时必须先在一张登记表上进行登记,该表为每一座位列一表目,包括座号和读者姓名。读者离开时要消掉登记信号,阅览室中共有100个座位,请写出算法。
注意:不是生产者-消费者关系,而是多个读者共享一个登记表,所以主要是互斥
思路
- 找出资源和约束定义变量
- 理出流程
资源是座位和登记表,约束是,没有空座不能进,登记表只能一个人用。所以seats用来计数(初始值100),mutex用来保护登记表(初始时置为1)。
流程为:读者进入,申请座位。如果有座位就登记,登记完释放。阅读走之后再登记,再释放,然后释放座位。
为什么不能先释放座位再登记?,其实可以,但这样更优雅。
如果先 V(seats) 再注销:
- 座位已经释放了,其他读者可能被唤醒并进来
- 但这个人还占着登记表在注销
- 新进来的读者在
P(mutex)那里等 - 逻辑上没问题,但登记表被占用的时间变长了
标准做法让登记表尽快释放。
代码
1 | semaphore seats = 100; // 空座位数 |
channal
对比
- 用PV操作,是自己在管理“等待”和“通知”的逻辑。
- 用Channel,是把数据交给通道,同步的逻辑由通道自动完成。
可以将Channel理解为在更高编程语言层面,对PV操作和信号量机制的一种封装和升级。
channel 的两种主要同步模式
1. 同步(无缓冲)Channel:强制握手
这是最纯粹的“通道即同步”模式。在C++的CSP(Communicating Sequential Processes)库或类似实现中最为典型
- 核心机制:一个发送操作必须等到一个接收操作准备好,两者才会“相遇”(Rendezvous),完成数据传输。在此之前,双方都会阻塞等待
- 同步效果:它强制生产者和消费者在某一时刻同步,无需任何额外的锁或条件变量,就能保证“数据被发送”这一事件与“数据被接收”这一事件同时发生。
- 代码示例(概念性):
1 | // 伪代码,展示同步通道的行为 |
2. 异步(缓冲)Channel:解耦通信与同步
这种模式更侧重于通信,同步是作为队列满或空时的边界条件存在。
- 核心机制:通道内部有一个缓冲区。发送者将数据放入缓冲区,如果缓冲区没满,就立即返回,不会阻塞。接收者从缓冲区取数据,如果缓冲区不为空,也立即返回
- 同步效果:
- 生产快于消费:当缓冲区满了,下一次
send操作就会阻塞,直到消费者取走数据。此时,生产者被同步到消费者的进度上。 - 消费快于生产:当缓冲区空了,下一次
receive操作就会阻塞,直到生产者放入新数据。此时,消费者被同步到生产者的进度上。
- 生产快于消费:当缓冲区满了,下一次
- 代码示例(Boost.Fiber):
1 |
|