Go 并发模式:使用 WaitGroup 和通道避免死锁


go 并发模式:使用 waitgroup 和通道避免死锁

本文深入探讨了Go语言中N个worker goroutine与一个监控goroutine协调时常见的死锁问题。通过分析`sync.WaitGroup`和通道(channel)的不当使用,文章提供了两种有效的解决方案:一是通过在所有worker完成后关闭通道,使接收方优雅退出;二是在打印逻辑也由单独goroutine处理时,引入额外的同步通道来确保主程序正确终止,从而避免`all goroutines are asleep - deadlock`。

在Go语言的并发编程中,goroutine、channel和sync.WaitGroup是实现高效并发模式的核心工具。然而,不恰当的使用方式,尤其是通道的生命周期管理,很容易导致程序进入死锁状态,并抛出all goroutines are asleep - deadlock的错误。本教程将通过一个经典的“生产者-消费者”场景,深入分析这类死锁的成因,并提供两种健壮的解决方案。

理解死锁问题:监控 Goroutine 的无限等待

考虑一个常见的并发场景:有N个工作(worker)goroutine负责生产数据并发送到一个共享通道,一个监控(monitor)goroutine负责从该通道接收并处理数据,而主程序需要等待所有工作和监控任务完成后才退出。

以下是一个可能导致死锁的初始代码示例:

package main

import (
    "fmt"
    "strconv"
    "sync"
)

func worker(wg *sync.WaitGroup, cs chan string, i int) {
    defer wg.Done()
    cs <- "worker" + strconv.Itoa(i)
}

func monitorWorker(wg *sync.WaitGroup, cs chan string) {
    defer wg.Done()
    for i := range cs { // 此处会无限等待
        fmt.Println(i)
    }
}

func main() {
    wg := &sync.WaitGroup{}
    cs := make(chan string)

    // 启动10个worker goroutine
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(wg, cs, i)
    }

    // 启动一个monitorWorker goroutine
    wg.Add(1)
    go monitorWorker(wg, cs)

    // 等待所有goroutine完成
    wg.Wait()
}

问题分析:

上述代码中,main函数启动了10个worker goroutine和一个monitorWorker goroutine。worker goroutine将数据发送到cs通道,并在完成后调用wg.Done()。monitorWorker goroutine通过for i := range cs循环从通道cs接收数据。

死锁发生的原因在于:

  1. 所有worker goroutine完成任务后,都已将数据发送到cs通道并调用了wg.Done()。
  2. 此时,monitorWorker goroutine的for i := range cs循环会继续尝试从cs通道接收数据。由于没有其他goroutine会再向cs发送数据,并且cs通道从未被关闭,monitorWorker将无限期地阻塞在此处。
  3. main函数调用wg.Wait(),它在等待monitorWorker goroutine调用wg.Done()。但monitorWorker被阻塞,无法执行defer wg.Done()。
  4. 最终,程序中所有可运行的goroutine(除了main函数,monitorWorker被阻塞)都已完成或处于休眠状态,导致Go运行时检测到死锁并报错all goroutines are asleep - deadlock。

解决方案一:通过专用 Goroutine 关闭通道

解决此问题的关键在于,当所有发送方完成任务后,必须关闭通道cs,以通知接收方monitorWorker(或main函数中的for range循环)不再有数据会到来,从而使其优雅地退出循环。

我们可以引入一个专门的monitorWorker goroutine来负责等待所有worker完成,然后关闭cs通道。而数据接收和打印的逻辑则可以放在main函数中。

Copymatic Copymatic

Cowriter是一款AI写作工具,可以通过为你生成内容来帮助你加快写作速度和激发写作灵感。

Copymatic 149 查看详情 Copymatic
package main

import (
    "fmt"
    "strconv"
    "sync"
)

func worker(wg *sync.WaitGroup, cs chan string, i int) {
    defer wg.Done()
    cs <- "worker" + strconv.Itoa(i)
}

// 新的monitorWorker职责:等待所有worker完成,然后关闭通道
func monitorWorker(wg *sync.WaitGroup, cs chan string) {
    wg.Wait() // 等待所有worker goroutine完成
    close(cs) // 所有worker完成后,关闭通道cs
}

func main() {
    wg := &sync.WaitGroup{}
    cs := make(chan string)

    // 启动10个worker goroutine
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(wg, cs, i)
    }

    // 启动一个专门的monitorWorker来关闭通道
    go monitorWorker(wg, cs) // 注意:这里不再将monitorWorker添加到wg中

    // main goroutine负责从通道接收并打印数据
    for i := range cs { // 当cs通道被关闭时,此循环将自动退出
        fmt.Println(i)
    }

    // main函数在此处退出,程序结束
}

方案分析:

  1. worker goroutine照常发送数据并调用wg.Done()。
  2. 新的monitorWorker goroutine在启动后,立即调用wg.Wait()。它会阻塞直到所有worker goroutine都调用了wg.Done()。
  3. 一旦wg.Wait()返回,意味着所有worker都已完成,monitorWorker随即调用close(cs)关闭通道。
  4. main函数中的for i := range cs循环会持续接收数据,直到cs通道被monitorWorker关闭。通道关闭后,for range循环会自动终止。
  5. main函数在循环结束后自然退出,程序正常终止,避免了死锁。

注意事项:

  • monitorWorker goroutine没有被添加到main函数的wg中,因为它不是一个需要main等待其完成的“工作”;它的职责是作为协调者,在worker完成后关闭通道。
  • 只有发送方才能关闭通道。 在本例中,虽然monitorWorker不是数据的直接发送方,但它通过wg.Wait()确保了所有发送方都已完成发送,因此它作为协调者来关闭通道是安全的。

解决方案二:多 Goroutine 协调与额外同步通道

如果业务需求坚持将数据接收和打印逻辑也放在一个独立的goroutine中(例如,printWorker),那么我们需要更复杂的同步机制来确保main函数在所有数据处理完毕后才退出。

package main

import (
    "fmt"
    "strconv"
    "sync"
)

func worker(wg *sync.WaitGroup, cs chan string, i int) {
    defer wg.Done()
    cs <- "worker" + strconv.Itoa(i)
}

// 职责同解决方案一:等待所有worker完成,然后关闭通道
func monitorWorker(wg *sync.WaitGroup, cs chan string) {
    wg.Wait()
    close(cs)
}

// 独立的打印goroutine,负责从cs接收并打印数据
func printWorker(cs <-chan string, done chan<- bool) {
    for i := range cs {
        fmt.Println(i)
    }
    // 当cs通道关闭且所有数据处理完毕后,向done通道发送信号
    done <- true
}

func main() {
    wg := &sync.WaitGroup{}
    cs := make(chan string)

    // 启动10个worker goroutine
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(wg, cs, i)
    }

    // 启动monitorWorker来关闭cs通道
    go monitorWorker(wg, cs)

    // 创建一个用于printWorker通知main完成的通道
    done := make(chan bool, 1) // 使用带缓冲的通道,避免printWorker阻塞
    // 启动printWorker goroutine
    go printWorker(cs, done)

    // main goroutine等待printWorker完成的信号
    <-done // 阻塞直到从done通道接收到信号
}

方案分析:

  1. worker goroutine和monitorWorker goroutine的功能与解决方案一相同。monitorWorker在所有worker完成后关闭cs通道。
  2. printWorker goroutine负责从cs通道接收并打印数据。当cs通道被关闭,且所有已发送的数据都被printWorker消费完毕后,printWorker的for range循环会终止。
  3. printWorker在循环终止后,向done通道发送一个true值。
  4. main函数通过

关键点:

  • done := make(chan bool, 1):done通道被创建为带缓冲的(容量为1)。这确保了printWorker在发送true到done时不会因为main尚未准备好接收而阻塞,从而避免了潜在的死锁或竞态条件。
  • 这种模式在需要更细粒度的goroutine协调时非常有用,它允许不同的goroutine在完成各自任务后,通过通道向其他goroutine发出信号。

总结与最佳实践

解决Go并发编程中的死锁问题,特别是all goroutines are asleep - deadlock,核心在于对goroutine生命周期和通道状态的精确管理。

  1. 通道的关闭是关键: 当不再有数据发送到通道时,必须关闭该通道(close(channel)),以通知所有接收方for range循环可以安全退出。
  2. 谁来关闭通道: 只有发送方(或能确认所有发送方已完成的协调者)才能关闭通道。关闭一个已经关闭的通道会导致panic。从一个已关闭的通道接收数据会立即返回零值,并且第二个返回值(ok)为false。
  3. sync.WaitGroup 的作用: WaitGroup主要用于等待一组goroutine完成。在上述示例中,它被用来等待所有worker goroutine完成。
  4. 明确的同步信号: 当一个goroutine的完成需要通知另一个goroutine(尤其是main函数)时,使用一个额外的通道进行信号传递是一种健壮的模式。
  5. 避免无限等待: 仔细检查for range循环在通道上的使用。如果通道永远不会关闭,for range将导致无限期阻塞。

通过理解这些原则并应用上述解决方案,您可以有效地构建健壮、高效且无死锁的Go并发程序。

以上就是Go 并发模式:使用 WaitGroup 和通道避免死锁的详细内容,更多请关注其它相关文章!


# go语言  # go  # 达州移动网站建设  # 锐酷网站建设流程  # 巩义网站建设优化诊断  # seo排名影响  # 响水seo优化哪个好  # 怒江州网站推广费用  # 私募股权基金网站建设  # 两江游的市场营销和推广  # 合川抖音关键词排名公司  # seo网站怎么设置https  # 数据处理  # 两种  # 主程序  # 尤其是  # 放在  # 器中  # 都已  # 发送到  # 完成后  # 死锁  # 同步机制  # 并发编程  # ai  # 工具 


相关栏目: 【 Google疑问12 】 【 Facebook疑问10 】 【 优化推广96088 】 【 技术知识133117 】 【 IDC资讯59369 】 【 网络运营7196 】 【 IT资讯61894


相关推荐: 小红书网页版在线直达 小红书网页版免费登录入口  iPhone12是否要更新ios16  抖音如何解除|直播|权限绑定_抖音关闭并解绑|直播|功能的方法  《i莞家》修改昵称方法  谷歌浏览器官网地址整理_谷歌浏览器新版直连2026稳定访问  《偃武》甘宁技能详解  ao3入口镜像地址 ao3镜像入口可靠跳转  win11如何运行chkdsk命令 Win11检查和修复磁盘逻辑错误教程【修复】  win11怎么更改账户类型 Win11标准用户和管理员权限切换【教程】  哈尔滨城市通昵称修改方法  多闪电脑版下载_多闪PC端模拟器使用  composer 提示 "requires ext-soap" 缺少 SOAP 扩展怎么办?  J*aScript二进制处理_ArrayBuffer与Blob  C++ optional用法详解_C++17处理可能为空的返回值  Cassandra中复合主键、二级索引与ORDER BY排序的限制与解决方案  支付宝登录刷脸不是本人如何解决  Sublime怎么自动添加CSS前缀_Sublime安装Autoprefixer插件  Linux如何自动分析系统异常日志_Linux日志智能检测  Flask 应用中图片动态更新与上传:实现客户端定时刷新与服务器端文件管理  《幻兽帕鲁》手游帕鲁捕捉技巧分享  Excel如何制作月度销售统计图_Excel动态图表制作与控件应用  如何解决Casbin日志与应用日志不统一的问题,使用casbin/psr3-bridge实现无缝集成  AO3永久镜像入口开放_AO3最新网址兼容所有浏览器  《小宇宙》标记不友善评论方法  大熊猫抓取竹子的“大拇指”其实是什么?蚂蚁庄园课堂今天答案最新11月30日  DeepSeek超全面指南:入门必看  Go语言中方法与接收器:指针和值类型的调用机制详解  C++如何实现矩阵乘法_C++二维数组矩阵运算代码示例  暴风影音官网正式版_暴风影音手机版官网下载安卓  12306不能订票的时间段是固定的吗? | 节假日购票时间有无变化  抖音作品被限流怎么办 抖音内容优化与流量恢复方法  《绿竹漫游》关闭消息通知方法  CSS过渡与滚动滚动事件结合应用_scroll与transition动画  哔哩哔哩在线观看入口 B站官网免费进入  漫蛙manwa2网页版书签同步链接_漫蛙manwa多设备登录入口  精通VS Code多光标编辑以实现闪电般快速的修改  J*a中的值传递到底指什么_值传递模型在参数传递中的真正含义说明  Sublime怎么快速复制文件路径_Sublime右键菜单增强技巧  如何在CSS中使用absolute实现登录弹窗居中_transform translate结合  网易云音乐闹钟铃声设置教程  使用document.execCommand实现Web文本编辑器加粗/取消加粗  漫蛙官网(首页入口)_漫蛙漫画稳定访问教程分享  使用Selenium在无头Chrome中交互动态菜单和复选框的策略  高德地图导航路线偏差报警频繁怎么办 高德地图路线偏差修复与优化方法  MongoDB聚合管道:高效统计列表中各项的文档数量  《海贝音乐》均衡器设置方法  天天漫画2025最新入口 天天漫画永久有效登录入口  如何在mysql中设计餐饮点餐系统_mysql点餐系统项目实战  《东方财富》条件单关闭方法  《大周列国志》皇帝律令功能介绍 

 2025-11-09

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.