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简化代码教程详解
- 上一篇
- JavaLambda简化代码教程详解
- 下一篇
- Linux配置Samba共享:跨平台文件访问教程
-
- Golang · Go教程 | 21分钟前 | 数据结构 · go · Go Link 循环链表 container/ring
- Go container/ring.Link 怎么拼接两个循环链表
- 125浏览 收藏
-
- Golang · Go教程 | 37分钟前 |
- Go container/list.MoveToFront 怎么实现最近使用顺序
- 361浏览 收藏
-
- Golang · Go教程 | 1小时前 | pdf · Go compress/lzw lzw.NewWriter LSB MSB
- Go compress/lzw.NewWriter 怎么选择 LSB 与 MSB 位序
- 150浏览 收藏
-
- Golang · Go教程 | 1小时前 | go · Go reset Multistream gzip.Reader io.ByteReader
- Go gzip.Reader.Multistream 怎么处理拼接的 gzip 数据
- 301浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Go bytes.Buffer.ReadFrom 怎么高效接收数据流
- 194浏览 收藏
-
- Golang · Go教程 | 3小时前 | go · Go io.Writer bytes.Reader WriteTo
- Go bytes.Reader.WriteTo 怎么把内存数据流式复制出去
- 466浏览 收藏
-
- Golang · Go教程 | 3小时前 | 文件处理 · go · Go bufio Scanner.Buffer 超长文本行
- Go bufio.Scanner.Buffer 怎么读取超长文本行
- 120浏览 收藏
-
- Golang · Go教程 | 3小时前 | 文件处理 · go · Go archive/tar next tar.Reader
- Go archive/tar.Reader.Next 怎么安全遍历归档条目
- 337浏览 收藏
-
- Golang · Go教程 | 4小时前 | 文件处理 · 标准库 · 资源管理 · go语言 · zip Go archive/zip OpenReader Reader.Open
- Go archive/zip.OpenReader 怎么按名称读取压缩包文件
- 130浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 324次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 378次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 375次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 339次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 165次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

