
在 go 语言中,通道(channel)是实现协程(goroutine)间通信的重要机制。然而,不当使用通道,尤其是无缓冲通道,很容易导致程序死锁。考虑以下计算自然数和的 go 程序示例:
package main
import "fmt"
func sum(nums []int, c chan int) {
var sum int = 0
for _, v := range nums {
sum += v
}
c <- sum // 将结果发送到通道
}
func main() {
allNums := []int{1, 2, 3, 4, 5, 6, 7, 8}
c1 := make(chan int) // 创建无缓冲通道
c2 := make(chan int) // 创建无缓冲通道
sum(allNums[:len(allNums)/2], c1) // 直接调用 sum 函数
sum(allNums[len(allNums)/2:], c2) // 直接调用 sum 函数
a := <-c1 // 从通道接收数据
b := <-c2 // 从通道接收数据
fmt.Printf("%d + %d is %d :D", a, b, a+b)
}运行这段代码会产生以下死锁错误:
throw: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.sum(0x44213af00, 0x800000004, 0x420fbaa0, 0x2f29f, 0x7aaa8, ...)
main.go:9 +0x6e
main.main()
main.go:16 +0xe6
goroutine 2 [syscall]:
created by runtime.main
/usr/local/go/src/pkg/runtime/proc.c:221
exit status 2这个死锁的根本原因在于 sum 函数被直接调用,而不是在一个独立的 Goroutine 中运行。当执行到 sum(allNums[:len(allNums)/2], c1) 这一行时,sum 函数会在当前(即 main)Goroutine 中执行。在 sum 函数内部,c <- sum 尝试将计算结果发送到通道 c。由于 c1 是一个无缓冲通道,它要求发送和接收操作必须同时准备就绪才能完成。此时,main Goroutine 正在 sum 函数内部执行发送操作,但还没有执行到 a := <-c1 这一行来接收数据。因此,发送操作会一直阻塞,导致 main Goroutine 停止。
由于 main Goroutine 阻塞,程序无法继续执行到第二个 sum 函数调用或任何通道接收操作。最终,Go 运行时检测到所有 Goroutine(包括 main Goroutine)都处于阻塞状态,没有 Goroutine 可以继续执行,从而报告死锁。尽管使用了两个独立的通道 c1 和 c2,但问题在于 sum 函数的同步调用模式,导致 main Goroutine 无法同时扮演发送者和接收者的角色。
解决上述死锁问题的一种方法是使用带缓冲的通道。带缓冲的通道允许在没有接收者准备就绪的情况下,发送一定数量的数据到通道中,直到缓冲区满。
修改 main 函数中通道的创建方式:
package main
import "fmt"
func sum(nums []int, c chan int) {
var sum int = 0
for _, v := range nums {
sum += v
}
c <- sum // 将结果发送到通道
}
func main() {
allNums := []int{1, 2, 3, 4, 5, 6, 7, 8}
c1 := make(chan int, 1) // 创建一个容量为1的缓冲通道
c2 := make(chan int, 1) // 创建一个容量为1的缓冲通道
sum(allNums[:len(allNums)/2], c1) // 直接调用 sum 函数
sum(allNums[len(allNums)/2:], c2) // 直接调用 sum 函数
a := <-c1 // 从通道接收数据
b := <-c2 // 从通道接收数据
fmt.Printf("%d + %d is %d :D", a, b, a+b)
}通过将通道 c1 和 c2 创建为容量为 1 的缓冲通道 (make(chan int, 1)),sum 函数中的 c <- sum 操作将不再立即阻塞。它会将计算结果放入通道的缓冲区中,然后立即返回,允许 main Goroutine 继续执行。这样,main Goroutine 可以依次调用两个 sum 函数,并将结果放入各自的缓冲通道。之后,main Goroutine 再从这两个通道中接收数据,从而避免了死锁。
注意事项: 使用缓冲通道时,需要仔细考虑缓冲区的容量。如果发送的数据量超过缓冲区容量,发送操作仍然会阻塞。对于本例,每个 sum 函数只发送一个整数,因此容量为 1 的缓冲区足以解决问题。
更符合 Go 语言并发编程范式,也是解决此类问题的推荐方法,是将 sum 函数的调用封装到独立的 Goroutine 中。这将使得 sum 函数与 main 函数并发执行,从而确保在 sum 函数尝试发送数据时,main 函数能够及时准备好接收数据。
修改 main 函数中 sum 函数的调用方式:
package main
import "fmt"
func sum(nums []int, c chan int) {
var sum int = 0
for _, v := range nums {
sum += v
}
c <- sum // 将结果发送到通道
}
func main() {
allNums := []int{1, 2, 3, 4, 5, 6, 7, 8}
c1 := make(chan int) // 保持无缓冲通道
c2 := make(chan int) // 保持无缓冲通道
go sum(allNums[:len(allNums)/2], c1) // 在新的 Goroutine 中运行
go sum(allNums[len(allNums)/2:], c2) // 在新的 Goroutine 中运行
a := <-c1 // 从通道接收数据
b := <-c2 // 从通道接收数据
fmt.Printf("%d + %d is %d :D", a, b, a+b)
}在此方案中,我们保留了无缓冲通道。关键的改变在于 go sum(...) 的使用。当 go sum(...) 被调用时,Go 运行时会启动一个新的 Goroutine 来执行 sum 函数,而 main Goroutine 会立即继续执行下一行代码。这意味着:
此时,两个 sum Goroutine 正在并行计算它们的子和,并将结果发送到 c1 和 c2。当其中一个 sum Goroutine 完成计算并执行 c <- sum 时,main Goroutine 已经在等待从该通道接收数据,或者很快就会开始等待。由于发送和接收操作能够同时准备就绪,无缓冲通道的阻塞条件被满足,数据得以顺利传输,从而避免了死锁。
理解 Go 语言中通道的缓冲特性和 Goroutine 的并发执行是避免死锁的关键。
在上述示例中,使用 Goroutine 来并发执行 sum 函数是更符合 Go 语言并发哲学的做法。它清晰地表达了“计算子和是独立的任务,可以并行进行”的意图,并通过通道安全地将结果传递回主逻辑。虽然使用缓冲通道也能解决特定场景下的死锁,但它通常用于流量控制或解耦,而不是作为替代 Goroutine 实现并发执行的主要手段。
因此,在设计并发程序时,应优先考虑使用 Goroutine 来启动并发任务,并根据同步需求和数据流特征选择合适的通道类型(无缓冲或带缓冲)。
以上就是解决 Go 语言中无缓冲通道导致的死锁问题的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号