channel:从理解源码到应用
“不要通过共享内存来通信,而要通过通信来实现内存共享”
channel 的底层就是通过 mutex 来控制并发的。只是 channel 是更高一层次的并发编程原语,封装了更多的功能。
使用原子函数、读写锁可以保证资源的共享访问安全,但使用 channel 更优雅
lock 用来保证每个读 channel 或写 channel 的操作都是原子的
为什么要channel
Go 通过 channel 实现 CSP 通信模型,主要用于 goroutine 之间的消息传递和事件通知。
有了 channel 和 goroutine 之后,Go 的并发编程变得异常容易和安全,得以让程序员把注意力留到业务上去,实现开发效率的提升。
数据结构
type hchan struct {
qcount uint // total data in the queue 当前队列(chan)中的元素个数
dataqsiz uint // size of the circular queue 环形队列容量
buf unsafe.Pointer // points to an array of dataqsiz elements 指针,指向底层环形队列存储(数组)
elemsize uint16 // chan中元素大小 // c.elemsize = uint16(elem.size)
closed uint32 // 是否关闭
elemtype *_type // element type channel容器内的元素类型
sendx uint // send index // 已发送元素在循环数组中的索引
recvx uint // receive index // 已接收元素在循环数组中的索引
recvq waitq // list of recv waiters // 等待接收的 goroutine 队列(双向链表)
sendq waitq // list of send waiters // 等待发送的 goroutine 队列(双向链表)
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex // 锁
}
解释:
buf指向底层数组,缓冲型channel会分配数组,非缓冲型数组则不分配。
sendx,recvx指向底层循环数组下标,表示当前可以发送和接受元素索引值,也就是环形队列中的队首和队尾。
waitq是元素为sudog的一个双向链表,而sudog是对goroutine的一个封装
lock用来保证每个读channel或写channel的操作都是原子的。
例如,现在有一个容量为 6 的,元素为 int 型的 channel ,其数据结构如下 :

解释:
qcount(4)表示队列中有4个元素
dataqsiz(6)表示队列的容量为6
buf指向底层数组(环形队列)
elemsize(8)表示channel容器中的元素类型大小。比如对于chan int,其值为8
创建channel
channel按是否带缓冲分类
Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作“同步模式”,带缓冲的则称为“异步模式”。
不带缓冲的channel(同步模式)
同步模式下,发送方和接收方要同步就绪,只有在两者都 ready 的情况下,数据才能在两者间传输(后面会看到,实际上就是内存拷贝)。否则,任意一方先行进行发送或接收操作,都会被挂起,等待另一方的出现才能被唤醒。
带缓冲的channel(异步模式)
异步模式下,在缓冲槽可用的情况下(有剩余容量),发送和接收操作都可以顺利进行。否则,操作的一方(如写入)同样会被挂起,直到出现相反操作(如接收)才会被唤醒。
小结一下:同步模式下,必须要使发送方和接收方配对,操作才会成功,否则会被阻塞;异步模式下,缓冲槽要有剩余容量,操作才会成功,否则也会被阻塞。
channel按读写分类
只读channel
对于<-chan类型来说,表示这个chan只有读权限
只写channel
小结一下:我们可以通过设置channel的读写权限来明确每个函数的身份(是消费者还是生产者,或者既是生产者又是消费者),同时可以有效避免只读成员发生误写操作、只写成员消费数据。
创建一个channel
我们可以通过make来创建一个channel,如var ch = make(chan int)。那么使用make创建一个channel都发生了什么呢?
通过查看汇编代码发现,是通过调用makechan来创建channel的,我们来看看makechan长什么样子吧。
从makechan中我们得到下面信息:
chan支持最大容量为
(1<<16) - 1对于无缓冲类型chan,不会去申请buf数组
我们传入的size(chan的容量),最终决定底层环形数组的长度
接收
接收值函数重载
从一个channel中获取值有两种形式
通过反汇编处理,发现分别调用的是下面两种函数
其实都是调用chanrecv
从recvchan中我们得知:
在非阻塞模式下并且channel没有被关闭。如果是非缓冲型channel,等待发送列队 sendq 里没有 goroutine 在等待,又或者是缓冲型channel,但 buf 里没有元素。select将无法从中获得数据,selected == false,这里的
received也为false会是代表channel已经被关闭了?其实不是,那只是在recv也就是<- ch场景下我们可以这样认为,这也是为什么在上面解释的原因。channel已经关闭,并且里面已经没有元素,我们仍然可以从中获得元素(得到的是channel容器元素中类型的零值)
如果等待发送队列中有goroutine存在。如果是非缓冲型channel,那么直接将其拷贝到接收者的栈;如果是缓冲型channel,那么将弹出队首元素,将其添加到队尾。
如果缓冲型channel中有元素,那么对于"val <- ch"这种方式的接收则会通过
typedmemmove进行拷贝,然后回收环形队列的队首。如果channel处于阻塞状态并且等待发送队列中不存在goroutine,那么当前go routine将会被挂起,直到有数据被send进channel而被唤醒。
发送
和分析<- ch一样,我们通过查看汇编代码得知ch <-最后调用的是chansend函数。
发送数据会有以下情况:
如果channel为nil,在非阻塞模式下会提示发送失败(return false),在阻塞模式下当前goroutine则会被挂起
在非阻塞模式下,如果发送数据给非缓冲型channel或者是元素已满的缓冲型channel,则会提示发送失败。
如果当前channel已经被关闭,当尝试向其发送数据时会panic
如果等待接收队列里有goroutine,直接将要发送的数据拷贝到接收goroutine。(当等待接收队列里有goroutine的时候说明,该channel是非缓冲型channel或者是元素为空的缓冲型channel)
如果是缓冲型channel并且还有缓冲空间,那么将数据拷贝到buf数组中(环形队列),提示发送成功
如果是缓冲型channel并且已经满了,那么将被挂起,直到被唤醒。
关闭
关闭channel,会执行函数closechan
从代码得知:
如果我们关闭一个
nil channel会panic如果我们关闭一个非打开状态的channel,会panic
成功关闭一个channel的话。如果等待接收队列里还有goroutine那么将会被释放;如果等待发送队列里还有goroutine将会被释放,只是这些goroutine都是因为panic而异常退出的。
channel异常总结

关闭已经关闭的channel也会引发panic
channel的应用
停止信号
任务定时
延时任务
定期任务
解耦生产方和消费方
控制并发
内存泄露
“通道可能会引发goroutine leak,确切地说,是指goroutine处于发送或接收阻塞状态,但一直未被唤醒。垃圾回收器并不收集此类资源,导致它们会在等待队列里长久休眠,形成资源泄漏。”
摘录来自: 雨痕. Go语言学习笔记。
参考
雨痕. Go语言学习笔记
最后更新于
这有帮助吗?