Golang实现哲学家就餐问题

Golang实现哲学家就餐问题 我正在完成加州大学尔湾分校Coursera Go语言课程中的哲学家就餐问题程序。

我遇到了一些死锁问题(尽管使用了互斥锁)。同步机制似乎存在某些问题。题目要求如下:

实现哲学家就餐问题,需遵循以下约束/修改条件:

  1. 应有5位哲学家共享筷子,每对相邻哲学家之间放置一根筷子
  2. 每位哲学家只应进餐3次(不同于课堂演示中的无限循环)
  3. 哲学家以任意顺序拿起筷子,而非按编号从小到大顺序(课堂演示中使用的方式)
  4. 哲学家开始进餐前,必须获得独立协程中运行的主持人的许可
  5. 主持人最多允许2位哲学家同时进餐
  6. 每位哲学家都有编号,从1到5
  7. 当哲学家开始进餐(获得必要锁之后),需单独打印一行"开始进餐 <编号>",其中<编号>是哲学家编号
  8. 当哲学家结束进餐(释放锁之前),需单独打印一行"结束进餐 <编号>",其中<编号>是哲学家编号

以下是我目前的代码:

package main

import (
	"fmt"
	"math/rand"

	"sync"
	"time"
)

var ch = make(chan int, 3)

type fork struct{ sync.Mutex }

type philosopher struct {
	id                  int
	leftFork, rightFork *fork
}

// Goes from thinking to hungry to eating and done eating then starts over.
// Adapt the pause values to increased or decrease contentions
// around the forks.
func (p philosopher) eat(n int) {

	say("thinking", p.id)
	randomPause(2)

	say("hungry", p.id)
	p.leftFork.Lock()
	p.rightFork.Lock()
	randomPause(5)

	say("eating", p.id)

	p.rightFork.Unlock()
	p.leftFork.Unlock()

	say("done eating", p.id)

	ch <- n

}

func randomPause(max int) {
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(max*1000)))
}

func say(action string, id int) {
	fmt.Printf("Philosopher #%d is %s\n", id+1, action)
}

func init() {
	// Random seed
	rand.Seed(time.Now().UTC().UnixNano())
}

func main() {
	// How many philosophers and forks
	count := 5

	// Create forks
	forks := make([]*fork, count)
	for i := 0; i < count; i++ {
		forks[i] = new(fork)
	}

	// Create philospoher, assign them 2 forks and send them to the dining table
	philosophers := make([]*philosopher, count)
	for i := 0; i < count; i++ {
		philosophers[i] = &philosopher{
			id: i, leftFork: forks[i], rightFork: forks[(i+1)%count]}
		for j := 0; j < 3; j++ {
			go philosophers[i].eat(j)
		}
	}

	philoEat := make(chan int, 3)
	<-philoEat

}

期待获得任何指导。谢谢!


更多关于Golang实现哲学家就餐问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

FistOfJustice:

os.Exit(0)

这个退出操作会在第一位哲学家吃完三次后立即执行。无论其他哲学家吃了多少次,程序都会直接退出。

os.Exit(0)

更多关于Golang实现哲学家就餐问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


必须将 Done 放在 go 协程内部,否则它会在协程启动后立即被调用。因此 waitgroup 必须是全局变量、作为参数传递,或者使用匿名函数先调用 eat() 再调用 Done()

package main

import (
	"fmt"
	"sync"
	"time"
)

type fork struct{ sync.Mutex }

type philosopher struct {
	id                  int
	leftFork, rightFork *fork
}

// 从思考到饥饿到进食,完成进食后重新开始
// 调整暂停时间值来增加或减少对叉子的争用
func (p philosopher) eat() {
	for j := 0; j < 3; j++ {
		p.leftFork.Lock()
		p.rightFork.Lock()

		say("eating", p.id)
		time.Sleep(time.Second)

		p.rightFork.Unlock()
		p.leftFork.Unlock()

		say("finished eating", p.id)
		time.Sleep(time.Second)
	}
	eatWgroup.Done()
}

func say(action string, id int) {
	fmt.Printf("Philosopher #%d is %s\n", id+1, action)
}

var eatWgroup sync.WaitGroup

func main() {
	// 哲学家和叉子的数量

	count := 5

	// 创建叉子
	forks := make([]*fork, count)
	for i := 0; i < count; i++ {
		forks[i] = new(fork)
	}

	// 创建哲学家,分配两个叉子并送到餐桌
	philosophers := make([]*philosopher, count)
	for i := 0; i < count; i++ {
		philosophers[i] = &philosopher{
			id: i, leftFork: forks[i], rightFork: forks[(i+1)%count]}
		eatWgroup.Add(1)
		go philosophers[i].eat()

	}
	eatWgroup.Wait()

}

你的版本无法正常工作的原因是,go 协程会一直运行直到遇到阻塞调用,比如等待磁盘、网络等类似操作。因此在紧密循环中运行 go 协程不会给其他协程任何时间,除非你有多于一个核心让它们可以并行运行。我将 waitgroup 移到了全局变量,这样在函数内部更容易访问,并加入了一些睡眠时间(因为进食和思考都需要时间),这样其他 go 协程就有机会运行。

更多阅读请参考 Medium

不要打断!

go 运行时调度器采用协作式调度,这意味着只有当当前协程阻塞或完成时,才会调度另一个协程。这些情况包括:

  • 通道发送和接收操作(如果这些操作会阻塞)
  • Go 语句(虽然不能保证新协程会立即被调度)
  • 阻塞性系统调用(如文件和网络操作)
  • 垃圾回收周期结束后被停止

感谢您的跟进。当我移除 os.Exit(0) 后,程序在协程运行后出现死锁,请看:

$ go run diningphilosophers.go
Philosopher #1 is eating
Philosopher #1 is finished eating
Philosopher #3 is eating
Philosopher #3 is finished eating
Philosopher #5 is eating
Philosopher #5 is finished eating
Philosopher #2 is eating
Philosopher #2 is finished eating
Philosopher #4 is eating
Philosopher #4 is finished eating
Philosopher #1 is eating
Philosopher #1 is finished eating
Philosopher #3 is eating
Philosopher #3 is finished eating
Philosopher #5 is eating
Philosopher #5 is finished eating
Philosopher #2 is eating
Philosopher #2 is finished eating
Philosopher #4 is eating
Philosopher #4 is finished eating
Philosopher #1 is eating
Philosopher #1 is finished eating
Philosopher #3 is eating
Philosopher #3 is finished eating
Philosopher #5 is eating
Philosopher #5 is finished eating
Philosopher #2 is eating
Philosopher #2 is finished eating
Philosopher #4 is eating
Philosopher #4 is finished eating
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        C:/Users/iokhamaf/Documents/Coursera/Course 3/Module 4/diningphilosophers.go:85 +0x2d4
exit status 2 

以下是修改后的代码(新代码),仍然有2位哲学家无法同时进餐:

package main

import (
	"fmt"

	"sync"
)

type fork struct{ sync.Mutex }

type philosopher struct {
	id                  int
	leftFork, rightFork *fork
}

// Goes from thinking to hungry to eating and done eating then starts over.
// Adapt the pause values to increased or decrease contentions
// around the forks.
func (p philosopher) eat() {
	for j := 0; j < 3; j++ {
		p.leftFork.Lock()
		p.rightFork.Lock()

		say("eating", p.id)

		p.rightFork.Unlock()
		p.leftFork.Unlock()

		say("finished eating", p.id)
	}

}

func say(action string, id int) {
	fmt.Printf("Philosopher #%d is %s\n", id+1, action)
}

func main() {
	// How many philosophers and forks
	var eatWgroup sync.WaitGroup
	count := 5

	// Create forks
	forks := make([]*fork, count)
	for i := 0; i < count; i++ {
		forks[i] = new(fork)
	}

	// Create philospoher, assign them 2 forks and send them to the dining table
	philosophers := make([]*philosopher, count)
	for i := 0; i < count; i++ {
		philosophers[i] = &philosopher{
			id: i, leftFork: forks[i], rightFork: forks[(i+1)%count]}
		eatWgroup.Add(1)
		go philosophers[i].eat()
		eatWgroup.Done()

	}
	eatWgroup.Wait()
	philoEat := make(chan int)
	<-philoEat

}

更新:

我对代码进行了一些修改。但是,它并没有并发运行。看起来每次只运行一个goroutine或线程。在这个问题中,通常可以有两个哲学家同时进食,而其他哲学家则在等待进食。以下是修改后的代码:

package main

import (
	"fmt"
	"math/rand"
	"os"

	"sync"
	"time"
)

type fork struct{ sync.Mutex }

type philosopher struct {
	id                  int
	leftFork, rightFork *fork
}

// 从思考到饥饿,再到进食和完成进食,然后重新开始。
// 调整暂停值以增加或减少对叉子的争用
func (p philosopher) eat() {
	for j := 0; j < 3; j++ {
		p.leftFork.Lock()
		p.rightFork.Lock()
		randomPause(5)

		say("eating", p.id)

		p.rightFork.Unlock()
		p.leftFork.Unlock()

		say("finished eating", p.id)
	}

	os.Exit(0)

}

func randomPause(max int) {
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(max*1000)))
}

func say(action string, id int) {
	fmt.Printf("Philosopher #%d is %s\n", id+1, action)
}

func init() {
	// 随机种子
	rand.Seed(time.Now().UTC().UnixNano())
}

func main() {
	// 哲学家和叉子的数量
	count := 5

	// 创建叉子
	forks := make([]*fork, count)
	for i := 0; i < count; i++ {
		forks[i] = new(fork)
	}

	// 创建哲学家,分配两把叉子并送他们到餐桌
	philosophers := make([]*philosopher, count)
	for i := 0; i < count; i++ {
		philosophers[i] = &philosopher{
			id: i, leftFork: forks[i], rightFork: forks[(i+1)%count]}

		go philosophers[i].eat()

	}

	philoEat := make(chan int)
	<-philoEat

}

以下是输出:

        $ go run diningphilosophers.go
        Philosopher #5 is eating
        Philosopher #5 is finished eating
        Philosopher #4 is eating
        Philosopher #4 is finished eating
        Philosopher #2 is eating
        Philosopher #2 is finished eating
        Philosopher #1 is eating
        Philosopher #1 is finished eating
        Philosopher #5 is eating
        Philosopher #5 is finished eating
        Philosopher #4 is eating
        Philosopher #4 is finished eating
        Philosopher #3 is eating
        Philosopher #3 is finished eating
        Philosopher #2 is eating
        Philosopher #2 is finished eating
        Philosopher #1 is eating
        Philosopher #1 is finished eating
        Philosopher #5 is eating
        Philosopher #5 is finished eating


    $ go run diningphilosophers.go
    Philosopher #3 is eating
    Philosopher #3 is finished eating
    Philosopher #1 is eating
    Philosopher #1 is finished eating
    Philosopher #5 is eating
    Philosopher #5 is finished eating
    Philosopher #4 is eating
    Philosopher #4 is finished eating
    Philosopher #3 is eating
    Philosopher #3 is finished eating
    Philosopher #2 is eating
    Philosopher #2 is finished eating
    Philosopher #1 is eating
    Philosopher #1 is finished eating
    Philosopher #5 is eating
    Philosopher #5 is finished eating
    Philosopher #4 is eating
    Philosopher #4 is finished eating
    Philosopher #3 is eating
    Philosopher #3 is finished eating

这显然没有正常工作,因为并非所有哲学家都进食了3次。

您的代码存在几个关键问题,导致死锁和未满足题目要求。以下是修复后的完整实现:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

type fork struct{ sync.Mutex }

type philosopher struct {
	id                  int
	leftFork, rightFork *fork
	eatCount            int
}

type host struct {
	permission chan struct{}
}

func (h *host) grantPermission() {
	h.permission <- struct{}{}
}

func (h *host) releasePermission() {
	<-h.permission
}

func newHost(maxConcurrent int) *host {
	h := &host{
		permission: make(chan struct{}, maxConcurrent),
	}
	// 预先填充许可
	for i := 0; i < maxConcurrent; i++ {
		h.permission <- struct{}{}
	}
	return h
}

func (p *philosopher) eat(wg *sync.WaitGroup, host *host) {
	defer wg.Done()
	
	for p.eatCount < 3 {
		// 思考
		randomPause(2)
		
		// 从主持人获取许可
		host.grantPermission()
		
		// 随机顺序拿起筷子
		first, second := p.getForkOrder()
		
		first.Lock()
		second.Lock()
		
		// 开始进餐
		fmt.Printf("开始进餐 %d\n", p.id+1)
		randomPause(3)
		
		// 结束进餐
		fmt.Printf("结束进餐 %d\n", p.id+1)
		
		second.Unlock()
		first.Unlock()
		
		// 释放主持人许可
		host.releasePermission()
		
		p.eatCount++
	}
}

func (p *philosopher) getForkOrder() (*fork, *fork) {
	if rand.Intn(2) == 0 {
		return p.leftFork, p.rightFork
	}
	return p.rightFork, p.leftFork
}

func randomPause(max int) {
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(max*1000)))
}

func main() {
	rand.Seed(time.Now().UnixNano())
	
	count := 5
	forks := make([]*fork, count)
	for i := 0; i < count; i++ {
		forks[i] = new(fork)
	}

	// 创建主持人,最多允许2位哲学家同时进餐
	host := newHost(2)

	philosophers := make([]*philosopher, count)
	for i := 0; i < count; i++ {
		philosophers[i] = &philosopher{
			id:        i,
			leftFork:  forks[i],
			rightFork: forks[(i+1)%count],
			eatCount:  0,
		}
	}

	var wg sync.WaitGroup
	for i := 0; i < count; i++ {
		wg.Add(1)
		go philosophers[i].eat(&wg, host)
	}

	wg.Wait()
	fmt.Println("所有哲学家已完成进餐")
}

主要修复和改进:

  1. 主持人机制:实现了host结构体,使用带缓冲的通道控制最多2位哲学家同时进餐
  2. 随机筷子顺序getForkOrder()方法随机选择先拿哪边的筷子
  3. 进餐次数限制:每个哲学家通过eatCount计数器确保只进餐3次
  4. 正确的同步:使用sync.WaitGroup等待所有哲学家完成,而不是未使用的通道
  5. 输出格式:严格按照题目要求输出"开始进餐 <编号>“和"结束进餐 <编号>”
  6. 死锁预防:通过主持人限制并发数,结合随机筷子顺序,有效预防死锁

这个实现满足了所有题目要求:5位哲学家、每人进餐3次、随机筷子顺序、主持人许可机制、最多2人同时进餐,以及正确的输出格式。

回到顶部