美文网首页
go 的 channel 实现原理

go 的 channel 实现原理

作者: wayyyy | 来源:发表于2022-08-30 01:07 被阅读0次
数据结构

src/runtime/chan.go:hchan 中定义了管道的数据结构:

type hchan struct {
    qcount   uint           // chan 里元素数量
    dataqsiz uint           // 底层循环数组的数量
    buf      unsafe.Pointer // 指向底层循环数组的指针,只针对有缓冲的 channel
    elemsize uint16         // chan 中元素大小
    closed   uint32         // chan 是否被关闭的标志
    elemtype *_type         // chan 中元素类型
    sendx    uint           // 已发送元素在循环数组中的索引
    recvx    uint           // 已接收元素在循环数组中的索引
    recvq    waitq          // 等待接收的 goroutine 队列
    sendq    waitq          // 等待发送的 goroutine 队列

    lock mutex              // 保护 hchan 中所有字段
}

从上面的数据结构可以看出来,hchan 由 缓冲队列,类型信息,协程等待队列 和 互斥锁 组成。

  • 环形队列
    chan 内部实现了一个环形队列作为其缓冲区,队列的长度是在创建chan时指定的。
    image.png
  • 等待队列

    • 从 channel 读取数据时,如果 channel 为空或者没有缓冲区,则当前协程会被阻塞,并被加入recvq 队列。
    • 从 channel 写入数据时,如果 channel 已满或没有缓冲区,则当前 协程 会被阻塞,并被加入到 sendq 队列
    image.png

    处于等待队列中的协程会在其他协程操作channel 时被唤醒:

    • 因读阻塞的协程会被向channel 写入数据的协程唤醒
    • 因写阻塞的协程会被从channel 读取数据的协程唤醒
  • 类型信息
    一个 channel 只能传递一种类型的值,类型信息存储在 hchan 数据结构中。

    • elemtype 代表类型,用于在数据传递过程中赋值
    • elemsize 代表类型大小,用于在buf中定位元素的位置
  • 互斥锁
    一个管道同时仅允许被一个协程读写,所以这里还会包含一个互斥锁。

channel 的 创建

创建 channel 的过程实际上是初始化 hchan 结构体的过程,其中类型信息和缓冲区的产固定由内置函数make指定,buf 的大小则由元素大小和缓冲区长度共同决定。

向 channel 写入数据

关闭 channel 会把 recvq 中的协程全部唤醒,这些协程获取的数据都为对应类型的零值,同时会把 sendq 队列中的协程全部唤醒,但这些协程会触发 panic

向一个 channel 写入数据的过程如下:

  • 如果缓冲区有空余位置,则将数据写入缓冲区,结束发送过程
  • 如果缓冲区没有空余位置,则将当前协程加入sendq队列,进入睡眠并等待被读协程唤醒。

这里实现上还有个小技巧,当接收队列recvq不为空时,说明缓冲区没有数据但有协程在等待数据,此时会把数据直接传递给recvq的队列中的第一个协程,而不必再写入缓冲区。

image.png
向 channel 读取数据

如果一个 channel 读取数据简单过程如下:

  • 如果缓冲区有数据,则从缓冲区取出数据,结束读取过程。
  • 如果缓冲区中没有数据,则将当前协程中加入recvq队列,进入睡眠并等待被写协程唤醒。

类似的,如果等待发送队列sendq不为空,且没有缓冲区,那么此时将直接从 sendq 队列第一个协程中获取数据。

image.png
关闭 channel

关闭 channel 时会把 recvq 中的协程全部唤醒,这些协程获取的数据都为对应类型的零值,同时会把 sendq 队列中的协程全部唤醒,但这些协程会触发 panic。

相关文章

网友评论

      本文标题:go 的 channel 实现原理

      本文链接:https://www.haomeiwen.com/subject/lxnvxrtx.html