
在 Go 并发编程中,死锁是一个常见且令人头疼的问题。当所有 Goroutine 都处于等待状态,无法继续执行时,Go 运行时会抛出 "throw: all goroutines are asleep - deadlock!" 错误。本文将深入分析一个实际的死锁案例,并提供详细的解决方案,包括使用 runtime.Gosched() 让出 CPU 时间片以及利用缓冲通道来打破僵局。此外,我们还会探讨如何避免在并发程序设计中引入不确定性,以提高程序的可维护性和可靠性。
死锁通常发生在多个 Goroutine 相互等待对方释放资源的情况下。由于每个 Goroutine 都无法继续执行,整个程序就被阻塞了。在 Go 中,通道(channel)是 Goroutine 之间进行通信和同步的主要方式,因此,不正确地使用通道是导致死锁的常见原因。
以下代码展示了一个可能导致死锁的并发程序:
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func Routine1(command12 chan int, response12 chan int, command13 chan int, response13 chan int) {
z12 := 200
z13 := 200
m12 := false
m13 := false
y := 0
for i := 0; i < 20; i++ {
y = rand.Intn(100)
if y == 0 {
fmt.Println(z12, " z12 STATE SAVED")
fmt.Println(z13, " z13 STATE SAVED")
y = 0
command12 <- y
command13 <- y
for m12 != true || m13 != true {
select {
case cmd1 := <-response12:
{
z12 = cmd1
if z12 != 0 {
fmt.Println(z12, " z12 Channel Saving.... ")
y = rand.Intn(100)
command12 <- y
}
if z12 == 0 {
m12 = true
fmt.Println(" z12 Channel Saving Stopped ")
}
}
case cmd2 := <-response13:
{
z13 = cmd2
if z13 != 0 {
fmt.Println(z13, " z13 Channel Saving.... ")
y = rand.Intn(100)
command13 <- y
}
if z13 == 0 {
m13 = true
fmt.Println(" z13 Channel Saving Stopped ")
}
}
default:
runtime.Gosched() // 让出 CPU 时间片
}
}
m12 = false
m13 = false
}
if y != 0 {
if y%2 == 0 {
command12 <- y
}
if y%2 != 0 {
command13 <- y
}
select {
case cmd1 := <-response12:
{
z12 = cmd1
fmt.Println(z12, " z12")
}
case cmd2 := <-response13:
{
z13 = cmd2
fmt.Println(z13, " z13")
}
default:
runtime.Gosched() // 让出 CPU 时间片
}
}
}
close(command12)
close(command13)
}
func Routine2(command12 chan int, response12 chan int, command23 chan int, response23 chan int) {
z21 := 200
z23 := 200
m21 := false
m23 := false
for i := 0; i < 20; i++ {
select {
case x, open := <-command12:
{
if !open {
return
}
if x != 0 && m23 != true {
z21 = x
fmt.Println(z21, " z21")
}
if x != 0 && m23 == true {
z21 = x
fmt.Println(z21, " z21 Channel Saving ")
}
if x == 0 {
m21 = true
if m21 == true && m23 == true {
fmt.Println(" z21 and z23 Channel Saving Stopped ")
m23 = false
m21 = false
}
if m21 == true && m23 != true {
z21 = x
fmt.Println(z21, " z21 Channel Saved ")
}
}
}
case x, open := <-response23:
{
if !open {
return
}
if x != 0 && m21 != true {
z23 = x
fmt.Println(z23, " z21")
}
if x != 0 && m21 == true {
z23 = x
fmt.Println(z23, " z23 Channel Saving ")
}
if x == 0 {
m23 = true
if m21 == true && m23 == true {
fmt.Println(" z23 Channel Saving Stopped ")
m23 = false
m21 = false
}
if m23 == true && m21 != true {
z23 = x
fmt.Println(z23, " z23 Channel Saved ")
}
}
}
default:
runtime.Gosched() // 让出 CPU 时间片
}
if m23 == false && m21 == false {
y := rand.Intn(100)
if y%2 == 0 {
if y == 0 {
y = 10
response12 <- y
}
}
if y%2 != 0 {
if y == 0 {
y = 10
response23 <- y
}
}
}
if m23 == true && m21 != true {
y := rand.Intn(100)
response12 <- y
}
if m23 != true && m21 == true {
y := rand.Intn(100)
command23 <- y
}
}
close(response12)
close(command23)
}
func Routine3(command13 chan int, response13 chan int, command23 chan int, response23 chan int) {
z31 := 200
z32 := 200
m31 := false
m32 := false
for i := 0; i < 20; i++ {
select {
case x, open := <-command13:
{
if !open {
return
}
if x != 0 && m32 != true {
z31 = x
fmt.Println(z31, " z21")
}
if x != 0 && m32 == true {
z31 = x
fmt.Println(z31, " z31 Channel Saving ")
}
if x == 0 {
m31 = true
if m31 == true && m32 == true {
fmt.Println(" z21 Channel Saving Stopped ")
m31 = false
m32 = false
}
if m31 == true && m32 != true {
z31 = x
fmt.Println(z31, " z31 Channel Saved ")
}
}
}
case x, open := <-command23:
{
if !open {
return
}
if x != 0 && m31 != true {
z32 = x
fmt.Println(z32, " z32")
}
if x != 0 && m31 == true {
z32 = x
fmt.Println(z32, " z32 Channel Saving ")
}
if x == 0 {
m32 = true
if m31 == true && m32 == true {
fmt.Println(" z32 Channel Saving Stopped ")
m31 = false
m32 = false
}
if m32 == true && m31 != true {
z32 = x
fmt.Println(z32, " z32 Channel Saved ")
}
}
}
default:
runtime.Gosched() // 让出 CPU 时间片
}
if m31 == false && m32 == false {
y := rand.Intn(100)
if y%2 == 0 {
response13 <- y
}
if y%2 != 0 {
response23 <- y
}
}
if m31 == true && m32 != true {
y := rand.Intn(100)
response13 <- y
}
if m31 != true && m32 == true {
y := rand.Intn(100)
response23 <- y
}
}
close(response13)
close(response23)
}
const bufferSize = 4 // 缓冲大小
func main() {
rand.Seed(time.Now().UnixNano()) // 初始化随机数生成器
command12 := make(chan int, bufferSize)
response12 := make(chan int, bufferSize)
command13 := make(chan int, bufferSize)
response13 := make(chan int, bufferSize)
command23 := make(chan int, bufferSize)
response23 := make(chan int, bufferSize)
go Routine1(command12, response12, command13, response13)
go Routine2(command12, response12, command23, response23)
Routine3(command13, response13, command23, response23)
}这段代码创建了三个 Goroutine,它们通过多个通道相互通信。Routine1 是一个发起者,它可以向 Routine2 和 Routine3 发送数据,并期望收到响应。Routine2 和 Routine3 则根据接收到的数据进行处理,并可能向其他 Goroutine 发送数据。
这段代码的复杂性在于它试图模拟一种状态保存机制,当 y 的值为 0 时,Goroutine 会尝试保存当前状态,并与其他 Goroutine 协调。这种复杂的逻辑增加了死锁的可能性。
在这个例子中,死锁的根本原因是:
具体来说,当 Routine1 尝试同时向 command12 和 command13 发送 0 时,如果 Routine2 和 Routine3 没有准备好接收,那么 Routine1 就会被阻塞。此时,如果 Routine2 和 Routine3 又在等待 Routine1 发送其他数据,那么就会形成一个死锁。
针对上述死锁问题,可以采取以下几种解决方案:
以下代码展示了如何使用 runtime.Gosched() 和缓冲通道来改进上述程序:
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
// ... (Routine1, Routine2, Routine3 函数的定义,与之前相同,但添加了 default case 并调用 runtime.Gosched())
const bufferSize = 4 // 缓冲大小
func main() {
rand.Seed(time.Now().UnixNano()) // 初始化随机数生成器
command12 := make(chan int, bufferSize)
response12 := make(chan int, bufferSize)
command13 := make(chan int, bufferSize)
response13 := make(chan int, bufferSize)
command23 := make(chan int, bufferSize)
response23 := make(chan int, bufferSize)
go Routine1(command12, response12, command13, response13)
go Routine2(command12, response12, command23, response23)
Routine3(command13, response13, command23, response23)
time.Sleep(5 * time.Second) // 保证所有 Goroutine 运行完成
}在这个改进后的代码中,我们首先将所有的通道都改为了缓冲通道,并设置了缓冲区大小为 4。这允许 Goroutine 在没有接收者的情况下发送少量数据,从而减少了阻塞的可能性。
其次,我们在 select 语句的 default 分支中添加了 runtime.Gosched() 调用。这使得 Goroutine 在没有其他事件发生时,可以主动让出 CPU 时间片,允许其他 Goroutine 运行。
最后,我们在 main 函数中添加了一个 time.Sleep() 调用,以确保所有的 Goroutine 都有足够的时间运行完成。
Go 并发编程中的死锁是一个常见但可以避免的问题。通过理解死锁产生的原因,并采取相应的解决方案,我们可以编写出更加健壮和可靠的并发程序。
在设计并发程序时,应该始终牢记以下几点:
通过遵循这些原则,我们可以编写出更加高效、可靠和易于维护的 Go 并发程序。
以上就是解决 Go 并发程序中的死锁问题:深入分析与实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号