本文旨在帮助开发者理解和解决Go并发编程中常见的死锁问题。通过分析一个包含三个并发goroutine互相通信的示例代码,我们将深入探讨死锁产生的原因,并提供一种通过引入缓冲通道和runtime.Gosched()来避免死锁的有效方法。本文还将强调并发程序设计中确定性和避免忙等待的重要性。
死锁的原因分析
在go语言中,死锁通常发生在多个goroutine因相互等待对方释放资源而无限期阻塞的情况下。在提供的示例代码中,三个goroutine Routine1、Routine2和Routine3通过channel进行通信。每个goroutine都试图从channel接收数据,并可能向其他channel发送数据。如果goroutine之间的channel操作顺序不当,就可能导致死锁。
具体来说,如果一个goroutine在尝试从一个空的channel接收数据时被阻塞,而同时其他goroutine也在等待该goroutine发送数据,那么就会形成一个循环等待的局面,从而导致死锁。
解决方案:缓冲通道与runtime.Gosched()
以下是一些可以有效避免死锁的方法:
-
使用缓冲通道:缓冲通道允许channel在没有接收者的情况下存储一定数量的值。这可以避免goroutine因等待发送数据而被立即阻塞。通过在创建channel时指定缓冲区大小,可以提高程序的并发性和容错性。
command12 := make(chan int, 10) // 创建一个缓冲区大小为10的channel
注意: 缓冲区大小的选择需要根据具体应用场景进行权衡。过小的缓冲区可能无法有效缓解阻塞,而过大的缓冲区则可能浪费内存。
-
使用runtime.Gosched():runtime.Gosched()函数可以让当前goroutine放弃执行,允许其他goroutine运行。这可以避免某个goroutine长时间占用CPU资源,从而导致其他goroutine无法及时执行。
在select语句中添加一个default case,并调用runtime.Gosched(),可以确保即使没有channel操作准备好,goroutine也不会无限期阻塞。
select { case cmd1 := <-response12: { // ... } case cmd2 := <-response13: { // ... } default: runtime.Gosched() // 放弃执行,让其他goroutine运行 }
示例代码修改
以下是修改后的示例代码,使用了缓冲通道和runtime.Gosched()来避免死锁:
package main import ( "fmt" "math/rand" "runtime" "time" ) func Routine1(command12 chan int, response12 chan int, command13 chan int, response13 chan int) { rand.Seed(time.Now().UnixNano()) // Seed the random number generator 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() } } 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() } } } close(command12) close(command13) } func Routine2(command12 chan int, response12 chan int, command23 chan int, response23 chan int) { rand.Seed(time.Now().UnixNano()) // Seed the random number generator 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() } 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) { rand.Seed(time.Now().UnixNano()) // Seed the random number generator 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() } 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) } func main() { command12 := make(chan int, 10) response12 := make(chan int, 10) command13 := make(chan int, 10) response13 := make(chan int, 10) command23 := make(chan int, 10) response23 := make(chan int, 10) go Routine1(command12, response12, command13, response13) go Routine2(command12, response12, command23, response23) Routine3(command13, response13, command23, response23) // Wait for a while to allow goroutines to complete time.Sleep(5 * time.Second) }
代码修改说明:
- 所有channel都创建为缓冲channel,缓冲区大小设置为10。
- 在每个select语句中添加了default case,并调用了runtime.Gosched()。
- 添加了随机数种子,保证每次运行结果不一致。
- 主函数中添加了time.Sleep(),等待goroutine执行完成。
其他注意事项
- 避免忙等待:忙等待是指goroutine在一个循环中不断检查某个条件是否满足,而不释放CPU资源。这会浪费CPU资源并可能导致死锁。应该使用channel或其他同步机制来等待事件发生。
- 确定性:并发程序的行为应该是可预测的。避免使用随机数或其他非确定性因素来控制程序的执行流程。
- 资源管理:确保在使用完资源后及时释放,避免资源泄漏。
- 使用工具:Go提供了一些工具来帮助开发者检测死锁,例如go vet和go race。
总结
通过理解死锁产生的原因,并采取适当的措施,可以有效地避免Go并发编程中的死锁问题。缓冲通道和runtime.Gosched()是两种常用的解决方案,但并非银弹。开发者需要根据具体应用场景选择合适的并发模型和同步机制,并仔细测试程序,以确保其正确性和可靠性。 此外,良好的代码设计习惯,例如避免忙等待和保持程序行为的确定性,也是编写高质量并发程序的关键。