update ch2.3 经典同步问题

This commit is contained in:
Penguin-SAMA
2023-06-07 00:02:46 +08:00
parent 8b4c050e47
commit 51e6e076ad
5 changed files with 390 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1326,3 +1326,393 @@ void consumer() { //消费者进程
哲学家进餐问题是进程同步的一个典型问题。
> 问题描述:
>
> 一张圆桌边上坐着5名哲学家每两名哲学家之间的桌上摆一根筷子两根筷子中间是一碗米饭。哲学家们倾注毕生精力用于思考和进餐哲学家在思考时并不影响他人。只有当哲学家饥饿时才视图拿起左、右两根筷子(一根一根地拿起)。若筷子已在他人手上,则需要等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,进餐完毕后,放下筷子继续思考。
<img src="./assets/v2-6fdb4a2f77bcd5a201454af1fdb3335e_720w.webp" alt="img" style="zoom:67%;" />
> 问题分析:
>
> - 关系分析。5名哲学家与左右邻居对其中间筷子的访问是互斥关系。
> - 整理思路。本题的关键是如何让一名哲学家拿到左右两根筷子而不造成死锁或饥饿现象。
> - 解决方法:
> 1. 让他们同时拿两根筷子。
> 2. 对每名哲学家的动作制定规则,避免饥饿或死锁现象的发生。
> - 信号量设置。定义互斥信号量数组`chopstick[5] = {1, 1, 1, 1, 1}`用于对5个筷子的互斥访问。哲学家按顺序编号为`0 ~ 4`,哲学家$ i $左边筷子编号为$ i $,哲学家右边筷子的编号为$ (i + 1)$ % 5。
``` c
semaphore chopstick[5] = {1, 1, 1, 1, 1}; //定义信号量数组并初始化
Pi() {
do {
P(chopstick[i]); //取左边筷子
P(chopstick[(i + 1) % 5]); //取右边筷子
eat;
V(chopstick[i]); //放回左边筷子
V(chopstick[(i + 1) % 5]); //放回右边筷子
think;
} while (1);
}
```
> ❗该算法存在以下问题:
>
> - 当5名哲学家都想要进餐并分别拿起左边的筷子时(都恰好执行完`wait(chopstick[i])`)筷子已被拿光,等到他们再想拿右边的筷子时(执行`wait(chopstick[(i+1)%5])`)就全被阻塞,因此出现了死锁。
>
> 💡可以施加一些限制条件,以防止死锁发生:
>
> - 至多允许4名哲学家同时进餐(破坏“循环等待”条件)
> - 仅当一名哲学家左右两边的筷子都可用时,才允许他抓起筷子;(破坏“请求和保持”条件)
> - 对哲学家进行编号,要求奇数号哲学家先拿左边的筷子,然后拿右边筷子,偶数号哲学家相反。(破坏“
> 循环等待”条件)
假设采用第二种限制条件制定正确规则:
```c
semaphore chopstick[5] = {1, 1, 1, 1, 1}; //初始化信号量
semaphore mutex = 1; //设置取筷子的信号量
Pi() {
do {
P(mutex); //在取筷子前获得互斥量
P(chopstick[i]); //取左边筷子
P(chopstick[(i + 1) % 5]); //取右边筷子
V(mutex); //释放取筷子的信号量
eat;
V(chopstick[i]); //放回左边筷子
V(chopstick[(i + 1) % 5]); //放回右边筷子
think;
} while (1);
}
```
### 3. 读者—写者问题
读者—写者问题是对数据对象的访问模型。
> 问题描述:
>
> 有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程同时访问共享数据时则可能导致数据不一致的错误。因此要求:
>
> 1. 允许多个读者可以同时对文件执行读操作;
> 2. 只允许一个写者往文件中写信息;
> 3. 任意一个写者在完成写操作之前不允许其他读者或写者工作;
> 4. 写者执行写操作前,应让已有的读者和写者全部退出。
#### 读者优先
在该算法中,读进程具有优先权,其访问临界区时,只要有一个读进程在读,读进程就能继续获得对临界区的控制权,但可能会导致写进程饥饿。
> 算法描述:
>
> - 当一个读进程正在读取文件时,写进程必须等待。
> - 在写进程等待时,随后的读进程仍可以进入临界区读取。
> - 只有在没有读进程在读取的情况下,写进程才可以写入。
```c
semaphore rmutex = 1; //定义readcount的互斥信号量初始值为1
semaphore wmutex = 1; //定义用于写的互斥信号量初始值为1
int readcount = 0; //用于记录当前读者数量初始值为0
void reader() {
do {
P(rmutex); //(readcount操作互斥)对rmutex执行P操作防止他人访问
if(readcount == 0)
P(wmutex); //(同步)如果自己是第一个读者,禁止写者写
readcount++; //读者数量+1
V(rmutex); //(readcount操作互斥)对rmutex执行V操作允许他人访问
进行读操作;
P(rmutex); //(readcount操作互斥)对rmutex执行P操作防止他人访问
readcount--; //读者数量-1
if (readcount == 0)
V(wmutex); //(同步)如果自己是最后一个读者,允许写者写
V(rmutex); //(readcount操作互斥)对rmutex执行V操作允许他人访问
} while (1);
}
void writer() {
do {
P(wmutex); //(写互斥)对wmutex执行P操作防止他人访问文件
进行写操作;
V(wmutex); //(写互斥)对wmutex执行V操作允许他人访问文件
} while (1);
}
```
#### 写者优先
在该算法中,写进程具有优先权,如果有写进程希望访问临界区,就会禁止新的读进程进入临界区,解决了写进程饥饿问题。
> 算法描述:
>
> - 当一个读进程在读取时,写进程不能进入临界区。
> - 当一个写进程表示希望进行写入时,随后的读进程不得进入临界区读取。
> - 只有当所有的写进程都离开临界区后,读进程才可以进入临界区读取。
```c
semaphore rmutex = 1; //定义readcount的互斥信号量初始值为1
semaphore wmutex = 1; //定义writecount的互斥信号量初始值为1
semaphore read_write = 1; //用于在读取时检测是否有写者的互斥信号量初始值为1
semaphore write_read = 1; //用于在写入时防止他人进入的互斥信号量初始值为1
int readcount = 0; //用于记录当前读者数量初始值为0
int writecount = 0; //用于记录当前写者数量初始值为0
void reader() {
do {
P(read_write); //(读写互斥)在读取时检测是否有写者,如有写者则等待
P(rmutex); //(readcount操作互斥)对rmutex执行P操作防止其他读者访问
readcount++; //读者数量+1
if (readcount == 1)
P(write_read); //(读写互斥)如果自己是第一个读者,禁止写者写
V(rmutex); //(readcount操作互斥)对rmutex执行V操作允许其他读者访问
V(read_write); //(读写互斥)允许其他读者或写者访问文件
进行读操作;
P(rmutex); //(readcount操作互斥)对rmutex执行P操作防止其他读者访问
if (readcount == 1)
V(write_read); //(读写互斥)如果自己是最后一个读者,允许他人读写
readcount--; //读者数量-1
V(rmutex); //(readcount操作互斥)对rmutex执行V操作允许其他读者访问
} while (1);
}
void writer() {
do {
P(wmutex); //(写互斥)对wmutex执行P操作防止其他写者访问writecount
writecount++; //写者数量+1
if (writecount == 1)
P(read_write); //(读写互斥)如果自己是第一个写者,禁止读者读
V(wmutex); //(写互斥)对wmutex执行V操作允许其他写者访问writecount
P(write_read); //(读写互斥)若没有读者或写者在访问文件,则可写入
进行写操作;
V(write_read); //(读写互斥)写操作完成,允许其他读者或写者访问文件
P(wmutex); //(写互斥)对wmutex执行P操作防止其他写者访问writecount
if (writecount == 1)
V(read_write); //(读写互斥)如果自己是最后一个写者,允许读者读
writecount--; //写者数量-1
V(wmutex); //(写互斥)对wmutex执行V操作允许其他写者访问writecount
} while (1);
}
```
#### 读写公平
读写公平算法不偏袒读者或写者中的任何一方,仅根据读写请求的到达顺序来赋予读写的权利。
> 算法描述:
>
> - 当没有读进程或写进程时,允许读进程或写进程进入。
> - 当一个读进程试图读取时,若有写者正在等待写操作或进行写操作时,应等待其完成写操作后,才能开始读操作。
> - 同理,当一个写进程试图写入时,若有读者正在等待或进行读操作时,也需要进行等待。
```c
semaphore rmutex = 1; //定义readcount的互斥信号量初始值为1
semaphore wmutex = 1; //定义用于写的互斥信号量初始值为1
semaphore read_write = 1; //用于在读取时检测是否有写者的互斥信号量初始值为1
int readcount = 0; //用于记录当前读者数量初始值为0
void reader() {
do {
P(read_write); //(读写互斥)在读取时检测是否有写者,如有写者则等待
P(rmutex); //(readcount操作互斥)对rmutex执行P操作防止他人访问
if (readcount == 0)
P(wmutex); //(同步)如果自己是第一个读者,禁止写者写
readcount++; //读者数量+1
V(rmutex); //(readcount操作互斥)对rmutex执行V操作允许他人访问
V(read_write); //(读写互斥)允许其他读者或写者访问文件
进行读操作;
P(rmutex); //(readcount操作互斥)对rmutex执行P操作防止他人访问
readcount--; //读者数量-1
if (readcount == 0)
V(wmutex); //(同步)如果自己是最后一个读者,允许写者写
V(rmutex); //(readcount操作互斥)对rmutex执行V操作允许他人访问
} while (1);
}
void writer() {
do {
P(read_write); //(读写互斥)检测是否有读者或写者,如有则等待
P(wmutex); //(写互斥)对wmutex执行P操作防止他人访问文件
进行写操作;
V(wmutex); //(写互斥)对wmutex执行V操作允许他人访问文件
V(read_write); //(读写互斥)允许其他读者或写者访问文件
} while (1);
}
```
### 吸烟者问题
> 问题描述:
>
> 假设一个系统有三个抽烟者进程和一个供应者进程。每个吸烟者不停地卷烟并抽掉它,但要卷起并抽掉一支烟,抽烟者需要有三种材料:**烟草、纸和胶水**。三个抽烟者中,第一个拥有烟草,第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,此时供应者就会将另外两种材料放到桌上,如此重复。
![image-20230606225659418](./assets/image-20230606225659418.png)
> 问题分析:
>
> - 关系分析。供应者与三个抽烟者分别是同步关系。由于供应者无法同时满足两个或以上的抽烟者,三个抽烟者对抽烟这个动作互斥。
> - 整理思路。显然这里有四个进程。供应者作为生产者向三个抽烟者提供材料。
> - 信号量设置。信号量`offer1``offer2``offer3`分别表示烟草和纸组合的资源、烟草和胶水组合的资源、纸和胶水组合的资源。信号量`finish`用于互斥进行抽烟动作
```c
int num = 0; //存储随机数
semaphore offer1 = 0; //定义信号量对应烟草和纸组合的资源
semaphore offer2 = 0; //定义信号量对应烟草和胶水组合的资源
semaphore offer3 = 0; //定义信号量对应纸和胶水组合的资源
semaphore finish = 0; //定义信号量表示抽烟是否完成
process P1() { //供应者
while (1) {
num++;
num = num % 3;
if (num == 0)
V(offer1); //提供烟草和纸
else if (num == 1)
V(offer2); //提供烟草和胶水
else
V(offer3); //提供纸和胶水
任意两种材料放在桌上;
P(finish);
}
}
process P2() { //拥有烟草者
while (1) {
P(offer3);
拿纸和胶水,卷成烟,抽掉;
V(finish);
}
}
process P3() { //拥有纸者
while (1) {
P(offer2);
拿烟草和胶水,卷成烟,抽掉;
V(finish);
}
}
process P4() { //拥有胶水者
while (1) {
P(offer1);
拿烟草和纸,卷成烟,抽掉;
V(finish);
}
}
```
## 2.3.9 管程
### 管程的定义
管程的特性保证了进程互斥,无须程序员自己实现互斥,从而降低了死锁发生的可能性。同时管程提供了条件变量,可以让程序员灵活地实现进程同步。
引入管程的**目的**
- 集中管理分散于不同进程的临界区。
- 防止进程违反同步操作。
- 便于用高级语言编写程序、检查程序的正确性。
管程是一个软件模块,由一些公共变量及其描述和所有访问这些变量的进程组成。进程对共享资源的请求、释放和其他操作必须通过同一个管程来实现。当多个进程请求访问同一资源时,会根据资源的情况接收或拒绝,确保每次只有一个进程进入临界区,有效实现进程互斥。
管程的语法描述:
```c
Monitor monitor_name { //管程名
share variable declarations; //共享变量说明
cond declarations; //条件变量说明
public: //能被进程调用的过程
void P1(...){
...;
}
void P2(...){
...;
}
void P3(...){
...;
}
{ //管程主体
initialization code; //初始化代码
...;
}
}
```
管程包含了面向对象的思想,将表达共享资源的数据结构和对其进行操作的进程封装在一个对象中,并封装了同步操作,从而对进程隐藏同步的细节,简化了调用同步功能的接口。
<img src="./assets/image-20230606233539715.png" alt="image-20230606233539715" style="zoom: 80%;" />
管程由**程序的名称**、**程序本地的共享数据结构**、**一组对数据结构进行操作的程序**、**一条初始化本地共享数据的语句**组成。
管程具有以下特性:
- 共享性。管程可以被系统中的不同进程以互斥方式访问,是一种共享资源。
- 安全性。管程的过程只能访问属于本管程的局部变量,甚至不能访问其局部变量以外的变量。
- 互斥性。进程对管程的访问是互斥的。在任何是否,一个管程中只能有一个活动进程。
- 封装性。管程中的过程只能使用管程中的数据结构,同时,管程中的数据结构都是私有的,也只能在管程中使用,进程可以通过调用管程中的过程来使用临界资源。
### 条件变量
条件变量是管程内的一种全局数据结构。
当一个进程进入管程后被阻塞,直到阻塞的原因解除时,在此期间,如果该进程不释放管程,那么其他进程无法进入管程。为此,将阻塞原因定义为条件变量`condition`
> 条件变量与P/V操作的区别
>
> 条件变量作为一种共享数据结构,进程在调用管程时,如遇到阻塞/唤醒条件时,会自行调用相应原语,将自己插入到对应的等待队列中(或从中释放),避免对管程的持续占用,实现管程的有序排队使用。
条件变量对管程中的所有过程时全局性的对条件变量x的操作如下
1. `x.wait()` 若调用管程的进程由于条件x而需要被阻塞或暂停则释放管程挂起调用进程并插入到条件x的等待队列中直到另一个进程向条件变量执行了`x.signal()`操作。
2. `x.signal()`当调用管程的进程注意到条件x已经改变调用对应的`x.signal()`操作。如果存在某个进程由于`x.wait()`操作被挂起,则将其释放;如果没有其他进程被阻塞,则不做任何操作。
3. `x.queue()`:如果至少有一个进程由于条件变量而被挂起,就返回`TRUE`,否则返回`FALSE`
条件变量的定义:
```c
monitor Demo {
共享数据结构 S;
condition x; //定义一个条件变量x
init_code() {...}
take_away() {
if (S <= 0)
x.wait(); //资源不够在条件变量x上阻塞等待
资源足够,分配资源,做一系列相应处理;
}
give_back() {
归还资源,做一系列相应处理;
if (有进程在等待)
x.signal(); //唤醒一个阻塞进程
}
}
```
> 条件变量和信号量的对比:
>
> **相似点:**条件变量的`wait`/`signal`操作类似于信号量的P/V操作可以实现进程的阻塞/唤醒。
>
> **不同点:**条件变量是**没有值**的,仅实现了**排队等待**功能;
>
> 而信号量是**有值**的,信号量的值反映了剩余资源数,而在管程中,剩余资源数用共享数据结构记录。
# 2.4 死锁