哲学家就餐是很经典的并发问题,问题描述如下
五位哲学家在同一张桌子旁吃饭。每个哲学家在餐桌上都有自己的位置。每个盘子之间都有一把叉子。这道菜是一种意大利面,必须用两把叉子吃。每个哲学家只能思考或者吃。此外,哲学家只有在有左右叉子的时候才能吃意大利面。因此,只有当离他们最近的两个邻居在思考而不是吃饭时,他们才会有两把叉子。哲学家吃完饭后,会把叉子都放下。问题是如何设计一种方案(一种并行算法),使哲学家不会挨饿;也就是说,每个人都可以永远在吃和思考之间交替,假设没有哲学家可以知道其他人什么时候想吃或想思考(一个信息不完全的问题)。
由题可以得出以下代码来实现哲学家就餐
type chopstick struct {
mu *sync.Mutex
}
type philoStatus int
const (
sleep philoStatus = iota
think
tryEat
eat
)
type philosopher struct {
name string
leftChopstick, rightChopstick *chopstick
status philoStatus
}
func newPhilosopher(name string, lc, rc *chopstick) *philosopher {
return &philosopher{
name: name,
leftChopstick: lc,
rightChopstick: rc,
status: sleep,
}
}
func (p *philosopher) dineNormal() {
for {
p.status = tryEat
p.leftChopstick.mu.Lock()
fmt.Printf("philosohper %s pick left chopstick \n", p.name)
p.rightChopstick.mu.Lock()
fmt.Printf("philosohper %s pick right chopstick \n", p.name)
p.status = eat
randomPause(100)
p.leftChopstick.mu.Unlock()
p.rightChopstick.mu.Unlock()
fmt.Printf("philosohper %s eaten \n", p.name)
p.status = think
}
}
func randomPause(max int) {
time.Sleep(time.Duration(rand.Intn(max)) * time.Millisecond)
}
限制就餐人数
有一个抽屉定理——若每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素
因此我们可以直接限制哲学家就餐人数。在哲学家人数为n时,限制最多只有(n-1)个哲学家能够同时就餐
func (p *philosopher) setActiveLimit(count int) {
if count <= 0 {
panic("wrong count")
}
if count==1 {
return
}
p.controls = make(chan struct{}, count-1)
}
func (p *philosopher) dineLimitEven() {
for {
// 1. 判断是否有数量限制
if p.controls != nil {
p.controls <- struct{}{}
}
// 2. 正常恰饭
p.status = tryEat
p.leftChopstick.mu.Lock()
fmt.Printf("philosohper %s pick left chopstick \n", p.name)
p.rightChopstick.mu.Lock()
fmt.Printf("philosohper %s pick right chopstick \n", p.name)
p.status = eat
randomPause(100)
p.leftChopstick.mu.Unlock()
p.rightChopstick.mu.Unlock()
fmt.Printf("philosohper %s eaten \n", p.name)
p.status = think
if p.controls != nil {
<-p.controls
}
}
}
奇家先拿左,偶家先拿右
所谓奇家先拿左,偶家先拿右就是给哲学家们编号,让奇数的哲学家先拿左边的餐具,偶数的哲学家先拿右边的餐具。
在只有A、B、C三个哲学家的情况下。这样的拿法就必定会使旁边的哲学家A、B第一选择产生冲突,让哲学家A能够拿到一个冲突的餐具,让哲学家B一直等待,从而为B旁边的C流出第一选择餐具,那么只要A、C任何一个拿到第二个餐具都不会出现死锁
// 首先给哲学家们编号
func (p *philosopher) setID(id int) {
p.id = id
}
func (p *philosopher) dineOddLeftEvenRight() {
for {
p.status = tryEat
// 奇数哲学家先左后右
if p.id%2 == 1 {
p.leftChopstick.mu.Lock()
fmt.Printf("philosohper %s pick left chopstick \n", p.name)
p.rightChopstick.mu.Lock()
fmt.Printf("philosohper %s pick right chopstick \n", p.name)
p.status = eat
randomPause(100)
p.leftChopstick.mu.Unlock()
p.rightChopstick.mu.Unlock()
} else {
// 偶数哲学家先右后左
p.rightChopstick.mu.Lock()
fmt.Printf("philosohper %s pick left chopstick \n", p.name)
p.leftChopstick.mu.Lock()
fmt.Printf("philosohper %s pick right chopstick \n", p.name)
p.status = eat
randomPause(100)
p.rightChopstick.mu.Unlock()
p.leftChopstick.mu.Unlock()
}
fmt.Printf("philosohper %s eaten \n", p.name)
p.status = think
}
}
资源分级
另外一种方法就是对资源进行分级,使每一个哲学家总是先高后低的获取资源。
因为必定存在一个最高级别的资源被两个哲学家同时抢占,那么必定结果是一个哲学家抢到,而另一个哲学家堵塞,那么就回到了限制就餐人数的状态,从而达到防止死锁的目的
不过资源分级最好不要有同级别资源,若存在同级别资源可能会导致死锁
// 设置资源级别
func (c *chopstick) setId(id int) {
c.id = id
}
func (p *philosopher) dineOrder() {
for {
p.status = tryEat
cs := p.getChopsticksOrderByID()
for _, c := range cs {
c.mu.Lock()
}
p.status = eat
randomPause(100)
for _, c := range cs {
c.mu.Unlock()
}
fmt.Printf("philosohper %s eaten \n", p.name)
p.status = think
}
}
func (p *philosopher) getChopsticksOrderByID() []*chopstick {
cs := make([]*chopstick, 2)
if p.leftChopstick.id >= p.rightChopstick.id {
cs[0] = p.leftChopstick
cs[1] = p.rightChopstick
} else {
cs[0] = p.rightChopstick
cs[1] = p.leftChopstick
}
return cs
}
分级方法的缺陷在于,很多时候我们无法预先知道所有需要的资源。并且这也并不公平,如果一个哲学家格外的慢的话,那么他可能永远无法吃上
Chandy-Misra算法
chandy-misra算法是Chandy和Misra提出来的允许任何代理去争夺任意数量资源的完全分布式的算法。不过违法了哲学家之间不能沟通的要求
- 当一个哲学家想要使用一组资源(即吃)时,他必须从竞争的邻居那里获得叉子。对于哲学家没有的所有这样的分叉,他们发送一个请求消息。
- 当拿着叉子的哲学家收到请求消息时,如果叉子是干净的,他们会保留它,但如果叉子脏了,他们就会放弃它。如果哲学家把叉子送过去,他们会先把叉子清洗干净。
- 哲学家吃完饭后,所有的叉子都变脏了。如果之前有其他哲学家要了一把叉子,刚刚吃完的哲学家就会把叉子洗干净并送出去。
这种解决方案还允许很大程度的并发,并将解决任意大的问题。
它还解决了饥饿问题。清洁/肮脏标签的作用是优先考虑最“饥饿”的进程,而不利于刚刚“吃”的进程。人们可以将他们的解决方案与哲学家不允许连续吃两次而不让其他人使用叉子的方法进行比较。Chandy和Misra的解决方案比这更灵活,但也有一个倾向于这个方向的元素。
在他们的分析中,他们从分叉的分布和它们的清洁/肮脏状态中得出了一个偏好等级系统。他们表明,这个系统可以描述一个有向无环图,如果是这样,他们的协议中的操作不能把这个图变成一个循环图。这保证了死锁不会发生。然而,如果系统初始化为一个完全对称的状态,就像所有哲学家拿着左边的叉子一样,那么图在一开始就是循环的,并且他们的解决方案不能防止死锁。初始化系统,使id较低的哲学家有脏叉,以确保图最初是无循环的。
下列代码是我尝试实现的,然而却无法确保哲学家就餐的时候一定持有所有需要的叉子。
type fork struct {
id int
ownerID int
dirty bool
mu *sync.Mutex
cond *sync.Cond
}
func (f *fork) request(ownerID int) {
for f.ownerID != ownerID {
if f.dirty {
f.mu.Lock()
if !f.dirty {
f.mu.Unlock()
continue
}
f.dirty = false
f.ownerID = ownerID
f.mu.Unlock()
} else {
f.cond.L.Lock()
for !f.dirty {
f.cond.Wait()
}
f.cond.L.Unlock()
}
}
}
func (f *fork) doneUsing() {
f.cond.L.Lock()
f.dirty = true
f.cond.L.Unlock()
f.cond.Broadcast()
}
type philo struct {
id int
name string
lf *fork
rf *fork
status philoStatus
}
func (p *philo) dine() {
for {
p.status = tryEat
p.lf.request(p.id)
fmt.Printf("philosohper %s pick left chopstick %d \n", p.name, p.lf.id)
p.rf.request(p.id)
fmt.Printf("philosohper %s pick right chopstick %d \n", p.name, p.rf.id)
p.status = eat
randomPause(100)
fmt.Printf("philosohper %s eaten \n", p.name)
p.lf.doneUsing()
p.rf.doneUsing()
p.status = think
}
}
type table struct {
forks []*fork
ps []*philo
}
func newTable(num int) *table {
forks := make([]*fork, num, num)
ps := make([]*philo, num, num)
for i := 0; i < num; i++ {
mu := &sync.Mutex{}
ownerID := i
if i == num-1 {
ownerID = 0
}
forks[i] = &fork{
id: i,
ownerID: ownerID,
dirty: true,
mu: &sync.Mutex{},
cond: sync.NewCond(mu),
}
}
for i := 0; i < num; i++ {
lfIndex := i - 1
if i == 0 {
lfIndex = num - 1
}
ps[i] = &philo{
id: i,
name: fmt.Sprintf("philosopher %d", i),
lf: forks[lfIndex],
rf: forks[i],
status: thinking,
}
}
return &table{
forks: forks,
ps: ps,
}
}
func (t *table) startDine() {
for _, p := range t.ps {
go p.dine()
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
fmt.Println("就餐完毕")
for _, p := range t.ps {
fmt.Printf("philosopher %s status %d", p.name, p.status)
}
}
Ref
- https://en.wikipedia.org/wiki/Dining_philosophers_problem