Go语言中WaitGroup死锁:值传递陷阱与正确用法

风浩酱_9329

风浩酱_9329

2025-09-23

1038人浏览

原创

Go语言中WaitGroup死锁:值传递陷阱与正确用法

本文深入探讨了Go语言中因sync.WaitGroup值传递导致的并发死锁问题。当WaitGroup作为参数传递给goroutine时,如果采用值传递,每个goroutine会操作其自身的副本,而非主goroutine等待的原始实例,从而导致主goroutine无限等待。文章通过示例代码详细分析了问题根源,并提供了通过指针传递WaitGroup的正确解决方案,强调了Go语言中结构体值传递的关键概念,以帮助开发者构建健壮的并发应用。

Go并发编程中的WaitGroup与死锁现象

在go语言中,sync.waitgroup是管理并发任务的重要工具,它允许一个goroutine等待一组其他goroutine完成。通常,我们通过add()方法设置需要等待的goroutine数量,每个goroutine完成时调用done(),最后主goroutine通过wait()阻塞直到所有done()都被调用。然而,在使用waitgroup时,一个常见的陷阱是因其传递方式不当而引发死锁。

考虑以下一个尝试使用WaitGroup协调生产者(push)和消费者(pull)goroutine的例子:

package main

import (
    "fmt"
    "sync"
)

func push(c chan int, wg sync.WaitGroup) { // 注意:wg是值传递
    for i := 0; i <p>当运行上述代码时,程序会输出部分结果,然后抛出死锁错误:</p><pre class="brush:php;toolbar:false;">0 true
1 true
2 true
3 true
4 true
throw: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x42130100, 0x42130100)
    /usr/local/go/src/pkg/runtime/zsema_amd64.c:146 +0x25
sync.(*WaitGroup).Wait(0x42120420, 0x0)
    /usr/local/go/src/pkg/sync/waitgroup.go:79 +0xf2
main.main()
    /Users/kuankuan/go/src/goroutine.go:31 +0xb9

goroutine 2 [syscall]:
created by runtime.main
    /usr/local/go/src/pkg/runtime/proc.c:221
exit status 2

死锁原因分析:Go语言的值传递特性

这个死锁的根本原因在于Go语言中结构体(sync.WaitGroup是一个结构体)的默认传递方式是值传递。

  1. 副本创建: 在main函数中,我们声明了一个var wg sync.WaitGroup。当我们将wg作为参数传递给push和pull这两个函数时,Go语言会为这两个函数各自创建wg的一个副本。
  2. 操作副本: push和pull函数内部调用的wg.Done()操作的是它们各自收到的WaitGroup副本,而不是main函数中声明的原始wg实例。
  3. 原始WaitGroup状态不变: 由于Done()操作的是副本,main函数中的原始wg的内部计数器从未减少。它在wg.Add(2)之后,计数器一直保持为2。
  4. 无限等待: 当main函数执行到wg.Wait()时,它会无限期地等待原始wg的计数器归零。然而,由于所有Done()调用都作用于副本,原始wg永远无法达到计数器为零的状态。
  5. 所有goroutine休眠: push和pull goroutine在完成各自的任务后,它们对副本wg调用Done()并退出。此时,除了等待中的main goroutine,没有其他活跃的goroutine可以改变原始wg的状态。Go运行时检测到所有goroutine都已休眠且无法继续执行(即main goroutine在等待一个永远不会发生的事件),便会抛出“all goroutines are asleep - deadlock!”的死锁错误。

正确的解决方案:通过指针传递WaitGroup

为了解决这个问题,我们需要确保所有goroutine操作的是同一个WaitGroup实例。在Go语言中,实现这一目标的方法是通过指针传递WaitGroup。当传递指针时,我们传递的是内存地址,所有操作都会作用于该地址指向的同一个WaitGroup对象。

以下是修正后的代码示例:

Go语言(Golang)1.26.0
Go语言(Golang)1.26.0

Go语言(Golang)1.26.0版本官方下载,版本号 1.26.0,适合旧项目维护、兼容性测试和指定版本开发环境搭建。

下载
package main

import (
    "fmt"
    "sync"
)

// push函数现在接收一个*sync.WaitGroup指针
func push(c chan int, wg *sync.WaitGroup) { 
    defer wg.Done() // 使用defer确保在函数退出前调用Done()
    for i := 0; i <p><strong>代码改进说明:</strong></p><ol>
<li>
<strong>函数签名修改:</strong> push和pull函数的wg参数类型从sync.WaitGroup改为了*sync.WaitGroup。</li>
<li>
<strong>调用方式修改:</strong> 在main函数中,调用go push(c, &wg)和go pull(c, &wg),通过&操作符获取wg变量的内存地址并传递。</li>
<li>
<strong>defer wg.Done():</strong> 在push和pull函数内部,使用defer wg.Done()确保无论函数如何退出(正常完成或发生panic),Done()都会被调用,从而正确地减少WaitGroup的计数器。</li>
<li>
<strong>Channel关闭时机:</strong> close(c)被移到wg.Wait()之后。这样可以确保所有生产者(这里只有一个push)都已完成其数据发送,并且WaitGroup已归零,此时关闭channel是安全的,可以通知消费者没有更多数据。</li>
</ol><p>运行修正后的代码,将不再出现死锁,程序会正常执行并退出。</p><h3>总结与最佳实践</h3>
  • 结构体值传递: Go语言中,结构体默认是值传递。这意味着当你将一个结构体作为函数参数传递时,函数会收到该结构体的一个独立副本。对副本的任何修改都不会影响原始结构体。
  • sync.WaitGroup的特殊性: sync.WaitGroup内部包含一个计数器,它的正确性依赖于所有操作都作用于同一个实例。因此,务必通过指针传递sync.WaitGroup给需要调用Add()或Done()的函数或goroutine。
  • 其他并发原语: 类似地,sync.Mutex、sync.RWMutex等并发原语也通常需要通过指针传递,以确保所有goroutine操作的是同一个锁实例,否则将失去同步的意义。
  • 使用defer: 在goroutine中使用defer wg.Done()是一个良好的实践,它能保证Done()在goroutine函数退出时被调用,即使函数提前返回或发生错误。

理解Go语言的值传递机制以及并发原语的正确使用方式,对于编写健壮、高效的并发程序至关重要。避免WaitGroup的值传递陷阱是Go并发编程中的一个基础且关键的知识点。

golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

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

下载

相关标签:

go语言 amd 并发编程

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

2025.06.09

5174

18

golang结构体方法
golang结构体方法

本专题整合了golang结构体相关内容,请阅读专题下面的文章了解更多。

2025.07.04

5351

25

C++ 智能指针与现代内存管理
C++ 智能指针与现代内存管理

深入讲解 C++ 现代内存管理的核心工具——智能指针,涵盖 unique_ptr 独占所有权语义、shared_ptr 引用计数机制与循环引用问题、weak_ptr 弱引用的应用场景、make_unique/make_shared 工厂函数的性能优势、自定义删除器的编写、RAII 资源管理思想的实践,以及从裸指针迁移到智能指针的重构策略,帮助开发者编写安全无泄漏的现代 C++ 代码。

2026.04.23

379

31

Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

2023.09.06

3249

5

go怎么实现链表
go怎么实现链表

go通过定义一个节点结构体、定义一个链表结构体、定义一些方法来操作链表、实现一个方法来删除链表中的一个节点和实现一个方法来打印链表中的所有节点的方法实现链表。

2023.09.25

6487

6

go语言编程软件有哪些
go语言编程软件有哪些

go语言编程软件有Go编译器、Go开发环境、Go包管理器、Go测试框架、Go文档生成器、Go代码质量工具和Go性能分析工具等。本专题为大家提供go语言相关的文章、下载、课程内容,供大家免费下载体验。

2023.10.13

762

5

0基础如何学go语言
0基础如何学go语言

0基础学习Go语言需要分阶段进行,从基础知识到实践项目,逐步深入。php中文网给大家带来了go语言相关的教程以及文章,欢迎大家前来学习。

2023.10.26

8265

20

Go语言实现运算符重载有哪些方法
Go语言实现运算符重载有哪些方法

Go语言不支持运算符重载,但可以通过一些方法来模拟运算符重载的效果。使用函数重载来模拟运算符重载,可以为不同的类型定义不同的函数,以实现类似运算符重载的效果,通过函数重载,可以为不同的类型实现不同的操作。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

3056

5

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

3304

5

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程