Go channel 实现原理分析 - 简书 - 图1

22019.10.01 17:23:43 字数 1,120 阅读 8,408

channel 一个类型管道,通过它可以在 goroutine 之间发送和接收消息。它是 Golang 在语言层面提供的 goroutine 间的通信方式。Go 依赖于成为 CSP 的并发模型,通过 Channel 实现这种同步模式。Golang 并发的核心哲学是不要通过共享内存进行通信。

下面 Go 通过 channel 来实现通信例子:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func goRoutineA(a <-chan int) {
  7. val := <-a
  8. fmt.Println("goRoutineA received the data", val)
  9. }
  10. func goRoutineB(b chan int) {
  11. val := <-b
  12. fmt.Println("goRoutineB received the data", val)
  13. }
  14. func main() {
  15. ch := make(chan int, 3)
  16. go goRoutineA(ch)
  17. go goRoutineB(ch)
  18. ch <- 3
  19. time.Sleep(time.Second * 1)
  20. }

终端显示结果:

  1. ~$ goRoutineA recv the data 3

上面的例子只输出 goRoutineA 信息,没有执行 goRoutineB 说明 channel 仅允许被一个 goroutine 读写。

下面通过源码程序执行过程分析,如果对 go 并发和调度相关知识不了解,可以预览这里

首先我们看下通道的结构 hchan, 源码再 src/runtime/chan.go 下

  1. type hchan struct {
  2. qcount uint
  3. dataqsiz uint
  4. buf unsafe.Pointer
  5. elemsize uint16
  6. closed uint32
  7. elemtype *_type
  8. sendx uint
  9. recvx uint
  10. recvq waitq
  11. sendq waitq
  12. lock mutex
  13. }
  14. type waitq struct {
  15. first *sudog
  16. last *sudog
  17. }

创建两种 channel 类型,一个带缓冲区和一个不带缓冲区的 channel

  1. ch:=make(chan int, 3)
  2. ch:=make(chan int)

带缓冲区


创建通道后的缓冲通道结构

  1. hchan struct {
  2. qcount uint : 0
  3. dataqsiz uint : 3
  4. buf unsafe.Pointer : 0xc00007e0e0
  5. elemsize uint16 : 8
  6. closed uint32 : 0
  7. elemtype *runtime._type : &{
  8. size:8
  9. ptrdata:0
  10. hash:4149441018
  11. tflag:7
  12. align:8
  13. fieldalign:8
  14. kind:130
  15. alg:0x55cdf0
  16. gcdata:0x4d61b4
  17. str:1055
  18. ptrToThis:45152
  19. }
  20. sendx uint : 0
  21. recvx uint : 0
  22. recvq runtime.waitq :
  23. {first:<nil> last:<nil>}
  24. sendq runtime.waitq :
  25. {first:<nil> last:<nil>}
  26. lock runtime.mutex :
  27. {key:0}
  28. }

源码在 $GOPATH/src/runtime/chan.go 下:

  1. func makechan(t *chantype, size int) *hchan {
  2. elem := t.elem
  3. ...
  4. }

创建一个带有 buffer 的 channel,底层的数据结构模型如图:

Go channel 实现原理分析 - 简书 - 图2

image.png

向 channel 中写入数据

底层 hchan 数据流程下如图:

Go channel 实现原理分析 - 简书 - 图3

image.png

Go channel 实现原理分析 - 简书 - 图4

image.png

发送操作步骤:

  • 锁定整个通道结构
  • 确定写入。城市 recvq 从等待队列中等待 goroutine,然后将元素直接写入 goroutine
  • 如果 recvq 为 Empty,则确定缓冲区是否可用,如果可用那么从当前 goroutine 复制数据到缓冲区中。
  • 如果缓冲区已经满了,则要写入的元素将保存在当前执行的 goroutine 结构中,并且当前 goroutine 在 sendq 中并且队列从运行时挂起。
  • 写入完成释放锁
  • 需要注意的接个属性的变化:buf、sendx、lock

执行流程图如下:

Go channel 实现原理分析 - 简书 - 图5

image.png

从 channel 中读取数据

从 channel 中读取数据操作几乎和写入操作雷同

  1. func goRoutineA(a <-chan int){
  2. val := <- a
  3. fmt.Println("goRoutineA received the data",val)
  4. }

底层 hchan 数据流转如下图:

Go channel 实现原理分析 - 简书 - 图6

image.png

Go channel 实现原理分析 - 简书 - 图7

image.png

  • 需要注意的几个属性变化 buf、sendx、recvx、lock

读数据操作如下:

  • 先获取 channel 全局锁
  • 尝试 sendq 等待队列中获取等待的 goroutine
  • 如果有等待的 goroutine,没有缓冲区,取出 goroutine 并读取数据,然后唤醒这个 goroutine,结束读取释放锁
  • 如果有等待 goroutine,且有缓冲区 (缓冲区满了),从缓冲区队列首取数据,再从 sendq 取出一个 goroutine,将 goroutine 中的数据存放到 buf 队列尾,结束读取释放锁。
  • 如果没有等待的 goroutine,且缓冲区有数据,直接读取缓冲区数据,结束释放锁。
  • 如果没有等待的 goroutine,且没有缓冲区或者缓冲区为空,将当前 goroutine 加入到 sendq 队列,进入睡眠,等待被写入 goroutine 唤醒,结束读取释放锁。

大概流程如下:

Go channel 实现原理分析 - 简书 - 图8

image.png

recvq 和 sendq 结构


recvq 和 sendq 基本上是链表,基本如下:

Go channel 实现原理分析 - 简书 - 图9

image.png

select

select 就是用来监听和 channel 有关的 IO 操作,当前 IO 操作发生触发相关动作执行

如下例子:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func goRoutineD(ch chan int, i int) {
  7. time.Sleep(time.Second * 3)
  8. ch <- i
  9. }
  10. func goRoutineE(chs chan string, i string) {
  11. time.Sleep(time.Second * 3)
  12. chs <- i
  13. }
  14. func main() {
  15. ch := make(chan int, 5)
  16. chs := make(chan string, 5)
  17. go goRoutineD(ch, 5)
  18. go goRoutineE(chs, "ok")
  19. select {
  20. case msg := <-ch:
  21. fmt.Println(" received the data ", msg)
  22. case msgs := <-chs:
  23. fmt.Println(" received the data ", msgs)
  24. }
  25. }

多次执行后的结果如下:

  1. received the data 5
  2. received the data ok
  3. received the data ok
  4. received the data ok

select 语句会阻塞,知道监测到一个可执行的 IO 操作为止,goRoutineD 和 goRoutineE 睡眠时间相同,都是 3s,从输出可以看出,从 channel 中读取数据顺序是随机的。

range


可以持续冲 channel 中读取数据,一直到 channel 被关闭,当 channel 中没有数据是会阻塞当前 goroutine,这里阻塞和读 channel 时阻塞处理机制一样。

例子如下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func goRoutineD(ch chan int, i int) {
  7. for i := 1; i <= 5; i++{
  8. time.Sleep(time.Second * 1)
  9. ch <- i
  10. }
  11. }
  12. func chanRange(chanName chan int) {
  13. for e := range chanName {
  14. fmt.Printf("Get element from chan: %d\n", e)
  15. if len(chanName) <= 0 {
  16. break
  17. }
  18. }
  19. }
  20. func main() {
  21. ch := make(chan int, 5)
  22. go goRoutineD(ch, 5)
  23. chanRange(ch)
  24. }

运行结果如下:

  1. Get element from chan: 1
  2. Get element from chan: 2
  3. Get element from chan: 3
  4. Get element from chan: 4
  5. Get element from chan: 5

死锁 (deadlock)


死锁是指两个或者两个以上的协程在执行任务过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象。在非缓冲信道如发生只流入不流出或者只流入出不流入就会发生死锁

死锁例子如下:

  1. package main
  2. func main(){
  3. ch:=make(chan int)
  4. ch <- 3
  5. }

向非缓冲区通道读取数据会发生阻塞导致死锁,解决办法开启缓冲区,先向 channel 中写入数据

  1. package main
  2. import(
  3. "fmt"
  4. )
  5. func main(){
  6. ch:=make(chan int)
  7. fmt.Println(<-ch)
  8. }

写入数据超过缓冲区数量也会发生死锁,解决办法将写入数据取走

  1. package main
  2. func main(){
  3. ch:=make(chan int,3)
  4. ch <- 3
  5. ch <- 4
  6. ch <- 5
  7. ch <- 6
  8. }

向关闭的 channel 写入数据。解决办法别向关闭的 channel 写入数据。

  1. package main
  2. func main(){
  3. ch:=make(chan int,3)
  4. ch <- 1
  5. close(ch)
  6. ch <- 2
  7. }

可以参考更多死锁例子:

更多精彩内容,就在简书 APP

“小礼物走一走,来简书关注我”

还没有人赞赏,支持一下

Go channel 实现原理分析 - 简书 - 图10

被以下专题收入,发现更多相似内容

推荐阅读更多精彩内容

  • 本文翻译自 Channels In Go Channel 是 Go 中一个重要的内置功能。这是让 Go 独一无二的功能之一,除…

  • 11.1 概述 11.1.1 并行和并发 并行 (parallel):指在同一时刻,有多条指令在多个处理器上同时执行…
    Go channel 实现原理分析 - 简书 - 图11
    小黑胖_
    阅读 948 评论 0 赞 7
    Go channel 实现原理分析 - 简书 - 图12

  • goroutine 并行 (parallel):指在同一时刻,有多条指令在多个处理器上同时执行。并发 (concurr…
    Go channel 实现原理分析 - 简书 - 图13

  • Chapter 8 Goroutines and Channels Go enable two styles of…

  • 咏,哥,留给世界一个鲜明的背影,飘然远去。 乍一看到消息,我一笑,是恶心人开的玩笑吧,网上这一类的消息还少么? 再…
    Go channel 实现原理分析 - 简书 - 图14
    雪地飞狐
    阅读 156 评论 0 赞 2
    Go channel 实现原理分析 - 简书 - 图15

  • 亲爱的利娟 当我看到你今天换位思考。 我觉得你是一个智慧并且有执行力的人。 让我感到高兴的是付出爱就会得到爱,加油…

  • 1,相互依存:我意识到我当时选择学习教练课程就是放弃了部分的短期利益而考虑长远益处所做的决定,从这点上看,我觉得当…
    https://www.jianshu.com/p/d841f251d3bc