
go 语言通过 goroutine 和 channel 提供了强大的并发编程模型。goroutine 是一种轻量级线程,而 channel 则是 goroutine 之间进行通信和同步的主要方式。通道允许数据在 goroutine 之间安全地传递,遵循“不要通过共享内存来通信,而是通过通信来共享内存”的设计哲学。通道可以是无缓冲的(发送和接收必须同时就绪)或有缓冲的(可以存储一定数量的数据)。
在 Go 语言中,通道是一种引用类型,就像切片、映射和接口一样。这意味着通道变量可以为 nil。一个 nil 通道在并发编程中具有非常特殊的行为,也是导致死锁的常见陷阱:
问题代码中,开发者试图创建一个通道切片来管理多个 Goroutine 的结果:
tmp_val := make([]chan float64, numberOfSlices) tmp_index := make([]chan int, numberOfSlices)
这里的关键在于 make([]chan float64, numberOfSlices) 的行为。它创建了一个长度为 numberOfSlices 的切片,其元素类型是 chan float64。然而,它并没有为切片中的每个通道元素进行初始化。由于通道是引用类型,这些元素在创建时会被其类型的零值填充,对于通道类型来说,零值就是 nil。
因此,tmp_val 和 tmp_index 切片中的每一个元素都是一个 nil 通道。
随后,在循环中启动 Goroutine 时:
go max(ans[i:i+incr],i,tmp_val[j],tmp_index[j])
每个 max Goroutine 都会尝试向 tmp_val[j] 和 tmp_index[j] 发送数据。由于这些通道都是 nil,所有的发送操作都将立即永久阻塞。
同时,在 main Goroutine 中,主程序也尝试从这些 nil 通道接收数据:
maximumFreq := <- tmp_index[0] maximumMax := <- tmp_val[0] // ... tmpI := <- tmp_index[i] tmpV := <- tmp_val[i]
这些接收操作同样会永久阻塞,因为它们试图从 nil 通道接收。最终,程序中所有的 Goroutine(包括 main Goroutine 和所有 max Goroutine)都处于阻塞状态,没有 Goroutine 可以继续执行,Go 运行时会检测到这种情况并报告死锁(all goroutines are asleep - deadlock!)。
解决此问题的核心在于确保每个通道在使用前都已正确初始化。这意味着在创建通道切片后,需要遍历切片,为每个索引位置的通道单独调用 make 函数进行初始化。
正确的做法是在循环中为每个通道分配内存并初始化:
package main
import (
"fmt"
"math/cmplx"
)
func max(a []complex128, base int, ans chan float64, index chan int) {
fmt.Printf("called for %d,%d\n", len(a), base)
maxi_i := 0
maxi := cmplx.Abs(a[maxi_i])
for i := 1; i < len(a); i++ {
if cmplx.Abs(a[i]) > maxi {
maxi_i = i
maxi = cmplx.Abs(a[i])
}
}
fmt.Printf("called for %d,%d and found %f %d\n", len(a), base, maxi, base+maxi_i)
ans <- maxi
index <- base + maxi_i
}
func main() {
ans := make([]complex128, 128) // 示例数据,实际应用中可能填充有意义的值
numberOfSlices := 4
incr := len(ans) / numberOfSlices
// 正确初始化通道切片中的每一个通道
tmp_val := make([]chan float64, numberOfSlices)
tmp_index := make([]chan int, numberOfSlices)
for i := 0; i < numberOfSlices; i++ {
tmp_val[i] = make(chan float64) // 初始化为无缓冲通道
tmp_index[i] = make(chan int) // 初始化为无缓冲通道
}
for i, j := 0, 0; i < len(ans); j++ {
fmt.Printf("From %d to %d - %d\n", i, i+incr, len(ans))
// 将已初始化的通道传递给 Goroutine
go max(ans[i:i+incr], i, tmp_val[j], tmp_index[j])
i = i + incr
}
// 从通道接收结果
maximumFreq := <-tmp_index[0]
maximumMax := <-tmp_val[0]
for i := 1; i < numberOfSlices; i++ {
tmpI := <-tmp_index[i]
tmpV := <-tmp_val[i]
if tmpV > maximumMax {
maximumMax = tmpV
maximumFreq = tmpI
}
}
fmt.Printf("Max freq = %d\n", maximumFreq) // 添加换行符以确保输出完整
}通过 tmp_val[i] = make(chan float64) 这样的语句,我们为切片中的每个元素创建了一个非 nil 的、可用的无缓冲通道。现在,Goroutine 可以向这些通道发送数据,主 Goroutine 也可以从这些通道接收数据,从而避免了死锁。
为了避免类似的死锁问题,并编写健壮的 Go 并发程序,请遵循以下最佳实践:
空(nil)通道是 Go 并发编程中一个常见的陷阱,它会导致发送和接收操作永久阻塞,进而引发死锁。核心原因在于 make([]chan T, N) 仅仅创建了一个切片,其中的通道元素默认是 nil。正确的做法是,在创建通道切片后,通过循环为切片中的每个索引位置独立地调用 make(chan T) 进行初始化。理解并遵循通道的初始化规则和行为,是编写高效、健壮 Go 并发程序的基石。
以上就是Go 并发编程:理解空(nil)通道与死锁的根源的详细内容,更多请关注php中文网其它相关文章!
编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号