彻底搞懂 Channel 实现原理

webff · · 2245 次点击 · 开始浏览    置顶

![ go-channel.png](https://static.golangjob.cn/230117/cd046dde911340bb39a54f84b0c19691.png) 最近大家私信我让我说说 Go 语言中的 Channel,年末了,有的人已经开始准备面试,真快呀!今天我们就来说说 Channel吗,日常开发中使用也是比较频繁的,面试也是高频。听我慢慢说来。 Channel (通道) 是 Go 语言高性能并发编程中的核心数据结构和与 Goroutine 之前重要的通信方式。在 Go 语言中通道是一种特殊的类型。通道像一个传送带或者队列,遵循先入先出(First In First Out)的规则,保证收发数据的顺序。 ### 1. 应用场景 在很多主流的编程语言中,多个线程间基本上都是通过共享内存来实现通信的,如Java。这类语言往往都需要限制一定的线程数量从而解决线程竞争。用图的方式简单表达一下。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/216e14da269a4bf79fff7fd4df4a2a14~tplv-k3u1fbpfcp-zoom-1.image) Go 语言的设计却截然不同,在 Go 语言提供了一种新的并发模型,在 Goroutine 中使用 Channel 传递数据,从而实现通信。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/95f92ba294a342adb2330bdf569d9660~tplv-k3u1fbpfcp-zoom-1.image) Go 语言提倡 **“不要通过共享内存的方式进行通信,而是通过 Channel 通信的方式共享内存”。** Don’t communicate by sharing memory, share memory by communicating 我们会结合 chanal 使用场景的 5 大类型来阐述,更好的了解 Channel。 - 数据交流 - 数据传递 - 信号通知 - 任务编排 - 锁 接下来学一下一下 chanel 的常见用法。 ### 2. 常见用法 我们一开始就说 Go 语言是通过通信来实现共享内存的,故我们可以从 channel 中接受数据,也能发送数据。下文中会简称 channek 为 chan。我们将从一下三种情况展开说下 - 仅接送数据 - 仅发送数据 - 既能接受也能发送数据 ``` chan int // 可以发送和接收 int 数据 chan <- struct{} // 只能发送 struct{} <-chan string // 只能从 chan 接收 string 数据 ``` 声明的通道类型变量需要使用内置的 make 函数初始化之后才能使用。格式如下: ``` make(chan 元素类型, [缓冲区大小]) make(chan int) // 无缓冲通道 make(chan int, 1024) // 有缓冲通道 ``` 记住 Go 语言中 chan 没有类型的限制,其中 chan 的缓冲大小是可选的,未初始化的是一个 nil 值。 - 指定缓冲区的大小,我们称其为 **“** **缓冲通道”** 。 - 未指定了缓冲区的大小,我们称其为 **“** **无缓冲通道”** 又称为阻塞的通道。 无缓冲通道 无缓冲的通道又称为阻塞的通道,如上方第 3 行代码,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。 缓冲通道 当制定了缓冲区的大小,初始化如上方第 4 行代码。若 chan 中有数据时,此时从 chan 中接收数据不会发生阻塞;若 chan 未满时,此时发送数据也不会发生阻塞,反之就会出现阻塞。 接下来说下 Channel 的基本用法。 #### 2.1 发送数据 将一个值发送到通道(chan) 中: ``` chan <- 1024 // 将1024 发到 chan 中 ``` #### 2.2 接收数据 从一个通道(chan) 中接收值: x := <- ch // 从ch中接收值并赋值给变量x <-ch // 从ch中接收值,并丢弃 #### 2.3 关闭通道 我们通过内置函数 close 函数来关闭通道: close(chan) #### 2.4 其他操作 Go 一些内置的函数都可以操作 chan 类型。比如 len、cap - len 可以返回 chan 中还未被处理的元素数量 - cap 可以返回 chan 的容量 **注:** 目前 Go 语言中并没有提供一个不对通道进行读取操作就能判断通道 chan 是否被关闭的方法。不能简单的通过 len(ch) 操作来判断通道 chan 是否被关闭。 用 for range 接收值: ``` func f(ch chan int) { for v := range ch { fmt.Println("接收到 chan 值:", v) } } ``` 还有 send 和 recv 可以作为 select 语句的 case: ``` var ch = make(chan int, 6) for i := 0; i < 6; i++ { select { case ch <- i: case v := <-ch: fmt.Println(v) } } ``` 下面我说下源码的角度分析一下 chan 的具体实现,掌握了原理,我们才能真正地用好它,才能在谈高薪是有底气! ### 3. 实现原理 #### 3.1 chan 数据结构 Go 语言的 Channel 在运行时使用 [runtime.hchan](https://draveness.me/golang/tree/runtime.hchan) 结构体表示。我们在 Go 语言中创建新的 Channel 时,实际上创建的都是如下所示的结构: ``` type hchan struct { qcount uint // 循环队列元素的数量 dataqsiz uint // 循环队列的大小 buf unsafe.Pointer // 循环队列缓冲区的数据指针 elemsize uint16 // chan中元素的大小 closed uint32 // 是否已close elemtype *_type // chan 中元素类型 sendx uint // send 发送操作在 buf 中的位置 recvx uint // recv 接收操作在 buf 中的位置 recvq waitq // receiver的等待队列 sendq waitq // senderl的等待队列 lock mutex // 互斥锁,保护所有字段 } ``` `runtime.hchan` 结构体中的五个字段 qcount、dataqsiz、buf、sendx、recv 构建底层的循环队列 (channel)。解释一下上面的字段含义: - qcount:代表循环队列 chan 中已经接收但还没被取走的元素的个数。 - datagsiz 循环队列 chan 的大小。选用了一个循环队列来存放元素,类似于队列的生产者 - 消费者场景 - buf:存放元素的循环队列的 buffer。 - elemtype 和 elemsize:循环队列 chan 中元素的类型和 size。chan 一旦声明,它的元素类型是固定的,即普通类型或者指针类型,元素大小自然也就固定了。 - sendx:处理发送数据的指针在 buf 中的位置。一旦接收了新的数据,指针就会加上 elemsize,移向下一个位置。buf 的总大小是 elemsize 的整数倍,而且 buf 是一个循环列表。 - recvx:处理接收请求时的指针在 buf 中的位置。一旦取出数据,指针会移动到下一个位置。 - recvq:chan 是多生产者多消费者的模式,如果消费者因为没有数据可读而被阻塞了,就会被加入到 recvq 队列中。 - sendq:如果生产者因为 buf 满了而阻塞,会被加入到 sendq 队列中。 #### 3.2 发送数据(send) 我们接下来继续介绍 chan 的接收数据。Go 语言中可以使用 ch <- i 向 chan 中发送数据。 我们看下 [chansend](https://github.com/golang/go/blob/41d8e61a6b9d8f9db912626eb2bbc535e929fefc/src/runtime/chan.go#L158) 源码,Go 编译器在向 chan 发送数据时,会将 send 转换成 chansend1 函数。如下: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00aaad0a7d1d454fa1131d3078ba6dc0~tplv-k3u1fbpfcp-zoom-1.image) chansend1 中调用 chansend 并传入 channel 和需要发送的数据。一开始会判断当前 chan 是否为 nil ,是 nil 会阻塞调用者 gopark。我们会发现第 11 行是不会被程序执行的。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5c3eb7cc36464d028491107eb9080bf8~tplv-k3u1fbpfcp-zoom-1.image) 当 chan 关闭了,此时发送数据会造成 panic 错误。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df616aa972ff4cf2ac4e7181be52f325~tplv-k3u1fbpfcp-zoom-1.image) 如果 chan 没有被关闭并且等待队列中已经有处于读等待的 Goroutine,那么会从接收队列 recvq 中取出最先陷入等待的 Goroutine 并直接向它发送数据。 如果创建的 chan 包含缓冲区(chanbuf)并且 chan 中的数据没有装满,会执行下面这段代码: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e8fa063939ee479d935e95e24850caca~tplv-k3u1fbpfcp-zoom-1.image) 在这里我们首先会使用缓冲区中计算出下一个可以存储数据的位置,然后通过 typedmemmove 将发送的数据拷贝到缓冲区中并增加 sendx 索引和 qcount 计数器。 #### 3.3 接收数据(recv) 接下来继续介绍 chan 的接收数据。Go 语言中可以使用两种不同的方式去接收 chan 中的数据: ``` i <- ch i, ok <- ch ``` 从 chan 中接收数据会被转换成 chanrecv1 和 chanrecv2 两种函数,但是最后还是会调用 chanrecv。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6249c8537dad4f65b9e85aef90074413~tplv-k3u1fbpfcp-zoom-1.image) 可以看到 chanrecv1 和 chanrecv2 中调用 chanrecv 时 block 的值都是 true,在 chanrecv 中 chan 为 nil ,我们从 nil 的 chan 中接收数据,调用者会被阻塞主 goroutine park,和发送一样,第 15 行也不会被执行。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e43c4a0178414016834d3201a7e318e6~tplv-k3u1fbpfcp-zoom-1.image) 当缓冲区中没有数据且当前的 chan 已经 close 了,那么会清除 ep 指针中的数据,代码段会返回 true、false。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/896a72a932774686b8dae2d2e0180d2e~tplv-k3u1fbpfcp-zoom-1.image) 当缓冲区满了,这个时候,如果是 unbuffer 的 chan,就直接将 sender 的数据复制给 receiver,否则就取出队列头等待的 Goroutine,并把这个 sender 的值加入到队列尾部。 #### 3.4 关闭(close) Go 语言中关闭一个 chan 用自带的 close 函数,编译器会转成调用 closechan,我们看下源码: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea5812ffe8354c579763a7cea3884a1c~tplv-k3u1fbpfcp-zoom-1.image) - close 一个 nil 的 chan 会出现 panic; - close 一个 已经 closed 的 chan,会出现 panic; - 当 chan 不为 nil 也不为 closed,才能 close 成功,从而把等待队列中的 sender(writer)和 receiver(reader)从队列中全部移除并唤醒。 源码的部分就到这里了,我们接下来说下开发中需要注意的点。 ### 4. 总结 我们开发中,chan 的值或者状态会有很多种情况,此时一定要注意使用方式,一些操作可能会出现 panic。我总结了一下异常场景,如下表: | | nil channel | 有值 channel | 没值 channel | 满 channel | | ------------------- | ----------- | ---------- | ---------- | --------- | | <- ch (发送数据) | 阻塞 | 发送成功 | 发送成功 | 阻塞 | | ch <- (接收数据) | 阻塞 | 接收成功 | 阻塞 | 接收成功 | | close(ch) 关闭channel | panic | 关闭成功 | 关闭成功 | 关闭成功| 欢迎点赞关注,感谢! ![微信.png](https://static.golangjob.cn/230117/e6d7567719c440eb84c31be78d9b8159.png)

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2245 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传