首页 > 后端开发 > Golang > 正文

Go 并发编程:解决 Goroutine Deadlock 问题

DDD
发布: 2025-08-15 20:38:01
原创
343人浏览过

go 并发编程:解决 goroutine deadlock 问题

本文旨在帮助开发者理解和解决 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。

错误的做法:

豆包AI编程
豆包AI编程

豆包推出的AI编程助手

豆包AI编程 483
查看详情 豆包AI编程

不要试图通过增加 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 Deadlock 问题的详细内容,更多请关注php中文网其它相关文章!

相关标签:
编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号