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    // 锁
}
img

解释:

buf指向底层数组,缓冲型channel会分配数组,非缓冲型数组则不分配。

sendxrecvx指向底层循环数组下标,表示当前可以发送和接受元素索引值,也就是环形队列中的队首和队尾。

waitq是元素为sudog的一个双向链表,而sudog是对goroutine的一个封装

lock用来保证每个读channel或写channel的操作都是原子的。

例如,现在有一个容量为 6 的,元素为 int 型的 channel ,其数据结构如下 :

img

解释:

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中我们得到下面信息:

  1. chan支持最大容量为(1<<16) - 1

  2. 对于无缓冲类型chan,不会去申请buf数组

  3. 我们传入的size(chan的容量),最终决定底层环形数组的长度

接收

接收值函数重载

从一个channel中获取值有两种形式

通过反汇编处理,发现分别调用的是下面两种函数

其实都是调用chanrecv

recvchan中我们得知:

  1. 在非阻塞模式下并且channel没有被关闭。如果是非缓冲型channel,等待发送列队 sendq 里没有 goroutine 在等待,又或者是缓冲型channel,但 buf 里没有元素。select将无法从中获得数据,selected == false,这里的received也为false会是代表channel已经被关闭了?其实不是,那只是在recv也就是<- ch场景下我们可以这样认为,这也是为什么在上面解释的原因。

  2. channel已经关闭,并且里面已经没有元素,我们仍然可以从中获得元素(得到的是channel容器元素中类型的零值)

  3. 如果等待发送队列中有goroutine存在。如果是非缓冲型channel,那么直接将其拷贝到接收者的栈;如果是缓冲型channel,那么将弹出队首元素,将其添加到队尾。

  4. 如果缓冲型channel中有元素,那么对于"val <- ch"这种方式的接收则会通过typedmemmove进行拷贝,然后回收环形队列的队首。

  5. 如果channel处于阻塞状态并且等待发送队列中不存在goroutine,那么当前go routine将会被挂起,直到有数据被send进channel而被唤醒。

发送

和分析<- ch一样,我们通过查看汇编代码得知ch <-最后调用的是chansend函数。

发送数据会有以下情况:

  1. 如果channel为nil,在非阻塞模式下会提示发送失败(return false),在阻塞模式下当前goroutine则会被挂起

  2. 在非阻塞模式下,如果发送数据给非缓冲型channel或者是元素已满的缓冲型channel,则会提示发送失败。

  3. 如果当前channel已经被关闭,当尝试向其发送数据时会panic

  4. 如果等待接收队列里有goroutine,直接将要发送的数据拷贝到接收goroutine。(当等待接收队列里有goroutine的时候说明,该channel是非缓冲型channel或者是元素为空的缓冲型channel)

  5. 如果是缓冲型channel并且还有缓冲空间,那么将数据拷贝到buf数组中(环形队列),提示发送成功

  6. 如果是缓冲型channel并且已经满了,那么将被挂起,直到被唤醒。

关闭

关闭channel,会执行函数closechan

从代码得知:

  1. 如果我们关闭一个nil channel会panic

  2. 如果我们关闭一个非打开状态的channel,会panic

  3. 成功关闭一个channel的话。如果等待接收队列里还有goroutine那么将会被释放;如果等待发送队列里还有goroutine将会被释放,只是这些goroutine都是因为panic而异常退出的。

channel异常总结

channel异常总结

关闭已经关闭的channel也会引发panic

channel的应用

停止信号

任务定时

延时任务

定期任务

解耦生产方和消费方

控制并发

内存泄露

“通道可能会引发goroutine leak,确切地说,是指goroutine处于发送或接收阻塞状态,但一直未被唤醒。垃圾回收器并不收集此类资源,导致它们会在等待队列里长久休眠,形成资源泄漏。”

摘录来自: 雨痕. Go语言学习笔记。

参考

深度解密Go语言之channelarrow-up-right

Golang 源码导读 —— channelarrow-up-right

我可能并不会使用golang chanarrow-up-right

Go语言基础之并发arrow-up-right

雨痕. Go语言学习笔记

最后更新于

这有帮助吗?