all goroutines are asleep - deadlock!

本文深入解析Go语言中通道通信的基本原理,包括同步无缓冲通信的特点,以及如何避免死锁问题。探讨了信号量模式下的通道使用,展示了如何通过缓冲通道实现异步通信,提升程序的伸缩性和响应能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

默认情况下,通信是同步且无缓冲的。这种特性导致通道的发送/接收操作,在对方准备好之前是阻塞的。

  • 对于同一个通道,发送操作在接收者准备好之前是阻塞的。如果通道中的数据无人接收,就无法再给通道传入其他数据。新的输入无法在通道非空的情况下传入,所以发送操作会等待channel再次变为可用状态,即通道值被接收后。
  • 对于同一个通道,接收操作是阻塞的,直到发送者可用。如果通道中没有数据,接收者就阻塞了。

示例一

package main

import "fmt"

func main() {
	ch1 := make(chan string)
	ch1 <- "hello world"
	fmt.Println(<-ch1)
}

执行后,会报这样的错误。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    D:/gotest/main.go:7 +0x5c
exit status 2
原因分析

在代码的第7行,我们给通道ch1中传入一个值"hello world"。但是根据我们之前讲的,对于同一无缓冲通道,在接收者未准备好之前,发送操作是阻塞的。而此处的通道ch1就是缺少一个配对的接收者,因此造成了死锁。
解决上面问题的方式有两种:第一种添加配对的接收者;第二种将默认的通道替换成缓冲通道。
方法一

package main

import "fmt"

func main() {
	ch1 := make(chan string)
	go func() {
		ch1 <- "hello world"
	}()
	fmt.Println(<-ch1)
}

在主函数中启用了一个goroutine,匿名函数用来发送数据,而在main()函数中接收通道中的数据。
方法二

package main

import "fmt"

func main() {
	ch1 := make(chan string, 1)
	ch1 <- "hello world"
	fmt.Println(<-ch1)
}

此时的ch1通道可以称为缓冲通道,在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。定义方法如:ch:=make(chan type, value)
这个value表示缓冲容量,它的大小和类型无关,所以可以给一些通道设置不同的容量,只要它们拥有相同的元素类型。
内置的cap函数可以返回缓冲区的容量,如果容量大于0,通道就是异步的了。缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是0或者未设置,通信仅在收发双方准备好的情况下才可以成功。

这种异步channel可以减少排队阻塞,在你的请求激增的时候表现得更好,更具伸缩性。

示例二

现在思考这样的问题,如果发送多个值,我们如何接收。例如:

package main

import (
	"fmt"
)

func main() {
	ch1 := make(chan string)
	go func() {
		fmt.Println(<-ch1)
	}()
	ch1 <- "hello world"
	ch1 <- "hello China"
}

上述代码执行后,结果为:

hello world
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    D:/gotest/main.go:13 +0x97

出现这样的结果是因为通道实际上是类型化消息的队列,它是先进先出(FIFO)的结构,可以保证发送给它们的元素的顺序。所以上面代码只取出了第一次传的值,即"hello world",而第二次传入的值没有一个配对的接收者来接收,因此就出现了deadlock。那么将代码变成这样,又会是什么结果呢?

package main

import "fmt"

func main() {
	ch1 := make(chan string)
	go func() {
		ch1 <- "hello world"
		ch1 <- "hello China"
	}()
	fmt.Println(<-ch1)
}

上面代码执行后结果为:hello world,但却没有报错,这又是为什么呢?

原因分析

信号量模式:协程通过在通道中放置一个值来处理结束的信号,main协程等待,直到从通道中获取到值。
上面的程序中有两个函数:main()函数和一个发送操作的匿名函数。它们按独立的处理单元按顺序启动,然后开始并行运行。通常情况下,由于main()函数不会等待其他非main协程的结束。但是此处的ch1相当于信号量,通过在ch1中放置一个值来处理结束的信号。main()协程等待<-ch,直到从中获取到值,然后程序直接退出。根本没有执行到继续往通道中传入"hello China",也就不会出现deadlock的出现。

示例三

package main

import "fmt"

func main() {
	ch1 := make(chan string)
	go func() {
		ch1 <- "hello world"
		ch1 <- "hello China"
	}()
	for {
		fmt.Println(<-ch1)
	}
}

执行后,会报这样的错误。

hello world
hello China
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    D:/gotest/main.go:12 +0x83
exit status 2

出现上面的结果是因为for循环一直在获取通道中的值,但是在读取完hello worldhello China后,通道中没有新的值传入,这样接收者就阻塞了。
那么如何判断通道是否阻塞,如何关闭通道,后面的文章将会陆续探讨。

参考文章

  1. 协程间的信道
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值