美文网首页
go chan管道读写解读

go chan管道读写解读

作者: Feel_狗焕 | 来源:发表于2024-04-26 23:42 被阅读0次

1、channel数据结构:

channel 是一种类型安全的(并发安全)、阻塞的、先进先出(FIFO)的数据结构,确保发送的数据按照发送的顺序接收。

通过 var 声明或者 make 函数创建的 channel 变量是一个存储在函数栈上的指针,指向堆上的 hchan 结构体。底层数据结构如下:


type hchan struct {

qcount uint // 队列中剩余的元素个数

dataqsiz uint // 环形队列长度,即可以存放的元素个数, make初始化时指定

// channel 分为无缓冲和有缓冲两种。

// 对于有缓冲的 channel 存储数据,使用了 ring buffer(环形缓冲区)来缓存写入的数据,本质是循环数组。

// 为什么是循环数组?普通数组不行吗?普通数组容量固定,更适合指定的空间,弹出元素时,普通数组需要全部都前移。

// 指向底层循环数组的指针(环形缓冲区)缓存区,实际上就是环形队列(有环形队列就有缓冲区,否则没有缓冲区),指向环形队列首部的指针,基于环形队列实现,大小等于make初始化channel时指定的环形队列长度,如果make初始化channel时不指定dataqsiz,则buf=0。只有缓冲型的channel才有buf

buf unsafe.Pointer

elemsize uint16 // 元素的大小

closed uint32 // channel是否关闭的标志

elemtype *_type // channel中的元素类型

// 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置

sendx uint // 写入数据的索引,即从哪个位置开始写入数据,取值[0, dataqsiz)

recvx uint // 读取数据的索引,即从哪个位置开始读取数据,取值[0, dataqsiz)

//接收等待队列,链表结构,长度无限长, 读取数据的goroutine等待队列, 如果channel的缓冲区为空或者没有缓冲区,读取数据的goroutine被阻塞,加入到recvq等待队列中。因读阻塞的goroutine会被向channel写入数据的goroutine唤醒

recvq waitq

//发送等待队列,链表结构,长度无限长, 写入数据的goroutine等待队列, 如果channel的缓冲区为满或者没有缓冲区,写入数据的goroutine被阻塞,加入到sendq等待队列中。因写阻塞的goroutine会被从channel读取数据的goroutine唤醒

sendq waitq

//并发控制锁, 同一时刻,只允许一个, channel不允许并发读写

lock mutex

}

1.1、环形队列:

chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。 下图展示了一个可缓存6个元素的channel示意图:

image.jpeg
  • dataqsiz指示了队列长度为6,即可缓存6个元素;
  • buf指向队列的内存,队列中还剩余两个元素;
  • qcount表示队列中还有两个元素;
  • sendx指示后续写入的数据存储的位置,取值[0, 6);
  • recvx指示从该位置读取数据, 取值[0, 6);

1.2、goroutine等待队列:

从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。向channel写数据,如

果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。 被阻塞的goroutine将会挂在channel的等待队列中:

  • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
  • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;

下图展示了一个没有缓冲区的channel,有几个goroutine阻塞等待读数据:

image.jpeg

注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向 channel一边写数据,一边读数据。

1.3、类型信息:

一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。

  • elemtype代表类型,用于数据传递过程中的赋值;
  • elemsize代表类型大小,用于在buf中定位元素位置;

2、channel读写流程:

G表示一个goroutine

2.1、写入:

image.jpeg

过程描述:

  • 如果等待接收队列recvq 不为空,说明没有缓冲区或者缓冲区没有数据,直接从recvq取出一个G数据写入,把G唤醒,结束发送过程;
  • 如果等待接收队列recvq为空,且缓冲区有空位,那么就直接将数据写入缓冲区sendx位置, sendx++, qcount++, 结束发送过程;
  • 如果等待接收队列recvq为空,缓冲区没有空位,将数据写入G,然后把G放到等待发送队列sendq中进行阻塞,等待被唤醒, 结束发送过程。当被唤醒的时候,需要写入的数据已经被读取出来,且已经完成了写入操作;

2.2、读取:

image.jpeg

虚线表示recvq中堵塞的G被唤醒的流程,如果G没有被唤醒,则一直堵塞下去,此时关闭channel,会得到channel类型的零值

过程描述:

  • 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,读取数据,最后把G唤醒,结束读取过程;
  • 如果等待发送队列sendq不为空,有缓冲区(此时缓冲区满了),从缓冲区中首部读出数据,把G中的数据写入缓冲区尾部,把G唤醒,结束读取过程;
  • 如果等待发送队列sendq为空,且环形队列无元素,将goruntime加入等待接收队列recvq中进行堵塞,等待被唤醒;
  • 如果等待发送队列sendq为空,环形队列有元素,直接从缓冲区读取数据,结束读取过程;

相关文章

  • Go并发模型:并发协程chan的优雅退出

    Go并发模型:并发协程chan的优雅退出 go chan的使用

  • chan深入理解之源码分析

    chan的理解 chan用于协程间通信,结构体如下,代码位置为go/src/runtime/chan.go 从结构...

  • go 的 channel 实现原理

    数据结构 src/runtime/chan.go:hchan 中定义了管道的数据结构: 从上面的数据结构可以看出来...

  • 高性能Go并发建议

    1.管道chan吞吐极限10,000,000,单次Put,Get耗时大约100ns/op,无论是采用单Go程,还是...

  • golang-channel

    缓冲的 chan, make(chan int, 缓冲大小): 表示满了 缓冲大小后 就开始堵塞,无法在往 管道 ...

  • Go中的chan

    go中的chan是动态的,因此千万不要把chan想象成slice切片类型的数据。 只要chan不close可以永远...

  • golang面试题:对未初始化的的chan进行读写,会怎么样?为

    问题 对未初始化的的chan进行读写,会怎么样?为什么? 怎么答 读写未初始化的chan都会阻塞。 举例 1.写未...

  • Go

    数据结构 chan channel是golang进程内协程间通讯的管道 例子 输出: 性质 从chan读,如果ch...

  • go小学鸡踩坑记录

    环境:go1.14.1 var ch chan int只是一个 nil ,并不能对 chan 进行 <- ch ,...

  • Go管道初识

    Go管道初识 Go管道基础知识 管道分类 无缓冲(unbuffered channel)无缓冲的通道是指在接收前没...

网友评论

      本文标题:go chan管道读写解读

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