• Golangchannel底层实现精要


    Golang-channel底层实现精要

    一.channel 背景知识

    • channel是Go语言内置的核心类型,可以将其看做一个管道,channel和goroutine一起为go并发编程提供了最优雅和便利的方案
    • 在Go中有一句经典名言,永远不要通过共享内存来通信,而是要通过通信来共享内存,channel便是用于实现goroutine间通信的
    • channel提供了三种类型
      • 单向只能发送:chan<- struct{} 只能发送struct (箭头指向channel,则代表发送)
      • 单向只能接收:<-chan struct{} 只能从chan里接收struct (箭头远离channel,则代表接收)
      • 双向即可发送也可接收:chan string 既能接收也能发送
    • nil是channel的零值,对值是nil的channel发送和接收总是会阻塞

    二.channel 底层实现

    1.channel底层结构

    简要说明:

    • buf是带缓冲的channle所特有的结构,是个循环链表,用来存储缓存数据
    • sendxrecvx是用于记录buf中发送和接收的index
    • lock是个互斥锁,目的是为了保证goroutine以先进先出FIFO的方式进入结构体
    • recvqsendq分别是往channel接收或发送数据的goroutine所抽象出来的数据结构,是个双向链表

    channel结构体的源码位于/runtime/chan.go中,结构体为hchan,源码如下(版本1.11)

    type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters
    
    // 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
    }
    

    2.channel实现原理

    1) channel 创建

    ch := make(chan int, 3)
    

    因为 channel 的创建全部调用的 mallocgc(),在堆上开辟的内存空间,所以channel 本身会被 GC 自动回收。回收的条件是没有goroutine引用

    简要说明:

    • 创建channel实际上就是在内存中实例化了一个hchan结构体,并返回一个chan指针
    • channle在函数间传递都是使用的这个指针,这就是为什么函数传递中无需使用channel的指针,而是直接用channel就行了,因为channel本身就是一个指针

    2) channel 发送数据

    以有缓冲的channel为例

    ch <- 1
    ch <- 2
    ch <- 3
    

    简要说明:

    • 发送数据前,会先锁住hchan这个结构体
    • 然后逐步往buf中填充数据(从goroutine中copy数据到buf),然后解锁
    • 注意sendx的变化,其记录了发送数据的index

    3) channel 接收数据

    <-ch
    <-ch
    <-ch
    

    简要说明:

    • 接收数据前,同样会先锁住hchan这个结构体
    • 然后逐步往buf中获取数据(buf中copy数据到goroutine),然后解锁
    • 注意recvx的变化,其记录了接收数据的index

    4) channel存储满了,底层如何处理的?

    我们都知道,当channle缓存满了的时候,会阻塞当前goroutine,但是,这是如何实现的呢?

    • goroutine的阻塞操作,实际上是调用send (ch <- xx)或者recv ( <-ch)主动触发的
    //goroutine1 中,记做G1
    ch := make(chan int, 3)
    ch <- 1
    ch <- 1
    ch <- 1
    

    这个时候,G1在正常运行,当再次调度send操作的时候,会主动调用Go的调度器,让当前协程G1等待,并且让出内核线程M,交给其他G使用

    同时,G1也会被抽象成含有G1指针和send元素的sudog结构体,保存到*sendq中等待被唤醒,那G1什么时候被唤醒呢?在有其他协程(G2)接收数据后被唤醒

    G2执行了recv,于是会发生以下操作:

    • 1)G2从buf中取出数据
    • 2)channel从sendq中推出G1,将G1当时的send数据推到buf中
    • 3)调用Go的调度器scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中

    5) channel是空的,底层如何处理的?

    当channel中无数据时,先执行G2的接收数据操作,G2会阻塞,这又是如何实现的呢?其实跟上面相差不大,可以顺着思路反推

    • 1)G2首先会主动调用Go调度器,让G2等待,并且让出M,交给其他G使用
    • 2)然后G2还好被抽象成含有G2指针和recv空元素的sudog结构体,保存到recvq中等待被唤醒

    此时,如果G1向channel中发送数据,会发生一个有意思的事情:

    • G1并没有锁住channel,然后将数据放入buf中,而是直接将数据从G1 copy到了 G2,这种方式非常好
    • 这样的话,在唤醒G2的过程中,G2无需再获得channel的锁,然后从buf中取数据,减少了内存cpoy,提高了效率

    • 通过Go的调度器唤醒G2,将G2加入到GPM模型中P的本地可运行G队列中

    3.总结

    • channel缓冲器满或空,其底层的处理都非常的精妙,主动调用调度器,阻塞当前G,将M交给其他G使用,然后将G指针和其他数据组装成sudog,加入recvq或者sendq队列,等待被调度,
    • 唤醒的流程也非常有趣,当G2接收但channel空阻塞时,G1发送数据,采用了直接copy方式,并没有锁住channel,将数据放入buf,而是直接从G1 复制到G2,减少了内存copy

    本文参考:

    心之所向,素履以往
  • 相关阅读:
    202020211 20209326 《Linux内核原理与分析》第一周作业
    指针入门,以及利用指针简单的数组逆置
    函数入门
    C语言关于处理数组元素的插入、删除、排序
    flag用法之一
    css3中的transform值matrix
    HTML5CANVAS做的打砖块游戏。。。
    用curl抓取网站数据,仿造IP、防屏蔽终极强悍解决方式
    javascript中最好用的extend继承方法
    使用CSS3中Media Queries兼容不同设备
  • 原文地址:https://www.cnblogs.com/yinbiao/p/15767096.html
Copyright © 2020-2023  润新知