PV操作:
PV操作包括P操作(Proberen,尝试)和V操作(Verhogen,增加),它们通过对信号量的操作来协调进程之间的行为。
信号量(Semaphore):(理解为flag这样的变量吧)值表示可用资源的数量或等待资源的进程数量。
P操作(Proberen):用于申请资源。当进程执行P操作时,会检查信号量的值:如果信号量的值大于0,则将其减1,进程继续执行。如果信号量的值为0,则进程被阻塞,等待信号量变为正值。
V操作(Verhogen):V操作用于释放资源。当进程执行V操作时,会将信号量的值加1;如果信号量的值大于0,则进程继续执行。如果信号量的值小于或等于0,则唤醒一个等待该信号量的进程。
特点:
不可中断:PV操作不可被中断;
成对出现:上有P,下必有V。(资源只是被借用,借完要还。)
(经典的)司机与售票员问题:
这里PV操作用于同步。
(经典的)生产者-消费者问题:
比如有一个槽,生产者不断往里放东西,消费者不断往外拿东西;如果槽满了,生产者就不能再生产东西往槽里放了;再如果槽空了,消费者就啥也不拿了;此外,生产者和消费者不同时访问这个槽。
这个槽叫做:缓冲区。现在我们使用PV操作来描述这个问题,然后你就会发现这PV操作是多么好用……
我们先使用几个信号量:empty表示缓冲区空闲的位置,full表示缓冲区被占用的位置,mutex表示进程对缓冲区进行了互斥的访问(mutex出现,代表着“我在访问临界区,别人不要进来”)。
其实不需要纠结Pmutex和Vmutex到底是啥操作,你就理解为上厕所时要给门上锁开锁。
下面时生产者和消费者的代码,千万要注意,信号量empty和full是有空闲和有占用(there exists)的意思啊!不是全空或全满!(not all)
void producer() {
while (true) {
// 生产数据
produce_item();
// 等待空闲位置
P(empty);
// 进入临界区,互斥访问缓冲区
P(mutex);
// 将数据放入缓冲区
put_item_into_buffer();
V(mutex);
// 增加已占用位置的数量
V(full);
}
}
void consumer() {
while (true) {
// 等待有数据可消费
P(full);
// 进入临界区,互斥访问缓冲区
P(mutex);
// 从缓冲区取出数据
take_item_from_buffer();
V(mutex);
// 增加空闲位置的数量
V(empty);
// 消费数据
consume_item();
}
}
那么再讲一下这个吧:
某寺庙有小和尚、老和尚若干,庙内有一个水缸,由小和尚提水入缸,供老和尚饮用。
水缸可以容纳30桶水,每次入水、取水仅为1桶,不可同时进行。水取自同一口井中,水井径
窄,每次只能容纳一个水桶取水。设水桶个数为5个,试用信号量和PV操作给出老和尚和小
和尚的活动。
典型的生产者消费者问题(对吧):
生产者:小和尚
消费者:老和尚
缓冲区(有限的且被主要针对的):水缸,长度(容量)为30桶水。
有限的(除了缓冲区):桶(5个)。
临界区(冲突,不能同时进行的):取水/入水(mutex的地方,每次入水、取水仅为1桶,不可同时进行)
悟:PV操作针对所有的有限资源。我们这里有两个缓冲区 -- 水缸,桶,那么就分别会有一对PV操作来描述他们。
void little_monk() {
while (true) {
// 等待空闲位置
P(empty);
// 等待可用的水桶
P(bucket);
// 进入临界区,互斥访问水缸
P(mutex);
// 将水倒入水缸
pour_water_into_tank();
V(mutex);
// 增加已占用位置的数量
V(full);
// 增加可用的水桶数量
V(bucket);
}
}
void old_monk() {
while (true) {
// 等待有水可饮用
P(full);
// 等待可用的水桶
P(bucket);
// 进入临界区,互斥访问水缸
P(mutex);
// 从水缸中取水
take_water_from_tank();
V(mutex);
// 增加空闲位置的数量
V(empty);
// 增加可用的水桶数量
V(bucket);
}
}
接着讲第二个经典操作:读者写者问题。
纯沙比的问题:描述为:比如你有一份共享文档,可以很多人一起读,但是同一时刻只有一个人才可写。当然我们说的不是踏马的文档。我是说进程。理解这个问题之后,看看官方是怎么说的:
“读者-写者问题是操作系统中经典的同步问题,用于描述多个进程(线程)对共享资源的访问问题。在这个问题中,共享资源可以被多个读者进程并发访问,但写者进程在写入数据时需要独占访问。读者和写者之间需要通过同步机制来协调它们的行为,以避免数据不一致或冲突。”
同上面的读者写者问题一样,我们先看:
临界区:这份文档
冲突的:读者和写者,写者和写者
引入以下信号量和变量:
mutex:用于对共享资源的互斥访问,初始值为1。
wrt:用于控制写者对共享资源的独占访问,初始值为1。
rc:一个整数变量,用于记录当前正在读取的读者数量,初始值为0。
然而这里的mutex不是描述这份“文档”(虽然看起来文档才是临界区):
在读者-写者问题中,mutex通常用于:
保护共享资源的访问,例如读者计数器rc的更新。
确保写者在写入时对共享资源的独占访问。
void writer() {
while (true) {
// 等待独占访问
P(wrt);
// 修改共享资源
write_shared_resource();
// 释放独占访问
V(wrt);
}
}
void reader() {
while (true) {
// 等待可以读取
P(can_read);
// 等待进入读取状态
P(mutex);
rc++;
if (rc == 1) {
// 如果是第一个读者,阻塞写者
P(wrt);
}
V(mutex);
// 读取共享资源
read_shared_resource();
// 等待离开读取状态
P(mutex);
rc--;
if (rc == 0) {
// 如果是最后一个读者,释放写者
V(wrt);
}
V(mutex);
// 释放可以读取
V(can_read);
}
}
这里出现了两组PVmutex:第一组:置Pwrt的时候,只需要一个读者,第二组:释放wrt的时候,也只需要一个读者,避免了多个读者同时阻止/释放wrt的情况。