
在 go 语言中,利用 goroutine 和 channel 实现并发任务处理是常见的模式。例如,将一个大型数组分成多个部分,由不同的 goroutine 并发计算各部分的和,最后通过 channel 汇总结果。
考虑以下场景:一个整数数组 a 需要计算总和。我们将其分成两部分,并启动两个 Goroutine 分别计算这两部分的总和,然后将结果发送到一个共享的 Channel 中。主 Goroutine 从 Channel 接收这两个结果并相加,得到最终的总和。
初始实现代码如下:
package main
import (
"fmt"
)
// Add 函数计算切片a中所有元素的和,并将结果发送到res通道。
func Add(a []int, res chan<- int) {
sum := 0
for _, v := range a {
sum += v
}
res <- sum // 将计算结果发送到通道
}
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7}
n := len(a)
ch := make(chan int) // 创建一个无缓冲通道
// 启动两个Goroutine并发计算
go Add(a[:n/2], ch)
go Add(a[n/2:], ch)
sum := 0
// 尝试使用range循环从通道接收数据
for s := range ch {
sum += s
}
// close(ch) // 初始代码中此处被注释或缺失
fmt.Println(sum)
}上述代码在运行时会发生死锁。其根本原因在于主 Goroutine 中 for s := range ch 语句的阻塞行为,以及通道 ch 从未被关闭。
为了解决这个问题,通常需要确保在所有发送操作完成后,通道会被关闭。然而,在有多个发送方的情况下,确定由哪个发送方来关闭通道是一个复杂且容易出错的问题(例如,过早关闭或重复关闭会导致 panic)。
当不确定何时关闭通道,或者有多个发送方时,一种更健壮的方法是不关闭通道,而是通过其他机制来判断何时停止接收。这里介绍一种基于计数器的解决方案,它通过跟踪已完成的 Goroutine 数量来管理接收过程。
核心思路是:
修正后的代码示例:
package main
import (
"fmt"
)
// Add 函数计算切片a中所有元素的和,并将结果发送到res通道。
func Add(a []int, res chan<- int) {
sum := 0
for _, v := range a {
sum += v
}
res <- sum // 将计算结果发送到通道
}
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7}
n := len(a)
ch := make(chan int) // 创建一个无缓冲通道
// 启动两个Goroutine并发计算
go Add(a[:n/2], ch)
go Add(a[n/2:], ch)
sum := 0
count := 0 // 初始化计数器,用于跟踪已接收的结果数量
// 循环接收数据,直到接收到预期的所有结果(这里是2个)
for count < 2 {
s := <-ch // 从通道接收一个值
sum += s
count++ // 递增计数器
}
// 当count达到2时,循环结束,所有预期结果都已接收
fmt.Println(sum)
}在修正后的 main 函数中:
运行此代码,将正确输出 28 (1+2+3+4+5+6+7)。
选择合适的通道接收方式:
通道关闭的风险:
sync.WaitGroup 的应用: 对于更复杂的并发场景,sync.WaitGroup 是一个更通用的同步原语,用于等待一组 Goroutine 完成。它可以与计数器方法结合使用,或单独用于确保所有工作 Goroutine 都已完成,然后再进行最终结果的汇总或通道关闭(如果需要)。例如:
// ... (Add 函数不变)
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7}
n := len(a)
ch := make(chan int)
var wg sync.WaitGroup // 引入WaitGroup
wg.Add(2) // 告知WaitGroup有两个Goroutine要等待
go func() {
defer wg.Done() // Goroutine完成时调用Done
Add(a[:n/2], ch)
}()
go func() {
defer wg.Done() // Goroutine完成时调用Done
Add(a[n/2:], ch)
}()
// 启动一个Goroutine来关闭通道,避免主Goroutine阻塞
go func() {
wg.Wait() // 等待所有Add Goroutine完成
close(ch) // 所有发送方完成后关闭通道
}()
sum := 0
for s := range ch { // 现在可以安全地使用range循环
sum += s
}
fmt.Println(sum)
}这种 sync.WaitGroup 配合 close(ch) 的模式在多发送方场景中更为常见,它将关闭通道的责任从发送方转移到一个专门的 Goroutine,并在所有发送方完成后执行关闭。
在 Go 语言并发编程中,理解 Channel 的工作原理,特别是 for range 循环对通道关闭的依赖,对于避免死锁至关重要。当有多个发送方时,直接在发送方中判断并关闭通道是困难且危险的。此时,采用基于计数器或 sync.WaitGroup 的策略来协调 Goroutine 的完成和通道数据的接收,是更安全和健壮的实践。这确保了程序在所有并发任务完成后能够正确地汇总结果并优雅地终止。
以上就是解决 Go 并发求和中的通道死锁:Range 与计数器方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号