今天咱们总结的对象是:通道Channel!
一、通道是什么?
作为Go语言最有特色的数据类型,通道(channel)完全可以与协程(goroutine)并驾齐驱,共同代表 Go 语言独有的并发编程模式CSP(Communicating Sequential Processes)和编程哲学。
Don’t communicate by sharing memory; share memory by communicating.
不要通过共享内存来通信,而应该通过通信来共享内存。
通道类型完美诠释了这个编程理念的后半句:通过通信来共享内存!通过通道,我们可以在多个 goroutine 之间传递数据。
当然,高并发是Go最具有自身特色的特性,所以我们将在第六章用一章的篇幅来详细总结高并发原理,今天这篇我们只看作为数据类型本身而言,通道的一些特性和需要注意的知识点。
通道(channel)是一种特殊的引用类型(零值为nil),它本身就是并发安全的(Go语言自带的唯一一个满足并发安全性的类型)。每个channel都有一个特殊的类型,也就是channel可发送数据的类型。
//声明一个传递布尔型的通道
//声明的通道后需要使用make函数初始化之后才能使用
var ch1 chan bool
// 创建一个int类型的通道
ch2 := make(chan int)
// 创建一个string切片类型带有缓冲的通道,通道容量为5
ch3 := make(chan []string, 5)
二、通道的常见操作
作为goroutine之间的数据“沟通通道”,那可以想象到对于通道来说,一共有三种操作:发送(send)、接收(receive)和关闭(close)!另外对于有缓冲通道(下面讲到),还有两个常见操作获取元素数量(len)和通道容量(cap),平时使用场景非常少(因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助)。
- 发送:发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
- 接收:接收就是从一个通道中接收值,可以付给变量,也可以忽略
- 关闭:使用内置的close函数可以关闭一个channel。对基于已关闭的channel的任何发送和再关闭操作都将导致panic异常,但是依旧可以接收,会接收到零值。另外,和关闭文件不同,通道关闭是可选的,因为通道是可以被垃圾回收机制回收的
- 获取通道内元素数量:使用内置的len函数
- 获取通道容量:使用内置的cap函数
//发送
ch <- x
// 接收
x = <-ch
// 接收,但忽略接收到的值
<-ch
// 关闭
close(ch)
//判断通道关闭的方法
//通道关闭后再取值ok=false
//但是这个值会有延迟,比如通道还有数据但已经关闭时,ok还是true
i, ok := <-ch1
if !ok {
……
}
//通道关闭后会退出for range循环
for i := range ch2 {
fmt.Println(i)
}
一个通道相当于一个先进先出(FIFO)的队列。可以看到上面的接送操作符<-的方向完美的展示出了“消息的去向”。
对于通道的基本操作,有以下基本特性:
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的
- 发送操作和接收操作中对元素值的处理都是不可分割的
- 发送操作在完全完成之前会被阻塞。接收操作也是如此
需要强调的是,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本,所以就是两步操作,复制元素副本,把副本放到通道里。而从通道出来时,实际上也有两步操作:第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。上面特性的第二条也就是在强调这“发送”和“接收”里面的“子操作”都是一气呵成不可打断的。
三、通道分类
- 无缓冲通道:又称为阻塞的通道,或同步通道,因为使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。就像在没有快递柜之前,快递员必须打电话把快递包裹亲手交给你一样,是必须同步进行的。
- 有缓冲的通道:和无缓冲通道不同的是,它有一个可以缓冲指定大小(称为容量)的空间“暂时存放”要传递的值。这时候就好比小区里安装了一个快递柜,能存放100个包裹,假如还有空位,快递小哥就会电话告诉你把包裹放在几号柜子里,你有时间来取就可以了。
- 单向通道:被限制了只能发送或接收的通道。主要是用来约束其他代码的行为。在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
上面代码中chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。
这里有一个优良实践,有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短,但是空结构体带来的性能开销更小。
四、通道引起阻塞
了解了通道操作的基本特性之后我们可以发现,基于这些特性,在对通道进行操作的时候就可能会产生代码阻塞。那都在哪些场景下可能会产生长时间的代码阻塞呢?
正常使用通道过程中可能产生的阻塞:
- 对于有缓冲通道:
- 通道已满:对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走
- 通道已空:对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现
- 对于无缓冲通道:无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。因为它用的是“同步”方式传递数据(想想上面接收快递的场景)。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。
func main() {
pipline := make(chan string)
pipline <- "hello world"
fmt.Println(<-pipline)
}
//fatal error: all goroutines are asleep - deadlock!
//上面的代码就是无缓冲通道,在接受者未准备好的情况下就发送会造成死锁
//解决方法可以是先让接收者执行
//当然也可以换成有缓冲通道
func main() {
pipline := make(chan string)
fmt.Println(<-pipline)
pipline <- "hello world"
}
错误使用通道过程中可能产生的阻塞:
- 对于值为nil的通道:对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。所以一定要记得用make初始化通道之后在使用!
五、通道引起panic
对通道的操作何时会引起panic,我们特别做一下总结:
- 通道已关闭:对它的发送和关闭操作,都会引起panic
对于通道类型的大致内容就总结到这里,你都Get了吗?