golang-chan原理

前言

chan是Golang中实现goroutine间通信的重要方式,简称管道。它是一种线程安全的数据结构,用于在Golang程序中传递信息。chan可以实现单向通信和双向通信,可以用于发送和接收数据,也可以用于同步goroutine。

1 数据结构

1.1 hchan

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 mutex
}

hchan:channel 数据结构

  • qcount:当前 channel 中存在多少个元素;
  • dataqsize: 当前 channel 能存放的元素容量;
  • buf:channel 中用于存放元素的环形缓冲区;
  • elemsize:channel 元素类型的大小;
  • closed:标识 channel 是否关闭;
  • elemtype:channel 元素类型;
  • sendx:发送元素进入环形缓冲区的 index;
  • recvx:接收元素所处的环形缓冲区的 index;
  • recvq:因接收而陷入阻塞的协程队列;
  • sendq:因发送而陷入阻塞的协程队列;

1.2 waitq

type waitq struct {
    first *sudog
    last  *sudog
}

waitq:阻塞的协程队列

  • first:队列头部
  • last:队列尾部

1.3 sudog

type sudog struct {
    g *g

    next *sudog
    prev *sudog
    elem unsafe.Pointer // data element (may point to stack)

    acquiretime int64
    releasetime int64
    ticket      uint32

    isSelect bool

    success bool

    parent   *sudog // semaRoot binary tree
    waitlink *sudog // g.waiting list or semaRoot
    waittail *sudog // semaRoot
    c        *hchan // channel
}

sudog:用于包装协程的节点

  • g:goroutine,协程;
  • next:队列中的下一个节点;
  • prev:队列中的前一个节点;
  • elem: 读取/写入 channel 的数据的容器;
  • isSelect:标识当前协程是否处在 select 多路复用的流程中;
  • c:标识与当前 sudog 交互的 chan.

2. 构造函数

// 代码较长,可以看流程图
func makechan(t *chantype, size int) *hchan {
    elem := t.elem

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

    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)

    return
}
  • 判断申请内存空间大小是否越界,mem 大小为 element 类型大小与 element 个数相乘后得到,仅当无缓冲型 channel 时,因个数为 0 导致大小为 0;
  • 根据类型,初始 channel,分为 无缓冲型、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;
  • 倘若为无缓冲型,则仅申请一个大小为默认值 96 的空间;
  • 如若有缓冲的 struct 型,则一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;
  • 倘若为有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续;
  • 对 channel 的其余字段进行初始化,包括元素类型大小、元素类型、容量以及锁的初始化.

3. 发送数据流程*

1.1代码位置/src/runtime/chan

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	......
}

在这里插入图片描述

4. 读数据流程*

4.1代码位置/src/runtime/chan chanrecv()

// 代码较长,可以看流程图
func chanrecv(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	......
}

4.2流程图

在这里插入图片描述

5. 阻塞和非阻塞

5.1 非阻塞模式逻辑区别
非阻塞模式下,读/写 channel 方法通过一个 bool 型的响应参数,用以标识是否读取/写入成功.

  • 所有需要使得当前 goroutine 被挂起的操作,在非阻塞模式下都会返回 false;
  • 所有是的当前 goroutine 会进入死锁的操作,在非阻塞模式下都会返回 false;
  • 所有能立即完成读取/写入操作的条件下,非阻塞模式下会返回 true.
    5.2 何时进入非阻塞模式
    默认情况下,读/写 channel 都是阻塞模式,只有在 select 语句组成的多路复用分支中,与 channel 的交互会变成非阻塞模式:
    5.3 代码
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
    return chansend(c, elem, false, getcallerpc())
}

func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
    return chanrecv(c, elem, false)
}

在 select 语句包裹的多路复用分支中,读和写 channel 操作会被汇编为 selectnbrecv 和 selectnbsend 方法,底层同样复用 chanrecv 和 chansend 方法,但此时由于第三个入参 block 被设置为 false,导致后续会走进非阻塞的处理分支.

6. 两种读 channel 的协议

读取 channel 时,可以根据第二个 bool 型的返回值用以判断当前 channel 是否已处于关闭状态:

ch := make(chan int, 2)
got1 := <- ch
got2,ok := <- ch

实现上述功能的原因是,两种格式下,读 channel 操作会被汇编成不同的方法:

func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

7. 关闭

  • 关闭未初始化过的 channel 会 panic;
  • 加锁;
  • 重复关闭 channel 会 panic;
  • 将阻塞读协程队列中的协程节点统一添加到 glist;
  • 将阻塞写协程队列中的协程节点统一添加到 glist;
  • 唤醒 glist 当中的所有协程.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值