默认情况下,通信是同步且无缓冲的。这种特性导致通道的发送/接收操作,在对方准备好之前是阻塞的。
- 对于同一个通道,发送操作在接收者准备好之前是阻塞的。如果通道中的数据无人接收,就无法再给通道传入其他数据。新的输入无法在通道非空的情况下传入,所以发送操作会等待
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 world
和hello China
后,通道中没有新的值传入,这样接收者就阻塞了。
那么如何判断通道是否阻塞,如何关闭通道,后面的文章将会陆续探讨。
参考文章