Go中的channel怎么实现的???

码海映月使
• 阅读 1865

概述

相信大家在开发的过程中经常会使用到go中并发利器channelchannelCSP并发模型中最重要的一个组件,两个独立的并发实体通过共享的通讯channel进行通信。大多数人只是会用这么个结构很少有人讨论它底层实现,这篇文章讲写写channel的底层实现。

channel

channel的底层实现是一个结构体,源代码如下:

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
}

可能看源代码不是很好看得懂,这里我个人画了一张图方便大家查看,我在上面标注了不同颜色,并且注释其作用。

Go中的channel怎么实现的???

通道像一个传送带或者队列,总是遵循FIFO的规则,保证收发数据的顺序,通道是goroutine间重要通信的方式,是并发安全的。

buf

hchan结构体中的buf指向一个循环队列,用来实现循环队列,sendx是循环队列的队尾指针,recvx是循环队列的队头指针,dataqsize是缓存型通道的大小,qcount是记录通道内元素个数。

在日常开发过程中用的最多就是ch := make(chan int, 10)这样的方式创建一个通道,如果这要声明初始化的话,这个通道就是有缓冲区的,也是图上紫色的bufbuf是在make的时候程序创建的,它有元素大小*元素个数组成一个循环队列,可以看做成一个环形结构,buf则是一个指针指向这个环。

Go中的channel怎么实现的???

上图对应的代码那就是ch = make(chan int,6)buf指向这个环在heap上的地址。

func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

      mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

    // Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
    // buf points into the same allocation, elemtype is persistent.
    // SudoG's are referenced from their owning thread so they can't be collected.
    // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
    var c *hchan
    switch {
    case mem == 0:
        // Queue or element size is zero.
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
      c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // Elements do not contain pointers.
        // Allocate hchan and buf in one call.
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // Elements contain pointers.
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)
    lockInit(&c.lock, lockRankHchan)

    if debugChan {
        print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
    }
    return c
}

上面就是对应的代码实现,上来它会检查你一系列参数是否合法,然后在通过mallocgc在内存开辟这块空间,然后返回。

sendx & recvx

下面我手动模拟一个ring实现的代码:

// Queue cycle buffer
type CycleQueue struct {
    data                  []interface{} // 存放元素的数组,准确来说是切片
    frontIndex, rearIndex int           // frontIndex 头指针,rearIndex 尾指针
    size                  int           // circular 的大小
}

// NewQueue Circular Queue
func NewQueue(size int) (*CycleQueue, error) {
    if size <= 0 || size < 10 {
      return nil, fmt.Errorf("initialize circular queue size fail,%d not legal,size >= 10", size)
    }
    cq := new(CycleQueue)
    cq.data = make([]interface{}, size)
    cq.size = size
    return cq, nil
}

// Push  add data to queue
func (q *CycleQueue) Push(value interface{}) error {
    if (q.rearIndex+1)%cap(q.data) == q.frontIndex {
        return errors.New("circular queue full")
    }
    q.data[q.rearIndex] = value
    q.rearIndex = (q.rearIndex + 1) % cap(q.data)
    return nil
}

// Pop return queue a front element
func (q *CycleQueue) Pop() interface{} {
    if q.rearIndex == q.frontIndex {
        return nil
    }
    v := q.data[q.frontIndex]
    q.data[q.frontIndex] = nil // 拿除元素 位置就设置为空
    q.frontIndex = (q.frontIndex + 1) % cap(q.data)
    return v
}

循环队列一般使用空余单元法来解决队空和队满时候都存在font=rear带来的二义性问题,但这样会浪费一个单元。golangchannel中是通过增加qcount字段记录队列长度来解决二义性,一方面不会浪费一个存储单元,另一方面当使用len函数查看队列长度时候,可以直接返回qcount字段,一举两得。

Go中的channel怎么实现的???

当我们需要读取的数据的时候直接从recvx指针上的元素取,而写就从sendx位置写入元素,如图:

Go中的channel怎么实现的???

sendq & recvq

当写入数据的如果缓冲区已经满或者读取的缓冲区已经没有数据的时候,就会发生协程阻塞。

Go中的channel怎么实现的???

如果写阻塞的时候会把当前的协程加入到sendq的队列中,直到有一个recvq发起了一个读取的操作,那么写的队列就会被程序唤醒进行工作。

Go中的channel怎么实现的???

当缓冲区满了所有的g-w则被加入sendq队列等待g-r有操作就被唤醒g-w,继续工作,这种设计和操作系统的里面thread5种状态很接近了,可以看出go的设计者在可能参考过操作系统的thread设计。

当然上面只是我简述整个个过程,实际上go还做了其他细节优化,sendq不为空的时候,并且没有缓冲区,也就是无缓冲区通道,此时会从sendq第一个协程中拿取数据,有兴趣的gopher可以去自己查看源代码,本文也是最近笔者在看到这块源代码的笔记总结。

点个关注

如果你没有关注请你点一个关注呗!持续更新中... 如果你需要更多,可以关注我同名微信公众号,分享一些RustGolangSystem Design相关的内容。
Go中的channel怎么实现的???

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这