os之PV

csdiy实验

本文将从定义,如何实现互斥,同步,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
2
3
4
5
6
7
8
9
10
11
semaphore mutex=-1;
p1(){
P(mutex);//申请占用资源
临界区
V(mutex);//释放资源
}
p2(){
P(mutex);
临界区
V(mutex);
}

也许你会疑惑p1p2不是重复的吗,但是这是一个重要思想
pv操作必须成对出现

如何实现同步

进程同步就是要各并发进程有序进行。比如司机售票员问题。需要售票员先关门,司机才能开车。而不能司机开车了售票员再关门。
同步 = 在先行进程里V一下(通知),在后行进程里P一下(等待),信号量初值0。

第一步:定义变量

同步关系 等待方 通知方 信号量命名 初值
关车门 → 启动 司机 售票员 start 0
停车 → 开车门 售票员 司机 open 0
为什么初值是0?
如果初值是1的话,不管售票员管没关门,司机都可以开车,同步就失效了。
设置为0,那么先跑的人一定会被阻塞,从而实现“后行进程必须等待先行进程”
1
semaphore event=0;

第二步:对每个对象进行思考

比如司机,需要进行开车停车,售票员则需要进行开门关门。关系为:
停车后->开门
关门后->开车
所以对于司机,他需要等待关门P,然后开车,车停后通知售票员开门V。
对于售票员,他需要通知司机可以启动V,然后等司机关门后停车P。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
司机() {
while(1) {
P(start); // 等关门
启动车辆();
正常行驶();
到站停车();
V(open); // 通知售票员:可以开门了
}
}

售票员() {
while(1) {
关车门();
V(start); // 通知司机:可以启动了
售票();
P(open); // 等停车
开车门();
}
}

为什么 V(start) 要放在 售票() 之前,而不是之后?
因为V 应该尽早发(条件满足就立刻发通知),不需要等把本进程所有事做完。
这样可以提高并发度。

PV实例题目

1.家人吃水果(同步)

题目

v桌上有一空盘,最多允许存放一只水果。爸爸可向盘中放一个苹果或放一个桔子,儿子专等吃盘中的桔子,女儿专等吃苹果。
试用P、V操作实现爸爸、儿子、女儿三个并发进程的同步。

思路

  • 设置信号量表示不同操作对象的状态
  • 思考每个主体需要做的事

这道题目来说,盘子有一个,盘子有三个状态(空盘,桔子,苹果)。那我设置三个信号量S,So,Sa,表示是否空盘。能否取桔子和苹果。
然后对于爸爸来说,他需要等盘子空(p(s)),然后放水果。如果是苹果就要通知女儿(V(Sa))。反之亦然。
对于女儿来说,她需要等待苹果(p(Sa)),如果等到了她拿走,就要通知爸爸盘子空了(V(S))

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
semaphore S = 1;    // 空盘信号量(初值1:一开始是空盘)
semaphore So = 0; // 桔子信号量(初值0:一开始没有桔子)
semaphore Sa = 0; // 苹果信号量(初值0:一开始没有苹果)

// 爸爸进程
father() {
while(1) {
P(S); // 等盘子空(盘子必须是空才能放)
放水果();
if (放的是桔子) {
V(So); // 通知儿子:有桔子了
} else {
V(Sa); // 通知女儿:有苹果了
}
}
}

// 儿子进程
son() {
while(1) {
P(So); // 等盘子里有桔子
取桔子();
V(S); // 通知爸爸:盘子空了(因为桔子被取走了)
吃桔子();
}
}

// 女儿进程
daughter() {
while(1) {
P(Sa); // 等盘子里有苹果

取苹果();
V(S); // 通知爸爸:盘子空了(因为苹果被取走了)

吃苹果();
}
}

2.读者登记表(互斥)

题目

有一阅览室,读者进入时必须先在一张登记表上进行登记,该表为每一座位列一表目,包括座号和读者姓名。读者离开时要消掉登记信号,阅览室中共有100个座位,请写出算法。
注意:不是生产者-消费者关系,而是多个读者共享一个登记表,所以主要是互斥

思路

  • 找出资源和约束定义变量
  • 理出流程

资源是座位和登记表,约束是,没有空座不能进,登记表只能一个人用。所以seats用来计数(初始值100),mutex用来保护登记表(初始时置为1)。

流程为:读者进入,申请座位。如果有座位就登记,登记完释放。阅读走之后再登记,再释放,然后释放座位。

为什么不能先释放座位再登记?,其实可以,但这样更优雅。
如果先 V(seats) 再注销:

  • 座位已经释放了,其他读者可能被唤醒并进来
  • 但这个人还占着登记表在注销
  • 新进来的读者在 P(mutex) 那里等
  • 逻辑上没问题,但登记表被占用的时间变长了
    标准做法让登记表尽快释放。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
semaphore seats = 100;   // 空座位数
semaphore mutex = 1; // 登记表互斥信号量
// 读者进程
reader() {
while(1) {
P(seats); // 1. 申请座位(没空就等)
P(mutex); // 2. 申请登记表
登记(); // 写信息
V(mutex); // 3. 释放登记表
阅读(); // 4. 读书
P(mutex); // 5. 申请登记表
注销(); // 删信息
V(mutex); // 6. 释放登记表
V(seats); // 7. 释放座位
}
}

channal

对比

  • PV操作,是自己在管理“等待”和“通知”的逻辑。
  • Channel,是把数据交给通道,同步的逻辑由通道自动完成。
    可以将Channel理解为在更高编程语言层面,对PV操作和信号量机制的一种封装和升级

channel 的两种主要同步模式

1. 同步(无缓冲)Channel:强制握手

这是最纯粹的“通道即同步”模式。在C++的CSP(Communicating Sequential Processes)库或类似实现中最为典型

  • 核心机制:一个发送操作必须等到一个接收操作准备好,两者才会“相遇”(Rendezvous),完成数据传输。在此之前,双方都会阻塞等待
  • 同步效果:它强制生产者和消费者在某一时刻同步,无需任何额外的锁或条件变量,就能保证“数据被发送”这一事件与“数据被接收”这一事件同时发生。
  • 代码示例(概念性)
1
2
3
4
5
6
7
8
9
10
// 伪代码,展示同步通道的行为
channel<int> sync_chan; // 假设这是一个同步(无缓冲)通道

// 线程 A (发送者)
sync_chan.send(42); // 此处会阻塞,直到线程B准备好接收

// 线程 B (接收者)
int value = sync_chan.receive(); // 此处会阻塞,直到线程A发送数据
// 当线程A的send和线程B的receive都执行到对应行时,数据被原子地传递,
// 然后两个线程可以继续执行。

2. 异步(缓冲)Channel:解耦通信与同步

这种模式更侧重于通信,同步是作为队列满或空时的边界条件存在。

  • 核心机制:通道内部有一个缓冲区。发送者将数据放入缓冲区,如果缓冲区没满,就立即返回,不会阻塞。接收者从缓冲区取数据,如果缓冲区不为空,也立即返回
  • 同步效果
    • 生产快于消费:当缓冲区满了,下一次send操作就会阻塞,直到消费者取走数据。此时,生产者被同步到消费者的进度上。
    • 消费快于生产:当缓冲区空了,下一次receive操作就会阻塞,直到生产者放入新数据。此时,消费者被同步到生产者的进度上。
  • 代码示例(Boost.Fiber)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <boost/fiber/buffered_channel.hpp>
#include <boost/fiber/fiber.hpp>
#include <iostream>

int main() {
// 创建一个容量为2的缓冲通道
boost::fibers::buffered_channel<int> chan{2};

boost::fibers::fiber producer([&chan] {
for (int i = 0; i < 5; ++i) {
chan.push(i); // push 操作可能因为队列满而阻塞
std::cout << "Produced: " << i << std::endl;
}
chan.close();
});

boost::fibers::fiber consumer([&chan] {
int i;
while (boost::fibers::channel_op_status::success == chan.pop(i)) {
std::cout << "Consumed: " << i << std::endl;
// pop 操作可能因为队列空而阻塞
}
});

producer.join();
consumer.join();

return 0;
}