哲学家进餐问题
5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)
所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。
假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。
设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。
为了方便测试这里选用leetcode的题库
哲学家进餐
哲学家进餐问题是并发编程中较为经典的一道题目,我们必须要保证互斥的情况下避免死锁以及饥饿。
我们需要注意的地方有三点
- 只有拿到一双叉子后,哲学家才能吃饭
- 如果叉子已经被别人拿走,则需要等到他吃完放下后才能去拿
- 如果哲学家拿到了一只叉子,即使拿不到第二只,他也不会放下手中的叉子
叉子只有5个,而哲学家也有5个,在最坏情况下每个哲学家都只能获取一只叉子,导致死锁。所以我们必须要保证至少有一个哲学家能够拿到两只叉子
方法一:当两边的叉子都可用时才拿
当某一个哲学家能够同时拿起左右两只叉子时,才让他拿,这样就能够保证不会因为每个科学家都只拿了一只叉子而导致死锁。
为了保证能够同时拿起,我们需要对拿叉子这一步骤进行加锁,保证哲学家能够同时拿起一双叉子,而不会拿了一边后另一边被人抢走
class DiningPhilosophers {
public:
DiningPhilosophers()
{}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
//对拿叉子进行这一流程进行加锁,保证其能同时拿起一双,而不会被其他人抢走
_lock.lock();
_fork[philosopher].lock();
_fork[(philosopher + 1) % 5].lock();
_lock.unlock();
//拿起左右叉子
pickLeftFork();
pickRightFork();
eat(); //吃饭
//放下左右叉子
putLeftFork();
putRightFork();
//解锁,让其他人获取叉子
_fork[philosopher].unlock();
_fork[(philosopher + 1) % 5].unlock();
}
private:
mutex _lock;
mutex _fork[5];
};
方法二:限制就餐的哲学家数量
如果要保证至少有一个哲学家能够进餐,那么我们可以采用最简单粗暴的方法,限制人数,只要同时进餐的哲学家不超过四人时,即使在最坏情况下,也至少有一个哲学家能够拿到多出来的那一个叉子。
我们需要用到一个计数器来表示当前就餐的人数,为了保证线程安全我们需要用到一个互斥锁和一个条件变量对其进行保护
class DiningPhilosophers {
public:
DiningPhilosophers()
:_count(0)
{}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
unique_lock<mutex> lock(_mtx);
_cond.wait(lock, [this]()->bool{
return _count < 4;
}); //当就餐人数不超过四人的时候允许拿叉子
++_count;
_fork[philosopher].lock();
_fork[(philosopher + 1) % 5].lock();
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
_fork[philosopher].unlock();
_fork[(philosopher + 1) % 5].unlock();
--_count;
_cond.notify_one(); //就餐完成,让下一个人进来就餐
}
private:
mutex _fork[5];
mutex _mtx;
condition_variable _cond;
int _count;
};
方法三:奇数先左后右,偶数先右后左
由于餐桌是一个如下图的圆环,如果我们此时规定奇数位的哲学家先拿左边的叉子,再拿右边的叉子。而偶数位的哲学家先拿右边的再拿左边的,此时竞争情况如下图所示
此时2号和3号哲学家争抢3号叉子,4号和5号哲学家争抢5号叉子,1号没有竞争对手,直接获取叉子1。
可以看到,在第一轮中所有哲学家先去争抢奇数叉子,抢到偶数叉子后再去争抢偶数叉子,这样就能够保证至少有一个科学家能够获得两只叉子
class DiningPhilosophers {
public:
DiningPhilosophers()
{}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
//如果是奇数则先抢左后抢右
if(philosopher & 1)
{
_fork[philosopher].lock();
_fork[(philosopher + 1) % 5].lock();
pickLeftFork();
pickRightFork();
}
//如果是偶数则先抢右后抢左
else
{
_fork[(philosopher + 1) % 5].lock();
_fork[philosopher].lock();
pickRightFork();
pickLeftFork();
}
eat(); //吃饭
putLeftFork(); //放下叉子
putRightFork();
_fork[philosopher].unlock();
_fork[(philosopher + 1) % 5].unlock();
}
private:
mutex _fork[5];
};