深入剖析 Go 语言中的 Channel:高级特性与注意事项
一、channel 的基本原理
chan 类型的数据结构如下图所示:
下面我们先来具体解释各个字段的含义:
qcount:代表 chan 中已经接收但还没被取走的元素的个数。内建函数 len 可以返回这个字段的值。dataqsiz:队列的大小。chan 使用一个循环队列来存放元素,循环队列很适合这种生产者 - 消费者的场景(我很好奇为什么这个字段省略 size 中的 e)。
buf:存放元素的循环队列的 buffer。elemtype 和 elemsize:chan 中元素的类型和 size。因为 chan 一旦声明,它的元素类型是固定的,即普通类型或者指针类型,所以元素大小也是固定的。
sendx:处理发送数据的指针在 buf 中的位置。一旦接收了新的数据,指针就会加上 elemsize,移向下一个位置。buf 的总大小是 elemsize 的整数倍,而且 buf 是一个循环列表。
recvx:处理接收请求时的指针在 buf 中的位置。一旦取出数据,此指针会移动到下一个位置。
recvq:chan 是多生产者多消费者的模式,如果消费者因为没有数据可读而被阻塞了,就会被加入到 recvq 队列中。
sendq:如果生产者因为 buf 满了而阻塞,会被加入到 sendq 队列中。
二、channel 的高级特性
1.双向 Channel 和单向 Channel
在使用 Channel 时,我们可以将其定义为双向 Channel 或者单向 Channel。双向 Channel 可以进行发送和接收操作,而单向 Channel 只能进行发送或者接收操作。
双向 Channel 的声明方式如下:
单向 Channel 的声明方式如下:
2.带缓冲区的 Channel 和无缓冲区的 Channel
在创建 Channel 时,我们可以指定缓冲区的大小。如果缓冲区大小为 0,则代表该 Channel 没有缓冲区,也就是无缓冲区的 Channel。如果缓冲区大小大于 0,则代表该 Channel 是带缓冲区的 Channel。
带缓冲区的 Channel 可以在缓冲区未满时进行发送操作,而无缓冲区的 Channel 只能在接收操作完成前进行发送操作,否则会阻塞当前 goroutine。
3.Range 遍历 Channel
在 Go 语言中,我们可以使用 for range 语句来遍历 Channel,这种方式可以避免手动进行循环和判断 Channel 是否关闭的操作。
语法如下:
当 Channel 被关闭后,for range 就会自动退出循环。
4.Channel 的选择器
在使用多个 Channel 进行数据交换时,我们可以使用 Channel 的选择器来实现非阻塞式的数据交换。选择器会等待多个 Channel 中的一个可用,并执行相应的操作,如果多个 Channel 同时可用,则随机选择一个进行操作。
选择器的语法如下:
5.Channel 的超时机制
在使用 Channel 进行数据交换时,我们可以使用带有超时机制的操作来避免 goroutine 的长时间阻塞。在 Go 语言中,我们可以使用 time 包中的 Timer 和 Ticker 来实现超时机制。
Timer 用于等待一段时间后执行某个操作,而 Ticker 则用于每隔一段时间执行某个操作。
Timer 的语法如下:
Ticker 的语法如下:
三、使用 Channel 容易犯的错误
首先,我们来总结下会 panic 的情况,总共有 3 种:
close 为 nil 的 chan;
send 已经 close 的 chan;
close 已经 close 的 chan。
goroutine 泄漏的问题也很常见,下面的代码也是一个实际项目中的例子:
在这个例子中,process 函数会启动一个 goroutine,去处理需要长时间处理的业务,处理完之后,会发送 true 到 chan 中,目的是通知其它等待的 goroutine,可以继续处理了。
我们来看一下第 10 行到第 15 行,主 goroutine 接收到任务处理完成的通知,或者超时后就返回了。
如果发生超时,process 函数就返回了,这就会导致 unbuffered 的 chan 从来就没有被读取。我们知道,unbuffered chan 必须等 reader 和 writer 都准备好了才能交流,否则就会阻塞。
超时导致未读,结果就是子 goroutine 就阻塞在第 7 行永远结束不了,进而导致 goroutine 泄漏。解决这个 Bug 的办法很简单,就是将 unbuffered chan 改成容量为 1 的 chan,这样第 7 行就不会被阻塞了。
一般而言,有几个原则可以记住:
共享资源的并发访问使用传统并发原语;
复杂的任务编排和消息传递使用 Channel;
消息通知机制使用 Channel,除非只想 signal 一个 goroutine,才使用 Cond;
简单等待所有任务的完成用 WaitGroup
评论