Golang中为什么会间歇性出现死锁错误?
Golang中为什么会间歇性出现死锁错误?
我正在尝试通过哲学家就餐问题来学习并发编程,但遇到了一个fatal error: all goroutines are asleep - deadlock!错误
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Chops struct{ sync.Mutex }
type Philo struct {
leftChopStick, rightChopStick *Chops
count int
}
var wq sync.WaitGroup
func (p Philo) eat(num int) {
for i := 0; i < 3; i++ {
if p.count > 3 {
continue
} else {
p.leftChopStick.Lock()
p.rightChopStick.Lock()
p.count++
fmt.Printf("starting to eat %d, %d time(s)\n", num+1, p.count)
p.leftChopStick.Unlock()
p.rightChopStick.Unlock()
fmt.Printf("finishing eating %d\n", num+1)
}
}
wq.Done()
}
func main() {
ChopSticks := make([]*Chops, 5)
for i := 0; i < 5; i++ {
ChopSticks[i] = new(Chops)
}
philos := make([]*Philo, 5)
rand.Seed(time.Now().UnixNano())
for i := 0; i < 5; i++ {
var randElem1 int
var randElem2 int
for {
if randElem1 != randElem2 {
break
} else {
randElem1 = rand.Intn(len(ChopSticks) - 1)
randElem2 = rand.Intn(len(ChopSticks) - 1)
}
}
philos[i] = &Philo{ChopSticks[randElem1], ChopSticks[randElem2], 0}
}
for i := 0; i < 5; i++ {
wq.Add(1)
go philos[i].eat(i)
}
wq.Wait()
}
更多关于Golang中为什么会间歇性出现死锁错误?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
是的,基本上我不是每次都会收到死锁错误
更多关于Golang中为什么会间歇性出现死锁错误?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我在Go Playground上测试过,没有任何问题。
这是测试分享链接:
https://play.golang.org/p/OyEKA55xhe9
meibou:
非常感谢 🙏 @hollowaykeanho 我会仔细研究并自己尝试的。
不客气。当你得到答案后,可以随时"标记为已解决"。
哦哇 😄 非常感谢 🙏 @hollowaykeanho 我会认真学习并亲自尝试。
@christophe_meessen 谢谢 🙏 我也会尝试原子操作
husonet 说:
我在 Go Playground 上试过,没有任何问题。
在我的本地机器上测试过。崩溃偶尔会发生。@meibou,你规划过你的 goroutine 吗?如果有,能分享一下规划方案吗?
编辑:或者你能分享一下哲学家就餐问题吗?
抱歉,我是Go语言新手,你所说的规划goroutines是什么意思?
问题是这样的:
- 应该有5位哲学家共享筷子,每对相邻的哲学家之间有一根筷子
- 每位哲学家应该只进食3次
- 哲学家以任意顺序拿起筷子,而非按编号从小到大顺序
为了进食,哲学家必须获得主机的许可,主机在自身的goroutine中执行 主机最多只允许2位哲学家同时进食
- 每位哲学家都有编号,从1到5
- 当哲学家开始进食时(在获得必要的锁之后),需要单独在一行打印"开始进食",其中是该哲学家的编号
- 当哲学家结束进食时(在释放锁之前),需要单独在一行打印"结束进食",其中是该哲学家的编号
首先需要修改代码中的筷子随机分配方式。哲学家们围坐成一个圆圈,每两人之间放着一根筷子。
理想情况下我们应该同时锁定两根筷子,但也可以按顺序进行,只要在无法锁定第二根筷子时释放第一根即可。这种情况下,锁定顺序无关紧要,不会导致死锁。
你可以按顺序为哲学家分配筷子,然后随机交换每个哲学家的左右筷子顺序,确保他们不会以相同顺序拿取筷子。
我们需要一个函数来尝试锁定两根筷子。这可以通过使用原子操作 CompareAndSwapUint32 实现。每根筷子都是一个初始化为0的Uint32,表示未被使用。
然后调用 atomic.CompareAndSwapUint32(&rightChopstick, 0, 1) 返回布尔值。如果 rightChopstick 为0(未被使用),将返回true并设置为1。如果成功,再对 leftChopstick 执行相同操作。如果两根筷子都能被锁定,则通过 atomic.StoreUint32(&rightChopstick,0) 和 atomic.StoreUint32(&leftChopstick,0) 释放,表示哲学家已完成用餐。
如果第一根筷子无法锁定,哲学家应该重新思考随机时间后再重试。如果第二根筷子无法锁定,必须释放第一根筷子,哲学家也需要重新思考。
这就是我用于解决哲学家就餐问题的同步算法总体思路。只要我们使用原子操作实现尝试锁定,左右顺序就无关紧要。
atomic.CompareAndSwapUint32(&rightChopstick, 0, 1)
atomic.StoreUint32(&rightChopstick,0)
atomic.StoreUint32(&leftChopstick,0)
哲学家就餐问题中的哲学家应该围坐成一个圆圈。参见[https://medium.com/@maheshkariya/operating-system-dining-philosopher-problem-using-semaphores-4caf0b22516f](https://medium.com/@maheshkariya/operating-system-dining-philosopher-problem-using-semaphores-4caf0b22516f)。
这意味着哲学家i左右两侧的筷子编号分别为i和(i + 1) mod 5。请参考所引文章中的伪代码。
为避免死锁,必须对筷子进行联合加锁。如果先锁定一根筷子再锁定另一根,仍可能陷入死锁。
随机化左右筷子的锁定顺序并不能避免死锁,反而会增加死锁概率。如果所有哲学家都先锁定右侧筷子,那么谁都无法获得左侧筷子。这正是导致死锁的原因!
在您的实现中,您随机分配了筷子。这意味着哲学家们不是处于一个大循环中,而可能形成两个或多个小循环。这会增加死锁概率。
不能对筷子使用互斥锁。哲学家必须在持有两根筷子时进食,但在等待第二根筷子时不能持续占用(锁定)已持有的筷子。
使用全局互斥锁可以轻松实现,但想必这不是您想要实现的方式。
另一种实现方式是:哲学家先锁定第一根筷子,然后尝试锁定第二根。如果成功,则开始进食,之后释放两把锁;否则,释放第一把锁并随机思考一段时间后重新尝试锁定两根筷子。
问题在于Go语言没有try lock方法。不过可以通过原子操作来实现。
采用try lock方法时,锁定顺序至关重要。必须始终先锁定右侧筷子再尝试锁定左侧筷子,或者采用相反顺序。否则仍会产生死锁。
如果存在能在一个原子步骤中同时锁定两根筷子或都不锁定的方法,则锁定顺序不再重要。
在你的代码中,死锁错误是由于哲学家就餐问题的经典死锁场景导致的。当所有哲学家同时拿起左手边的筷子时,就会出现循环等待的情况,每个哲学家都在等待右边的筷子,但右边的筷子已经被其他哲学家拿走了。
主要问题在于:
- 筷子分配逻辑有问题 - 随机分配可能导致哲学家拿到的不是相邻的筷子
- 没有实现任何死锁避免机制
以下是修复后的代码:
package main
import (
"fmt"
"sync"
"time"
)
type Chops struct{ sync.Mutex }
type Philo struct {
id int
leftChopStick, rightChopStick *Chops
count int
}
var wq sync.WaitGroup
func (p *Philo) eat() {
for i := 0; i < 3; i++ {
// 先锁左边的筷子,再锁右边的筷子(避免死锁的关键)
if p.id%2 == 0 {
// 偶数编号的哲学家先拿左边筷子
p.leftChopStick.Lock()
p.rightChopStick.Lock()
} else {
// 奇数编号的哲学家先拿右边筷子(打破循环等待)
p.rightChopStick.Lock()
p.leftChopStick.Lock()
}
p.count++
fmt.Printf("starting to eat %d, %d time(s)\n", p.id+1, p.count)
time.Sleep(time.Millisecond * 100) // 模拟吃饭时间
fmt.Printf("finishing eating %d\n", p.id+1)
p.leftChopStick.Unlock()
p.rightChopStick.Unlock()
time.Sleep(time.Millisecond * 100) // 思考时间
}
wq.Done()
}
func main() {
// 创建5根筷子
ChopSticks := make([]*Chops, 5)
for i := 0; i < 5; i++ {
ChopSticks[i] = new(Chops)
}
// 创建5个哲学家,正确分配相邻的筷子
philos := make([]*Philo, 5)
for i := 0; i < 5; i++ {
philos[i] = &Philo{
id: i,
leftChopStick: ChopSticks[i],
rightChopStick: ChopSticks[(i+1)%5],
count: 0,
}
}
// 启动所有哲学家
for i := 0; i < 5; i++ {
wq.Add(1)
go philos[i].eat()
}
wq.Wait()
fmt.Println("All philosophers have finished eating")
}
关键修改:
- 正确的筷子分配:每个哲学家获得左边和右边的相邻筷子
- 死锁避免策略:偶数编号哲学家先拿左边筷子,奇数编号哲学家先拿右边筷子,打破循环等待
- 指针接收器:将
eat方法的接收器改为指针,确保count字段能正确更新 - 移除随机分配:使用确定性的相邻筷子分配
这个实现确保了不会出现死锁,因为哲学家拿筷子的顺序不同,打破了循环等待的条件。

