
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 <- value 会阻塞,直到有另一个Goroutine从该通道接收数据;同样,接收操作 <- c 也会阻塞,直到有另一个Goroutine向该通道发送数据。
在上述示例中,main Goroutine首先调用 sum(allNums[:len(allNums)/2], c1)。在 sum 函数内部,当执行到 c <- sum 这一行时,由于 c1 是一个无缓冲通道,且当前没有任何Goroutine正在从 c1 读取数据,因此 sum 函数(以及调用它的 main Goroutine)会被阻塞。由于 main Goroutine被阻塞,它无法继续执行到 a := <- c1 这一行来读取数据,从而形成了经典的死锁:发送方在等待接收方,而接收方(在 main Goroutine的后续代码中)永远无法到达。第二个 sum 函数的调用甚至不会被执行到,因为第一个 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}
// 创建带缓冲的通道,缓冲区大小为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 <- sum 时,只要通道的缓冲区未满,就可以将数据写入缓冲区并立即返回,而不会阻塞。当缓冲区满时,发送操作才会阻塞。由于我们只发送一个值,缓冲区大小为1足以避免死锁。main Goroutine可以顺序调用两个 sum 函数,将结果存入各自的缓冲通道,然后继续执行接收操作。
注意事项: 使用带缓冲通道虽然可以解决死锁,但需要谨慎选择缓冲区大小。过小的缓冲区可能仍然导致阻塞,而过大的缓冲区可能占用过多内存,并可能掩盖设计上的并发问题。通常,带缓冲通道适用于生产者-消费者模式中,当生产速度和消费速度不匹配时作为缓冲队列。
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 := <- c1 和 b := <- c2 时,两个 sum Goroutine可能已经在后台计算并将结果发送到了 c1 和 c2。
如果 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号