
在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 < 5; i++ {
c <- i
}
wg.Done() // 对wg的副本调用Done()
}
func pull(c chan int, wg sync.WaitGroup) { // 注意:wg是值传递
for i := 0; i < 5; i++ {
result, ok := <-c
fmt.Println(result, ok)
}
wg.Done() // 对wg的副本调用Done()
}
func main() {
var wg sync.WaitGroup
wg.Add(2) // 期望等待两个goroutine
c := make(chan int)
go push(c, wg) // 传递wg的副本
go pull(c, wg) // 传递wg的副本
wg.Wait() // 主goroutine等待原始wg
close(c) // 通常在所有生产者完成后关闭channel
}当运行上述代码时,程序会输出部分结果,然后抛出死锁错误:
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语言中结构体(sync.WaitGroup是一个结构体)的默认传递方式是值传递。
为了解决这个问题,我们需要确保所有goroutine操作的是同一个WaitGroup实例。在Go语言中,实现这一目标的方法是通过指针传递WaitGroup。当传递指针时,我们传递的是内存地址,所有操作都会作用于该地址指向的同一个WaitGroup对象。
立即学习“go语言免费学习笔记(深入)”;
以下是修正后的代码示例:
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 < 5; i++ {
c <- i
}
// 在push完成后,我们通常会关闭channel,但这里为了演示WaitGroup,暂时不在push中关闭
// 如果需要关闭,应该在所有生产者完成后,且由一个专门的goroutine或主goroutine来完成
}
// pull函数现在接收一个*sync.WaitGroup指针
func pull(c chan int, wg *sync.WaitGroup) {
defer wg.Done() // 使用defer确保在函数退出前调用Done()
for i := 0; i < 5; i++ {
result, ok := <-c
if !ok { // 检查channel是否关闭
fmt.Println("Channel closed, no more data.")
break
}
fmt.Println(result, ok)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2) // 期望等待两个goroutine
c := make(chan int)
// 传递wg的地址(指针)给goroutine
go push(c, &wg)
go pull(c, &wg)
wg.Wait() // 主goroutine等待原始wg
close(c) // 所有goroutine完成后关闭channel,通知消费者
fmt.Println("All goroutines finished and channel closed.")
// 为了确保pull goroutine能接收到channel关闭信号并退出,
// 我们需要给pull goroutine足够的时间处理完所有数据并接收到关闭信号。
// 在实际应用中,pull goroutine通常会在channel关闭后自动退出其循环。
// 这里的例子中,由于pull循环次数固定,且push完成后channel才关闭,
// pull可能在channel关闭前就已经完成并调用了Done()。
// 更好的做法是,让pull goroutine循环直到channel关闭。
}代码改进说明:
运行修正后的代码,将不再出现死锁,程序会正常执行并退出。
理解Go语言的值传递机制以及并发原语的正确使用方式,对于编写健壮、高效的并发程序至关重要。避免WaitGroup的值传递陷阱是Go并发编程中的一个基础且关键的知识点。
以上就是Go语言中WaitGroup死锁:值传递陷阱与正确用法的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号