
go语言以其内置的并发原语——goroutine和channel——而闻名,它们为编写高效且易于维护的并发程序提供了强大的支持。然而,如果不正确地理解和使用这些原语,特别是通道(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函数
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)
}运行上述代码,程序会抛出 all goroutines are asleep - deadlock! 的错误。其根本原因在于Go语言中通道的默认行为:当使用 make(chan int) 创建一个无缓冲通道时,发送操作 c
在上述示例中,main Goroutine首先调用 sum(allNums[:len(allNums)/2], c1)。在 sum 函数内部,当执行到 c
解决上述死锁问题的一种直接方法是为通道添加缓冲区。带缓冲的通道允许在没有并发接收者的情况下,向通道发送有限数量的数据,而不会立即阻塞。
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}
// 创建带缓冲的通道,缓冲区大小为1
c1 := make(chan int, 1)
c2 := make(chan int, 1)
sum(allNums[:len(allNums)/2], c1)
sum(allNums[len(allNums)/2:], c2)
a := <- c1
b := <- c2
fmt.Printf("%d + %d is %d :D", a, b, a + b)
}在此修改中,c1 := make(chan int, 1) 创建了一个缓冲区大小为1的通道。这意味着 sum 函数在执行 c
注意事项: 使用带缓冲通道虽然可以解决死锁,但需要谨慎选择缓冲区大小。过小的缓冲区可能仍然导致阻塞,而过大的缓冲区可能占用过多内存,并可能掩盖设计上的并发问题。通常,带缓冲通道适用于生产者-消费者模式中,当生产速度和消费速度不匹配时作为缓冲队列。
Go语言中处理并发的更惯用和推荐的方式是将独立的并发任务封装到Goroutine中运行。这样,main Goroutine可以启动其他Goroutine,而不会被它们的执行阻塞,从而允许并发的发送和接收操作。
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函数作为独立的Goroutine运行
go sum(allNums[:len(allNums)/2], c1)
go sum(allNums[len(allNums)/2:], c2)
// main Goroutine现在可以并发地从通道接收数据
a := <- c1
b := <- c2
fmt.Printf("%d + %d is %d :D", a, b, a + b)
}在这个版本中,go sum(...) 语句会启动一个新的Goroutine来执行 sum 函数。main Goroutine会立即继续执行下一行代码,而不会等待 sum 函数完成。这意味着当 main Goroutine到达 a :=
如果 sum Goroutine先发送数据,而 main Goroutine尚未到达接收点,那么:
无论哪种情况,由于 main Goroutine和 sum Goroutine现在是并发执行的,它们可以互相配合完成发送和接收操作,从而避免了死锁。这种方式是Go语言中实现并发协作的典型模式,它利用了Goroutine的轻量级特性和通道的同步机制。
package main
import "fmt"
// sum 函数计算整数切片的和,并将结果发送到通道
func sum(nums []int, c chan int) {
total := 0
for _, v := range nums {
total += v
}
c <- total // 将计算结果发送到通道
}
func main() {
allNums := []int{1, 2, 3, 4, 5, 6, 7, 8}
// 创建两个无缓冲通道,以上就是理解Go通道死锁:无缓冲通道的陷阱与并发解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号