
go语言以其内置的并发原语——goroutine和channel而闻名。goroutine是轻量级的线程,而channel则提供了goroutine之间安全通信的机制。通道允许数据在goroutine之间传递,从而避免了传统共享内存并发模型中常见的竞态条件。然而,不当的通道使用方式,特别是对通道的初始化和生命周期管理不当,可能导致程序陷入死锁。
死锁是指两个或多个Goroutine在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们将永远无法继续执行。在Go语言中,最常见的死锁情景之一就是向一个未初始化的(nil)通道发送数据,或者从一个未初始化的(nil)通道接收数据。
考虑以下Go语言代码片段,它尝试利用多个Goroutine并行计算一个复数切片中子切片的最大幅值及其索引:
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() {
ansSlice := make([]complex128, 128) // 示例数据
numberOfSlices := 4
incr := len(ansSlice) / numberOfSlices
// 问题所在:创建通道切片,但通道本身未初始化
tmp_val := make([]chan float64, numberOfSlices)
tmp_index := make([]chan int, numberOfSlices)
for i, j := 0, 0; i < len(ansSlice); j++ {
fmt.Printf("From %d to %d - %d\n", i, i+incr, len(ansSlice))
// 启动Goroutine,并尝试向 tmp_val[j] 和 tmp_index[j] 发送数据
go max(ansSlice[i:i+incr], i, tmp_val[j], tmp_index[j])
i = i + incr
}
// 主Goroutine尝试从通道接收数据
// ... 此处会发生死锁,因为发送方和接收方都在等待nil通道
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)
}运行上述代码,会发现程序在Goroutine尝试向通道发送数据时,或者主Goroutine尝试从通道接收数据时,会立即陷入死锁并报错:fatal error: all goroutines are asleep - deadlock!。
造成死锁的根本原因在于通道的初始化方式。在Go语言中,通道是一种引用类型,其零值为nil。当使用make([]chan float64, numberOfSlices)这样的语句来创建一个通道切片时,实际上是创建了一个包含numberOfSlices个nil通道的切片。切片中的每个元素都指向通道类型的零值,即nil。
立即学习“go语言免费学习笔记(深入)”;
Go语言对nil通道有特殊的行为规定:
在上述示例代码中,当max Goroutine被启动时,它接收到的是tmp_val[j]和tmp_index[j],而这些在循环外部创建的切片元素默认都是nil通道。因此,当max Goroutine尝试执行ans <- maxi或index <- base+maxi_i时,它实际上是在向一个nil通道发送数据,这会导致该Goroutine永久阻塞。同样,主Goroutine尝试从tmp_index[0]和tmp_val[0]接收数据时,也会因为这些是nil通道而永久阻塞。所有Goroutine都阻塞,没有Goroutine能够继续执行,从而导致了死锁。
要解决这个问题,必须在将通道传递给Goroutine之前,对切片中的每个通道进行单独的初始化。使用make(chan Type)可以创建一个可用的、非nil的通道实例。
修改后的代码如下:
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() {
ansSlice := make([]complex1128, 128) // 示例数据
numberOfSlices := 4
incr := len(ansSlice) / numberOfSlices
tmp_val := make([]chan float64, numberOfSlices)
tmp_index := make([]chan int, numberOfSlices)
for i, j := 0, 0; j < numberOfSlices; j++ { // 循环 numberOfSlices 次
// 关键修正:在循环内部初始化每个通道
tmp_val[j] = make(chan float64)
tmp_index[j] = make(chan int)
fmt.Printf("From %d to %d - %d\n", i, i+incr, len(ansSlice))
go max(ansSlice[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)
}在修正后的代码中,我们在for循环内部为tmp_val和tmp_index切片中的每个元素分别调用了make(chan Type)。这样,每个Goroutine都会收到一个有效的、可用于发送和接收数据的通道实例,从而避免了死锁。
本教程通过一个具体的Go语言死锁案例,深入剖析了未初始化(nil)通道的危害及其导致死锁的机制。核心要点是:在Go语言中,使用make([]chan Type, size)创建的通道切片,其内部元素默认为nil通道,而非可用的通道实例。向nil通道发送或从nil通道接收都会导致永久阻塞,进而引发死锁。 解决之道在于始终通过make(chan Type)显式地初始化每个通道实例,确保它们在被使用前是有效的。理解并遵循这些通道使用原则,是编写健壮、高效Go并发程序的关键。
以上就是Go语言中通道死锁的常见陷阱:理解并避免nil通道的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号