本文来自小林coding
哲学家就餐问题
哲学家就餐问题对于互斥访问有限的竞争问题(如IO设备)以类的建模过程十分有用。
方案一:先拿做再拿右
很简单我们直接定一个规矩,每个哲学家都先去拿左边 的叉子、然后去拿右边的叉子;
然后进餐;
最后放下左边的叉子、放下右边的叉子;
但是,如果每个哲学家同时拿到了左边的叉子,就会导致死锁。
方案二:共享区加锁
对于方案一的死锁,我们如何解决?
很简单,在临界区加锁即可。
这样的话,每次进餐只能有一位哲学家能吃。
方案三:根据不同的编号采取不同的动作
我们先重申一下问题,既然方案二使用互斥信号量,会导致只能允许一个哲学家就餐,那么我们就不用它;
方案一的问题在于,会出现所有哲学家同时拿左边刀叉的可能性,那我们就避免哲学家可以同时拿左边的刀叉,用分支结构根据哲学家的编号不同,而采取不同的动作。
让偶数编号的哲学家「先拿左边的拆后拿右边的拆」,奇数编号的哲学家「先拿右边的叉子后拿左边的叉子」。
这里写一个简单的伪代码:
cpp
#define N 5
semaphore fork[N];
void smart_person(int i) {
while (TRUE) {
think(); // 哲学家思考
if (i % 2 == 0) {
P(fork[i]); // 拿左边叉子
P(fork[(i + 1) % N]); // 拿右边叉子
} else {
P(fork[(i + 1) % N]); // 先拿右边叉子
P(fork[i]); // 再拿左边叉子
}
eat(); // 哲学家就餐
V(fork[i]); // 放左边的叉子
V(fork[(i + 1) % N]); // 放右边的叉子
}
}
方案四:比较复杂
我们用一个数组 state 来记录每一位哲学家的三种状态,分别是:进餐状态、思考状态、饥饿状态(试图拿叉子)
我们规定只有两个邻居都没有进餐时,才可以进入进餐状态。
第 i
个哲学家的左邻右舍,由宏 LEFT
和 RIGHT
定义:
- LEFT:(i + 5 - 1) % 5
- RIGHT:(i + 1) % 5
比如 i 为 2 ,则LEFT
为1,RIGHT
为3。
cpp
#define N 5
#define LEFT (i + N - 1) % N
#define RIGHT (i + 1) % N
#define THINKING 0 // 思考状态
#define HUNGRY 1 // 饥饿状态
#define EATING 2 // 进餐状态
int state[N]; //数组记录每个哲学家的状态
semaphore s[5]; // 每个哲学家一个信号量,初值 0
semaphore mutex; // 互斥信号量,初值为1
void test(int i) {
if (state[i] == HUNGRY &&
state[LEFT] != EATING &&
state[RIGHT] != EATING) {
state[i] = EATING;//两把叉子到手,进入就餐状态
V(s[i]);//通知第i个哲学家就餐
}
}
// 要么拿到两把叉子,要么被阻塞
void take_forks(int i) {
P(mutex); //进入临界区
state[i] = HUNGRY;
test(i);
V(mutex);
P(s[i]); // 没有叉子就阻塞,有就继续执行
}
//两把叉子放回原处,并在需要的时候唤醒左邻右舍
void put_forks(int i) {
P(mutex);
state[i] = THINKING; // 吃完饭叫出叉子标记思考状态
test(LEFT);
test(RIGHT);
V(mutex);
}
void smart_person(int i) {
while(TRUE) {
think();
take_forks(i);//准备去拿叉子
eat();
put_forks(i); // 吃完饭放叉子
}
}
读者-写者问题
它为数据库访问建立了一个模型。
读者只会读取数据,不会修改数据,而写者即可以读数据也可以修改数据。
- 【读-读】允许:同一时刻,允许多个读者同时读
- 【读-写】互斥:没有写者时读者才能读,没有读者时写者才能写
- 【写-写】互斥:没有其他写着,写着才能写。
方案一:用信号量
信号量wMutex
:控制写操作的互斥信号量,初始值为1;
读者计数rCount
:正在进行读操作的读者个数,初始化为0;
信号量rCountMutex
:控制对rCount读者计数器的互斥修改,初始值为1;
cpp
semaphore wMutex; // 控制写操作的互斥信号量,初始值为1
semaphore rCountMutex; // 控制对Rcount 的互斥修改,初始值为1
int rCount = 0; // 正在进行读操作的读者个数,初始化为0
//写进程/线程执行函数
void writer() {
while (TRUE) {
P(wMutex); // 进入临界区
write();
V(wMutex); //进入临界区
}
}
void reader() {
while (TRUE) {
P(rCountMutex); //进入临界区
if (rCount == 0) {
P(wMutex); //如果有写者,阻塞写者
}
rCount++; // 读者计数+1
V(rCountMutex); //离开临界区
read(); // 读数据
P(rCountMutex); // 进入临界区
rCount--; // 读完数据,准备离开
if (rCount == 0) {
V(wMutex); //最后一个读者离开,唤醒写者
}
V(rCountMutex); // 离开临界区
}
}
上面这个实现是读者优先,只要读者正在读,后来的读者都可以直接进入,如果读者不断进入,写着会处于饥饿状态。
方案二:写者优先
- 只要有写者准备要写入,写者应尽快执行写操作,后来的读者必须阻塞;
- 如果有写者持续不断写入,则读者处于饥饿。
方案一的基础上增加如下变量:
这里的 rMutex
的作用:开始有多个读者读数据,他们全部进入读者队列,此时来了一个写者,执行了 P(rMutex)
之后,后续的读者由于阻塞在了rMutex
上,都不能再进入读者队列,而写者到来,则可以全部进入写者队列,因此保证了写者优先。
同时,第一个写者执行了 P(rMutex)
之后,也不能马上开始写,必须等到所有进入读者队列的读者都执行完读操作,通过 V(wDataMutex)
唤醒写者的写操作。
⭐️方案三:公平策略
- 优先级相同;
- 写者、读者互斥访问;
- 只能一个写者访问临界区;
- 可以有多个读者同时访问临界区;