Go Chanel 使用与原理 一

访问已经关闭的 chanel

// exp3 用来测试访问一个已经关闭的且里面还有值未取出的 chanel 会发生什么事?
// 结果是先取出 chanel 里面的值,之后返回零值
func exp3(){
    sigCh:= make(chan string)
    ch:=make (chan int,2)
    ch <- 3
    ch <- 4
    close(ch)

    go func (){
        for i:=0;i<6;i++ {
            fmt.Println("b goroutine recieve: " ,<-ch)
        }
        sigCh <-"over"
    }()
    <-sigCh
}

控制台结果如图:

《Go Chanel 使用与原理 一》

往已经关闭的 chanel 写入数据

// exp4 用来测试往一个已经关闭的 chanel 写入数据会发生什么事?
// 结果是发生 panic :send on closed channel
func exp4 (){
    sigCh:= make(chan string)
    ch:=make (chan int,2)
    ch <- 3
    ch <- 4
    close(ch)

    go func (){
        ch <- 6
        sigCh <-"over"
    }()
    <-sigCh
}

控制台结果如图:

《Go Chanel 使用与原理 一》

关闭已经关闭的 chanel

// exp5 关闭已经关闭的 chanel 会发生什么事?
// 结果是发生 panic : close of closed channel
func exp5 (){
    sigCh:= make(chan string)
    ch:=make (chan int,2)
    ch <- 3
    ch <- 4
    close(ch)

    go func (){
        close(ch)
        sigCh <-"over"
    }()
    <-sigCh
}

《Go Chanel 使用与原理 一》

chanel 底层实现

chanel 的数据结构

《Go Chanel 使用与原理 一》

  • 基于数组的循环队列,有缓冲的channel用它暂存数据
  • 基于链表的单向队列,用于保存阻塞在此channel上的goroutine

chanel 的创建

  • chanel 是分配在堆上面的
  • 使用 make 内建函数返回的就是指针,所以我们函数传参时不必使用指向 chanel 的指针

《Go Chanel 使用与原理 一》

chanel 的发送与接收

发送数据给 chanel

  1. acquire 获得锁
  2. dequeue 入队列,将数据拷贝到 buf 队列上
  3. release 释放锁

从 chanel 接收数据

  1. acquire 获得锁
  2. enqueue 出队列,拷贝 buf 队列的数据
  3. release 释放锁

没有共享内存(除了 hchan 对象)“不通过共享内存来通信,而是通过通信来共享内存”,而怎么通信呢?其实就是 copies。
《Go Chanel 使用与原理 一》

补充:运行时调用程序

goroutine 是一种用户级线程,由 Go 运行时系统来创建和管理,而不是操作系统。相较于操作系统的线程更加轻量级。一个 OS thread 对应对个 goroutine。这便是 Go 语言的 M:N 调度模型。
《Go Chanel 使用与原理 一》

MGP 模型,其中 P 为调度所需要的上下文资源,拥有运行队列。
《Go Chanel 使用与原理 一》

chanel 缓存满,发送方 goroutine 阻塞

当 G1 发送数据给已经满了的 chanel 时,会通过调用 gopark() 方法呼叫调度器将当前 goroutine(即 G1)设置为 waiting 状态。

《Go Chanel 使用与原理 一》
然后断开 G1 和 M 的联系

《Go Chanel 使用与原理 一》

调度下一条可运行的 goroutine
《Go Chanel 使用与原理 一》

chanel 缓存有空余,发送方 goroutine 唤醒

被阻塞的 发送方、接收方 goroutine 分别保存在 hchan 结构体的 sendq 、recvq 字段中。并且是封装为一个个 sudog 结构体对象保存的。
《Go Chanel 使用与原理 一》

G1 创建一个 sudog 结构体对象,然后将 sudog 放入 hchan 类型对象中的 sendq 队列上。
《Go Chanel 使用与原理 一》

接下来,当 G2 取走 chanel 中的数据,chanel 缓存有空余,G1 不再阻塞,但是是谁调用调度器将 G1 唤醒的呢?

《Go Chanel 使用与原理 一》
如上图,是 G2 ,通过调用 goready(G1) 方法呼叫调度器将 G1 唤醒。所以说“通过通信来共享内存”

《Go Chanel 使用与原理 一》

G1 往一个空的 chanel 接收数据导致阻塞

当 chanel 有数据不为空了,G1 并不是:

  1. 确认锁然后
  2. 取出 buf 数组中的元素
  3. goready(G2)

《Go Chanel 使用与原理 一》

而是有一种更聪明的办法,直接写到等待接受队列中的 G2
《Go Chanel 使用与原理 一》

《Go Chanel 使用与原理 一》

《Go Chanel 使用与原理 一》

总结

  • 使用锁保证并发安全
  • 采用 FIFO 先进先出策略存储
  • 以 sudog 结构体类型存储阻塞的 goroutine 作为队列节点
  • 通过 gopark,goready 来呼叫(calls into)运行时调度器

《Go Chanel 使用与原理 一》

关于无缓存 chanel ,如果接收方先阻塞,则发送方“直接写入”数据到接受方的栈结构中。
如果发送方先阻塞,则接收方直接从发送方的 sudog 结构中获取数据。
《Go Chanel 使用与原理 一》

引用文章出处:

https://speakerdeck.com/kavya…

https://segmentfault.com/a/11…

    原文作者:chencanxin
    原文地址: https://segmentfault.com/a/1190000018512084
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞