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

9 回复

是的,基本上我不是每次都会收到死锁错误

更多关于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方法时,锁定顺序至关重要。必须始终先锁定右侧筷子再尝试锁定左侧筷子,或者采用相反顺序。否则仍会产生死锁。

如果存在能在一个原子步骤中同时锁定两根筷子或都不锁定的方法,则锁定顺序不再重要。

在你的代码中,死锁错误是由于哲学家就餐问题的经典死锁场景导致的。当所有哲学家同时拿起左手边的筷子时,就会出现循环等待的情况,每个哲学家都在等待右边的筷子,但右边的筷子已经被其他哲学家拿走了。

主要问题在于:

  1. 筷子分配逻辑有问题 - 随机分配可能导致哲学家拿到的不是相邻的筷子
  2. 没有实现任何死锁避免机制

以下是修复后的代码:

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")
}

关键修改:

  1. 正确的筷子分配:每个哲学家获得左边和右边的相邻筷子
  2. 死锁避免策略:偶数编号哲学家先拿左边筷子,奇数编号哲学家先拿右边筷子,打破循环等待
  3. 指针接收器:将eat方法的接收器改为指针,确保count字段能正确更新
  4. 移除随机分配:使用确定性的相邻筷子分配

这个实现确保了不会出现死锁,因为哲学家拿筷子的顺序不同,打破了循环等待的条件。

回到顶部