英文:
Golang implementation of dining philosophers variant problem
问题
我想实现一个变种的经典餐厅哲学家问题,其定义如下:
使用以下约束/修改实现餐厅哲学家问题。
- 有5个哲学家共享筷子,每个相邻的哲学家之间有一根筷子。
- 每个哲学家只能吃3次(不像我们在讲座中那样无限循环)。
- 哲学家可以以任意顺序拿起筷子(不像我们在讲座中那样按最低编号优先)。
- 为了吃饭,哲学家必须得到一个在自己的goroutine中执行的主持人的许可。
- 主持人最多允许2个哲学家同时就餐。
- 每个哲学家都有一个编号,从1到5。
- 当一个哲学家开始吃饭(在获得必要的锁之后)时,他会单独打印一行“starting to eat”,其中是哲学家的编号。
- 当一个哲学家吃完饭(在释放锁之前)时,他会单独打印一行“finishing eating”,其中是哲学家的编号。
我实现了以下代码:
package main
import (
"fmt"
"sync"
)
// 定义变量
var numPhilo int = 5
var numCS int = 5
var eatTimes int = 3
var numEatingPhilo int = 2
type Host struct {
// 允许哲学家就餐的通道
requestChannel chan *Philo
// 守护进程终止信号的通道
quitChannel chan int
// 当前就餐的哲学家的记录
eatingPhilos map[int]bool
// 修改eatingPhilos变量的互斥锁
mu sync.Mutex
}
// 管理允许就餐的哲学家的守护进程函数
func (pHost *Host) manage() {
// 守护进程在后台运行,只有在接收到终止信号时才退出
for {
// 如果通道已满,则释放队列头部的哲学家
if len(pHost.requestChannel) == numEatingPhilo {
finished := <-pHost.requestChannel
currEating := make([]int, 0, numPhilo)
for tmpIdx, eating := range pHost.eatingPhilos {
if eating {
currEating = append(currEating, tmpIdx)
}
}
fmt.Printf("%v 正在就餐,清理 %d\n", currEating, finished.idx)
pHost.eatingPhilos[finished.idx] = false
}
select {
case <-pHost.quitChannel:
fmt.Println("停止主持")
return
default:
}
}
}
type ChopS struct {
mu sync.Mutex
}
type Philo struct {
// 哲学家的索引
idx int
// 哲学家就餐的次数
numEat int
leftCS, rightCS *ChopS
host *Host
}
func (pPhilo *Philo) eat(wg *sync.WaitGroup) {
for pPhilo.numEat < eatTimes {
// 当哲学家打算就餐时,锁定相应的筷子
pPhilo.leftCS.mu.Lock()
pPhilo.rightCS.mu.Lock()
// 在通道中预留一个位置用于就餐
// 如果通道缓冲区已满,将被阻塞,直到通道空间被释放
pPhilo.host.requestChannel <- pPhilo
pPhilo.host.mu.Lock()
pPhilo.host.eatingPhilos[pPhilo.idx] = true
pPhilo.host.mu.Unlock()
pPhilo.numEat++
fmt.Printf("开始就餐 %d,第 %d 次\n", pPhilo.idx, pPhilo.numEat)
fmt.Printf("结束就餐 %d,第 %d 次\n", pPhilo.idx, pPhilo.numEat)
pPhilo.rightCS.mu.Unlock()
pPhilo.leftCS.mu.Unlock()
wg.Done()
}
}
func main() {
var wg sync.WaitGroup
requestChannel := make(chan *Philo, numEatingPhilo)
quitChannel := make(chan int, 1)
host := Host{requestChannel: requestChannel, quitChannel: quitChannel, eatingPhilos: make(map[int]bool)}
CSticks := make([]*ChopS, numCS)
for i := 0; i < numCS; i++ {
CSticks[i] = new(ChopS)
}
philos := make([]*Philo, numPhilo)
for i := 0; i < numPhilo; i++ {
philos[i] = &Philo{idx: i + 1, numEat: 0, leftCS: CSticks[i], rightCS: CSticks[(i+1)%5], host: &host}
}
go host.manage()
wg.Add(numPhilo * eatTimes)
for i := 0; i < numPhilo; i++ {
go philos[i].eat(&wg)
}
wg.Wait()
host.quitChannel <- 1
}
然而,我注意到程序在某些情况下实际上会失败,例如:
开始就餐 5,第 1 次
结束就餐 5,第 1 次
开始就餐 2,第 1 次
结束就餐 2,第 1 次
[5 2] 正在就餐,清理 5
[2] 正在就餐,清理 2
[] 正在就餐,清理 5
开始就餐 5,第 2 次
结束就餐 5,第 2 次
开始就餐 5,第 3 次
结束就餐 5,第 3 次
[5 2] 正在就餐,清理 2
[5] 正在就餐,清理 5
开始就餐 2,第 2 次
结束就餐 2,第 2 次
[4] 正在就餐,清理 4
开始就餐 4,第 1 次
结束就餐 4,第 1 次
[] 正在就餐,清理 2
开始就餐 4,第 2 次
结束就餐 4,第 2 次
[4] 正在就餐,清理 4
开始就餐 4,第 3 次
结束就餐 4,第 3 次
开始就餐 2,第 3 次
结束就餐 2,第 3 次
开始就餐 1,第 1 次
结束就餐 1,第 1 次
[2 4 1] 正在就餐,清理 4
[2 1] 正在就餐,清理 1
[2] 正在就餐,清理 3
开始就餐 1,第 2 次
结束就餐 1,第 2 次
[1 2] 正在就餐,清理 1
开始就餐 3,第 1 次
结束就餐 3,第 1 次
开始就餐 3,第 2 次
结束就餐 3,第 2 次
[3 2] 正在就餐,清理 1
[2 3] 正在就餐,清理 3
开始就餐 3,第 3 次
结束就餐 3,第 3 次
开始就餐 1,第 3 次
结束就餐 1,第 3 次
停止主持
有时候似乎哲学家无法同时就餐,并且根据日志,记录哲学家的映射表行为异常。
请问有人能指出实现的哪个部分不正确吗?有什么明显的错误或不良实践吗?
英文:
I would like to implement a variant of the classical dining philosophers problem which has the definition as:
Implement the dining philosopher’s problem with the following constraints/modifications.
- There should be 5 philosophers sharing chopsticks, with one chopstick between each adjacent pair of philosophers.
- Each philosopher should eat only 3 times (not in an infinite loop as we did in lecture)
- The philosophers pick up the chopsticks in any order, not lowest-numbered first (which we did in lecture).
- In order to eat, a philosopher must get permission from a host which executes in its own goroutine.
- The host allows no more than 2 philosophers to eat concurrently.
- Each philosopher is numbered, 1 through 5.
- When a philosopher starts eating (after it has obtained necessary locks) it prints “starting to eat ” on a line by itself, where is the number of the philosopher.
- When a philosopher finishes eating (before it has released its locks) it prints “finishing eating ” on a line by itself, where is the number of the philosopher.
I implemented the following code:
package main
import (
"fmt"
"sync"
)
// define variables
var numPhilo int = 5
var numCS int = 5
var eatTimes int = 3
var numEatingPhilo int = 2
type Host struct {
// channel for allowed philosopher for eating
requestChannel chan *Philo
// channel for terminate signal for the daemon
quitChannel chan int
// bookkeeping of the current eating philosophers
eatingPhilos map[int]bool
// mutex to lock the modification of the eatingPhilos variable
mu sync.Mutex
}
// daemon function to manage the allowed philosophers
func (pHost *Host) manage() {
// daemon serving in the backend and only exits for terminate signal
for {
// if the channel is full, release the head of the queue
if len(pHost.requestChannel) == numEatingPhilo {
finished := <-pHost.requestChannel
currEating := make([]int, 0, numPhilo)
for tmpIdx, eating := range pHost.eatingPhilos {
if eating {
currEating = append(currEating, tmpIdx)
}
}
fmt.Printf("%v have been eating, clearing up %d\n", currEating, finished.idx)
pHost.eatingPhilos[finished.idx] = false
}
select {
case <-pHost.quitChannel:
fmt.Println("stop hosting")
return
default:
}
}
}
type ChopS struct {
mu sync.Mutex
}
type Philo struct {
// index of the philosopher
idx int
// number of times the philosopher has eaten
numEat int
leftCS, rightCS *ChopS
host *Host
}
func (pPhilo *Philo) eat(wg *sync.WaitGroup) {
for pPhilo.numEat < eatTimes {
// once the philosopher intends to eat, lock the corresponding chopsticks
pPhilo.leftCS.mu.Lock()
pPhilo.rightCS.mu.Lock()
// reserve a slot in the channel for eating
// if channel buffer is full, this is blocked until channel space is released
pPhilo.host.requestChannel <- pPhilo
pPhilo.host.mu.Lock()
pPhilo.host.eatingPhilos[pPhilo.idx] = true
pPhilo.host.mu.Unlock()
pPhilo.numEat++
fmt.Printf("starting to eat %d for %d time\n", pPhilo.idx, pPhilo.numEat)
fmt.Printf("finishing eating %d for %d time\n", pPhilo.idx, pPhilo.numEat)
pPhilo.rightCS.mu.Unlock()
pPhilo.leftCS.mu.Unlock()
wg.Done()
}
}
func main() {
var wg sync.WaitGroup
requestChannel := make(chan *Philo, numEatingPhilo)
quitChannel := make(chan int, 1)
host := Host{requestChannel: requestChannel, quitChannel: quitChannel, eatingPhilos: make(map[int]bool)}
CSticks := make([]*ChopS, numCS)
for i := 0; i < numCS; i++ {
CSticks[i] = new(ChopS)
}
philos := make([]*Philo, numPhilo)
for i := 0; i < numPhilo; i++ {
philos[i] = &Philo{idx: i + 1, numEat: 0, leftCS: CSticks[i], rightCS: CSticks[(i+1)%5], host: &host}
}
go host.manage()
wg.Add(numPhilo * eatTimes)
for i := 0; i < numPhilo; i++ {
go philos[i].eat(&wg)
}
wg.Wait()
host.quitChannel <- 1
}
However, I noticed that the program is actually failing in some cases, i.e.
starting to eat 5 for 1 time
finishing eating 5 for 1 time
starting to eat 2 for 1 time
finishing eating 2 for 1 time
[5 2] have been eating, clearing up 5
[2] have been eating, clearing up 2
[] have been eating, clearing up 5
starting to eat 5 for 2 time
finishing eating 5 for 2 time
starting to eat 5 for 3 time
finishing eating 5 for 3 time
[5 2] have been eating, clearing up 2
[5] have been eating, clearing up 5
starting to eat 2 for 2 time
finishing eating 2 for 2 time
[4] have been eating, clearing up 4
starting to eat 4 for 1 time
finishing eating 4 for 1 time
[] have been eating, clearing up 2
starting to eat 4 for 2 time
finishing eating 4 for 2 time
[4] have been eating, clearing up 4
starting to eat 4 for 3 time
finishing eating 4 for 3 time
starting to eat 2 for 3 time
finishing eating 2 for 3 time
starting to eat 1 for 1 time
finishing eating 1 for 1 time
[2 4 1] have been eating, clearing up 4
[2 1] have been eating, clearing up 1
[2] have been eating, clearing up 3
starting to eat 1 for 2 time
finishing eating 1 for 2 time
[1 2] have been eating, clearing up 1
starting to eat 3 for 1 time
finishing eating 3 for 1 time
starting to eat 3 for 2 time
finishing eating 3 for 2 time
[3 2] have been eating, clearing up 1
[2 3] have been eating, clearing up 3
starting to eat 3 for 3 time
finishing eating 3 for 3 time
starting to eat 1 for 3 time
finishing eating 1 for 3 time
stop hosting
where sometimes it seems the philosophers cannot eat concurrently, and also according to the logs, the bookkeeping map is acting weird.
Could someone please point out which part of the implementation is improper? Anything obviously wrong or bad practice?
答案1
得分: 1
你在这里发布的代码存在数据竞争问题(Host.eatingPhilos
在没有任何保护的情况下从多个Go协程中访问)。在在code review上发布问题之前,你解决了这些问题,这在某种程度上解决了问题(但引入了其他问题)。
我在code review上对此进行了详细回答,并提供了一些建议等。由于你在那里发布的代码与这里的代码不同,所以重复回答没有太大意义。然而,我将包含我的建议解决方案,因为它采用了与你采用的方法显著不同的方法(为了解决一些逻辑问题)。请注意,这个解决方案采用了一个简单的方法来解决“饥饿哲学家”问题(每个哲学家只有一根筷子,所以没有人可以拿到第二根)。参见Wikipedia页面了解其他更好的解决方案(但我想你想使用Go协程)。
警告 - 下面的代码可能包含错误!
package main
import (
"fmt"
"math/rand"
"sync"
"time"
"golang.org/x/exp/slices"
)
const (
numPhilo = 5
eatTimes = 3
numEatingPhilo = 2
)
type eatRequest struct {
who int // 请求者的编号
finishedFnChan chan func() // 当批准请求时,将在此通道上发送一个带有完成时调用的函数
}
// simulateHost - 主持人必须在哲学家吃饭之前提供许可
// 当通道关闭时退出
func simulateHost(requestChannel <-chan eatRequest) {
awaitRequest := requestChannel
finishedChan := make(chan struct {
who int
done chan struct{}
})
var whoEating []int // 跟踪当前正在吃饭的人
for {
select {
case request, ok := <-awaitRequest:
if !ok {
return // 通道关闭意味着我们完成了(finishedChan保证为空)
}
// 检查一下 - 确认哲学家不贪心!(永远不应该发生)
if slices.Index(whoEating, request.who) != -1 {
panic("同一个哲学家发出多个请求")
}
whoEating = append(whoEating, request.who) // 新请求总是放在最后
fmt.Printf("%d 开始吃饭(当前正在吃饭的人:%v)\n", request.who, whoEating)
// 告诉哲学家,并提供他们在完成后告诉我们的方法
request.finishedFnChan <- func() {
d := make(chan struct{})
finishedChan <- struct {
who int
done chan struct{}
}{who: request.who, done: d}
<-d // 等待请求被处理(确保我们永远不会有两个来自同一个哲学家的活动请求)
}
case fin := <-finishedChan:
idx := slices.Index(whoEating, fin.who)
if idx == -1 {
panic("哲学家多次停止吃饭!")
}
whoEating = append(whoEating[:idx], whoEating[idx+1:]...) // 删除元素
fmt.Printf("%d 完成吃饭(当前正在吃饭的人:%v)\n", fin.who, whoEating)
close(fin.done)
}
// 哲学家吃饭的人数发生了变化
if len(whoEating) < numEatingPhilo {
awaitRequest = requestChannel
} else {
awaitRequest = nil // 忽略新的吃饭请求,直到有哲学家吃完(nil通道永远不会被选中)
}
}
}
// ChopS 表示一根筷子
type ChopS struct {
mu sync.Mutex
idx int // 包括索引可以使调试更简单
}
// philosopher 模拟一个哲学家(大脑在一个容器中!)
func philosopher(philNum int, leftCS, rightCS *ChopS, requestToEat chan<- eatRequest) {
for numEat := 0; numEat < eatTimes; numEat++ {
// 一旦哲学家打算吃饭,锁定相应的筷子
for {
leftCS.mu.Lock()
// 尝试获取右边的筷子 - 如果有人拿着,我们替换左边的筷子并重试
// (以避免死锁)
if rightCS.mu.TryLock() {
break
}
leftCS.mu.Unlock()
}
// 我们有筷子,但需要主持人的许可
ffc := make(chan func()) // 当被接受时,我们将收到一个完成吃饭时调用的函数
requestToEat <- eatRequest{
who: philNum,
finishedFnChan: ffc,
}
doneEating := <-ffc
fmt.Printf("哲学家 %d 开始吃饭(第 %d 次)\n", philNum, numEat)
time.Sleep(time.Millisecond * time.Duration(rand.Intn(200))) // 吃饭需要随机的时间
fmt.Printf("哲学家 %d 完成吃饭(第 %d 次)\n", philNum, numEat)
rightCS.mu.Unlock()
leftCS.mu.Unlock()
doneEating() // 告诉主持人我们吃完了
}
fmt.Printf("哲学家 %d 已经吃饱了\n", philNum)
}
func main() {
CSticks := make([]*ChopS, numPhilo)
for i := 0; i < numPhilo; i++ {
CSticks[i] = &ChopS{idx: i}
}
requestChannel := make(chan eatRequest)
var wg sync.WaitGroup
wg.Add(numPhilo)
for i := 0; i < numPhilo; i++ {
go func(philNo int) {
philosopher(philNo, CSticks[philNo-1], CSticks[philNo%numPhilo], requestChannel)
wg.Done()
}(i + 1)
}
go func() {
wg.Wait()
close(requestChannel)
}()
simulateHost(requestChannel)
}
英文:
The code you posted here contains data races (Host.eatingPhilos
is accessed from multiple go-routines without any protection). You resolved these before posting your question on code review which went some way towards a working solution (but introduced other issues).
I answered this fully on code review and provided some feedback etc. As the code you posted there differs from what is here there is little point in duplicating the answer. However, I will include my suggested solution because it adopts a significantly different approach to that you took (in order to work around a few logic issues). Note that this solution takes an easy way out of the "starving philosopher" issue (each philosopher has one chopstick, so none can get a second). See the Wikipedia page for some other, better, solutions (but I figure you wanted to use go routines).
warning - the below may well contain bugs!
package main
import (
"fmt"
"math/rand"
"sync"
"time"
"golang.org/x/exp/slices"
)
const (
numPhilo = 5
eatTimes = 3
numEatingPhilo = 2
)
type eatRequest struct {
who int // Who is making the request
finishedFnChan chan func() // When approves a response will be sent on this channel with a function to call when done
}
// simulateHost - the host must provide permission before a philosopher can eat
// Exits when channel closed
func simulateHost(requestChannel <-chan eatRequest) {
awaitRequest := requestChannel
finishedChan := make(chan struct {
who int
done chan struct{}
})
var whoEating []int // tracks who is currently eating
for {
select {
case request, ok := <-awaitRequest:
if !ok {
return // Closed channel means that we are done (finishedChan is guaranteed to be empty)
}
// Sanity check - confirm that philosopher is not being greedy! (should never happen)
if slices.Index(whoEating, request.who) != -1 {
panic("Multiple requests from same philosopher")
}
whoEating = append(whoEating, request.who) // New request always goes at the end
fmt.Printf("%d started eating (currently eating %v)\n", request.who, whoEating)
// Let philosopher know and provide means for them to tell us when done
request.finishedFnChan <- func() {
d := make(chan struct{})
finishedChan <- struct {
who int
done chan struct{}
}{who: request.who, done: d}
<-d // Wait until request has been processed (ensure we should never have two active requests from one philosopher)
}
case fin := <-finishedChan:
idx := slices.Index(whoEating, fin.who)
if idx == -1 {
panic("philosopher stopped eating multiple times!")
}
whoEating = append(whoEating[:idx], whoEating[idx+1:]...) // delete the element
fmt.Printf("%d completed eating (currently eating %v)\n", fin.who, whoEating)
close(fin.done)
}
// There has been a change in the number of philosopher's eating
if len(whoEating) < numEatingPhilo {
awaitRequest = requestChannel
} else {
awaitRequest = nil // Ignore new eat requests until a philosopher finishes (nil channel will never be selected)
}
}
}
// ChopS represents a single chopstick
type ChopS struct {
mu sync.Mutex
idx int // Including the index can make debugging simpler
}
// philosopher simulates a Philosopher (brain in a vat!)
func philosopher(philNum int, leftCS, rightCS *ChopS, requestToEat chan<- eatRequest) {
for numEat := 0; numEat < eatTimes; numEat++ {
// once the philosopher intends to eat, lock the corresponding chopsticks
for {
leftCS.mu.Lock()
// Attempt to get the right Chopstick - if someone else has it we replace the left chopstick and try
// again (in order to avoid deadlocks)
if rightCS.mu.TryLock() {
break
}
leftCS.mu.Unlock()
}
// We have the chopsticks but need the hosts permission
ffc := make(chan func()) // when accepted we will receive a function to call when done eating
requestToEat <- eatRequest{
who: philNum,
finishedFnChan: ffc,
}
doneEating := <-ffc
fmt.Printf("philosopher %d starting to eat (%d feed)\n", philNum, numEat)
time.Sleep(time.Millisecond * time.Duration(rand.Intn(200))) // Eating takes a random amount of time
fmt.Printf("philosopher %d finished eating (%d feed)\n", philNum, numEat)
rightCS.mu.Unlock()
leftCS.mu.Unlock()
doneEating() // Tell host that we have finished eating
}
fmt.Printf("philosopher %d is full\n", philNum)
}
func main() {
CSticks := make([]*ChopS, numPhilo)
for i := 0; i < numPhilo; i++ {
CSticks[i] = &ChopS{idx: i}
}
requestChannel := make(chan eatRequest)
var wg sync.WaitGroup
wg.Add(numPhilo)
for i := 0; i < numPhilo; i++ {
go func(philNo int) {
philosopher(philNo, CSticks[philNo-1], CSticks[philNo%numPhilo], requestChannel)
wg.Done()
}(i + 1)
}
go func() {
wg.Wait()
close(requestChannel)
}()
simulateHost(requestChannel)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论