Go并发死锁原因及解决技巧
Go并发编程中死锁问题一直是开发者需要重视的挑战。本文以实际案例出发,深入剖析了Go程序中死锁的常见原因,重点强调了无缓冲通道的阻塞性、相互依赖的Goroutine以及复杂的状态管理如何导致死锁。针对这些问题,本文提出了包括利用`runtime.Gosched()`让出CPU时间片、使用缓冲通道以及简化状态管理等多种解决方案,旨在帮助开发者避免并发程序中的不确定性,提高程序的可维护性和可靠性。通过理解死锁的本质并采取相应的预防措施,开发者能够编写出更加健壮的Go并发程序。
在 Go 并发编程中,死锁是一个常见且令人头疼的问题。当所有 Goroutine 都处于等待状态,无法继续执行时,Go 运行时会抛出 "throw: all goroutines are asleep - deadlock!" 错误。本文将深入分析一个实际的死锁案例,并提供详细的解决方案,包括使用 runtime.Gosched() 让出 CPU 时间片以及利用缓冲通道来打破僵局。此外,我们还会探讨如何避免在并发程序设计中引入不确定性,以提高程序的可维护性和可靠性。
理解 Go 中的死锁
死锁通常发生在多个 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 协调。这种复杂的逻辑增加了死锁的可能性。
死锁的原因
在这个例子中,死锁的根本原因是:
- 无缓冲通道的阻塞性: 如果一个 Goroutine 尝试向一个无缓冲通道发送数据,但没有其他 Goroutine 准备好接收,那么发送操作将会被阻塞。同样,如果一个 Goroutine 尝试从一个无缓冲通道接收数据,但通道中没有数据,那么接收操作也会被阻塞。
- 相互依赖的 Goroutine: Routine1、Routine2 和 Routine3 相互依赖,它们之间的通信需要按照特定的顺序进行。如果任何一个 Goroutine 被阻塞,其他 Goroutine 也可能因为等待而无法继续执行。
- 复杂的状态管理: 状态保存机制增加了代码的复杂性,使得 Goroutine 之间的交互更加难以预测。
具体来说,当 Routine1 尝试同时向 command12 和 command13 发送 0 时,如果 Routine2 和 Routine3 没有准备好接收,那么 Routine1 就会被阻塞。此时,如果 Routine2 和 Routine3 又在等待 Routine1 发送其他数据,那么就会形成一个死锁。
解决方案
针对上述死锁问题,可以采取以下几种解决方案:
- 使用 runtime.Gosched(): 在 select 语句的 default 分支中调用 runtime.Gosched() 可以让出 CPU 时间片,允许其他 Goroutine 运行。这有助于打破僵局,避免死锁。
- 使用缓冲通道: 将无缓冲通道改为缓冲通道可以缓解阻塞问题。缓冲通道允许在没有接收者的情况下发送一定数量的数据,从而减少了 Goroutine 之间的依赖性。
- 简化状态管理: 重新设计状态保存机制,使其更加简单和可预测。避免在 Goroutine 之间传递复杂的状态信息,尽量使用原子操作或互斥锁来保护共享数据。
- 超时机制: 在 select 语句中使用 time.After 添加超时机制,避免 Goroutine 无限期地等待。
代码改进
以下代码展示了如何使用 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 都有足够的时间运行完成。
注意事项
- 缓冲通道的大小: 缓冲通道的大小需要根据实际情况进行调整。如果缓冲区太小,仍然可能导致阻塞;如果缓冲区太大,可能会浪费内存。
- runtime.Gosched() 的使用: runtime.Gosched() 应该谨慎使用。过度使用 runtime.Gosched() 可能会降低程序的性能。
- 并发程序的确定性: 尽量避免在并发程序中使用随机数或依赖于外部状态的操作。这可以提高程序的可预测性和可维护性。
总结
Go 并发编程中的死锁是一个常见但可以避免的问题。通过理解死锁产生的原因,并采取相应的解决方案,我们可以编写出更加健壮和可靠的并发程序。
在设计并发程序时,应该始终牢记以下几点:
- 尽量减少 Goroutine 之间的依赖性。
- 避免在 Goroutine 之间传递复杂的状态信息。
- 使用原子操作或互斥锁来保护共享数据。
- 使用缓冲通道来缓解阻塞问题。
- 谨慎使用 runtime.Gosched()。
- 尽量提高并发程序的确定性。
通过遵循这些原则,我们可以编写出更加高效、可靠和易于维护的 Go 并发程序。
理论要掌握,实操不能落!以上关于《Go并发死锁原因及解决技巧》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!

- 上一篇
- 择吉日技巧与实用方法大全

- 下一篇
- 华为手机UC缓存视频转存教程
-
- Golang · Go教程 | 1分钟前 | Http请求 net/http 响应处理 GolangHTTP http.Client
- GolangHTTP请求响应处理全解析
- 169浏览 收藏
-
- Golang · Go教程 | 25分钟前 |
- GolangARM交叉编译方法详解
- 205浏览 收藏
-
- Golang · Go教程 | 33分钟前 |
- Go调用WindowsAPI:字符串转LPCWSTR技巧
- 367浏览 收藏
-
- Golang · Go教程 | 36分钟前 |
- GolangTCP长连接短连接管理技巧
- 115浏览 收藏
-
- Golang · Go教程 | 49分钟前 | golang Goroutine 调度控制 并发性能优化 context.Context
- Golang并发优化:goroutine调度技巧
- 190浏览 收藏
-
- Golang · Go教程 | 1小时前 | golang prometheus 指标收集 HTTP端点 client_golang
- Golang监控:Prometheus指标收集教程
- 105浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Golang容器健康检查与自愈实现方法
- 140浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Golang命令模式详解:封装与执行分离
- 387浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Golang协程泄漏排查与pprof使用方法
- 244浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 514次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 499次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 千音漫语
- 千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
- 1057次使用
-
- MiniWork
- MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
- 1009次使用
-
- NoCode
- NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
- 1042次使用
-
- 达医智影
- 达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
- 1056次使用
-
- 智慧芽Eureka
- 智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
- 1035次使用
-
- Golangmap实践及实现原理解析
- 2022-12-28 505浏览
-
- 试了下Golang实现try catch的方法
- 2022-12-27 502浏览
-
- 如何在go语言中实现高并发的服务器架构
- 2023-08-27 502浏览
-
- go和golang的区别解析:帮你选择合适的编程语言
- 2023-12-29 502浏览
-
- 提升工作效率的Go语言项目开发经验分享
- 2023-11-03 502浏览