Go并发编程:Goroutine死锁避免技巧
本文深入探讨Go并发编程中常见的Goroutine死锁问题,并提供实用的解决方案。通过一个发布者-订阅者模式的案例,详细分析了死锁产生的原因:Goroutine间因channel阻塞造成的相互等待。文章强调避免简单地增加channel缓冲区来掩盖问题,而是要理解并发机制的本质。针对该问题,提供了两种有效的解决方案:一是通过在Subscriber中发送退出信号,并使用带缓冲的channel进行同步;二是利用sync.WaitGroup更优雅地实现Goroutine同步。本文旨在帮助开发者更好地理解和解决Go并发编程中的死锁问题,提升并发程序的稳定性和可靠性,从而写出更健壮的Go程序。
本文旨在帮助开发者理解和解决 Go 语言中常见的 Goroutine Deadlock 问题。通过分析一个简单的 Observer Pattern 实现案例,我们将深入探讨 Deadlock 产生的原因,并提供两种有效的解决方案:使用带缓冲的 channel 或利用 sync.WaitGroup 进行 Goroutine 同步。避免简单地通过增加 channel 缓冲区来掩盖问题,强调理解并发机制的重要性。
在 Go 语言中,Goroutine 是轻量级的并发执行单元,而 channel 则是 Goroutine 之间通信和同步的重要机制。然而,不恰当的使用 channel 可能会导致 Deadlock,即所有 Goroutine 都处于阻塞状态,程序无法继续执行。下面我们通过一个 Observer Pattern 的例子来分析 Deadlock 的产生以及如何解决。
Deadlock 示例分析
考虑以下 Observer Pattern 的实现:
package main import ( "fmt" ) type Publisher struct { listeners []chan int } type Subscriber struct { Channel chan int Name string } func (p *Publisher) Sub(c chan int) { p.listeners = append(p.listeners, c) } func (p *Publisher) Pub(m int, quit chan int) { for _, c := range p.listeners { c <- m } quit <- 0 } func (s *Subscriber) ListenOnChannel() { data := <-s.Channel fmt.Printf("Name: %v; Data: %v\n", s.Name, data) } func main() { quit := make(chan int) p := &Publisher{} subscribers := []*Subscriber{ {Channel: make(chan int), Name: "1"}, {Channel: make(chan int), Name: "2"}, {Channel: make(chan int), Name: "3"}, } for _, v := range subscribers { p.Sub(v.Channel) go v.ListenOnChannel() } p.Pub(2, quit) <-quit }
这段代码尝试实现一个发布者-订阅者模式。Publisher 维护一个监听者列表,当发布消息时,将消息发送到每个监听者的 channel 中。Subscriber 监听自己的 channel,接收到消息后打印。
运行这段代码会抛出 "fatal error: all goroutines are asleep - deadlock!" 错误。
原因分析:
问题在于 p.Pub(2, quit) 函数。它在同一个 Goroutine 中同时向所有 listener 的 channel 发送数据,并向 quit channel 发送数据。由于 quit channel 是无缓冲的,这意味着 p.Pub 必须等待有人从 quit channel 接收数据才能继续执行。然而,main 函数中 <-quit 也正等待从 quit channel 接收数据。这种相互等待导致了 Deadlock。
错误的做法:
不要试图通过增加 channel 的 buffer size 来解决这类问题。虽然增加 buffer size 可能会暂时缓解问题,但它只是掩盖了问题的本质,并没有真正解决并发逻辑上的错误。在开发初期,尽量使用无缓冲的 channel,这样可以更容易地发现并发问题。
解决方案
方案一:在 Subscriber 中发送 quit 信号
修改 ListenOnChannel 函数,在接收到数据后向 quit channel 发送信号。同时,在 main 函数中等待所有 Subscriber 完成。
func (s *Subscriber) ListenOnChannel(quit chan int) { data := <-s.Channel fmt.Printf("Name: %v; Data: %v\n", s.Name, data) quit <- 0 // Subscriber 完成后发送 quit 信号 } func main() { quit := make(chan int, 3) // 创建带缓冲的 quit channel p := &Publisher{} subscribers := []*Subscriber{ {Channel: make(chan int), Name: "1"}, {Channel: make(chan int), Name: "2"}, {Channel: make(chan int), Name: "3"}, } for _, v := range subscribers { p.Sub(v.Channel) go v.ListenOnChannel(quit) // 传递 quit channel } p.Pub(2, quit) for i := 0; i < len(subscribers); i++ { <-quit // 等待所有 Subscriber 完成 } }
在这个方案中,quit channel 的 buffer size 需要设置为 3,因为需要接收来自三个 Subscriber 的信号。
方案二:使用 sync.WaitGroup
sync.WaitGroup 提供了更优雅的方式来等待一组 Goroutine 完成。
package main import ( "fmt" "sync" ) type Publisher struct { listeners []chan int } type Subscriber struct { Channel chan int Name string } func (p *Publisher) Sub(c chan int) { p.listeners = append(p.listeners, c) } func (p *Publisher) Pub(m int) { for _, c := range p.listeners { c <- m } // 关闭所有 channel,通知 Subscriber 结束 for _, c := range p.listeners { close(c) } } func (s *Subscriber) ListenOnChannel(wg *sync.WaitGroup) { defer wg.Done() // Goroutine 结束时调用 Done for data := range s.Channel { // 使用 range 接收数据,channel 关闭后循环结束 fmt.Printf("Name: %v; Data: %v\n", s.Name, data) } } func main() { var wg sync.WaitGroup p := &Publisher{} subscribers := []*Subscriber{ {Channel: make(chan int), Name: "1"}, {Channel: make(chan int), Name: "2"}, {Channel: make(chan int), Name: "3"}, } for _, v := range subscribers { p.Sub(v.Channel) wg.Add(1) // 启动一个 Goroutine 前调用 Add go v.ListenOnChannel(&wg) // 传递 WaitGroup } p.Pub(2) wg.Wait() // 等待所有 Goroutine 完成 }
在这个方案中,我们使用 sync.WaitGroup 来跟踪所有 Subscriber Goroutine 的状态。在每个 Subscriber Goroutine 启动前,调用 wg.Add(1) 增加计数器。在 Goroutine 结束时,调用 wg.Done() 减少计数器。main 函数调用 wg.Wait() 等待计数器归零,即所有 Goroutine 完成。
同时,我们使用 close(c) 关闭了所有的 channel,这样 Subscriber 就可以通过 range 循环接收数据,当 channel 关闭时,循环自动结束。
总结
Deadlock 是 Go 并发编程中常见的问题,理解 Deadlock 产生的原因,并选择合适的解决方案至关重要。通过本文的分析,我们学习了如何避免 Deadlock,并掌握了两种有效的解决方案:使用带缓冲的 channel 或利用 sync.WaitGroup 进行 Goroutine 同步。在实际开发中,应根据具体场景选择合适的方案,并避免简单地通过增加 channel 缓冲区来掩盖问题。
好了,本文到此结束,带大家了解了《Go并发编程:Goroutine死锁避免技巧》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多Golang知识!

- 上一篇
- JavaLambda简化代码教程详解

- 下一篇
- Linux配置Samba共享:跨平台文件访问教程
-
- Golang · Go教程 | 9分钟前 |
- Golang错误处理方式对比分析
- 219浏览 收藏
-
- Golang · Go教程 | 13分钟前 | golang 泛型编程
- Golang空接口与泛型应用技巧
- 404浏览 收藏
-
- Golang · Go教程 | 19分钟前 |
- GolangWebSocket教程:客户端与服务端实现
- 484浏览 收藏
-
- Golang · Go教程 | 22分钟前 |
- GolangCLI工具开发,cobra库入门详解
- 167浏览 收藏
-
- Golang · Go教程 | 39分钟前 |
- Golang模块加速:direct与镜像站对比解析
- 204浏览 收藏
-
- Golang · Go教程 | 45分钟前 |
- Golang切片扩容技巧与优化方法
- 203浏览 收藏
-
- Golang · Go教程 | 48分钟前 | Vscode 断点调试 launch.json Golang调试 dlv
- Golang调试技巧与问题解决指南
- 364浏览 收藏
-
- Golang · Go教程 | 54分钟前 | 权限设置 GoModules GOPRIVATE SSH密钥 Golang私有模块
- Golang私有模块配置与权限设置详解
- 421浏览 收藏
-
- Golang · Go教程 | 59分钟前 |
- Golang切片扩容技巧全解析
- 442浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Gomodinit初始化模块教程
- 267浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 512次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 499次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 千音漫语
- 千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
- 971次使用
-
- MiniWork
- MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
- 928次使用
-
- NoCode
- NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
- 958次使用
-
- 达医智影
- 达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
- 976次使用
-
- 智慧芽Eureka
- 智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
- 956次使用
-
- 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浏览