前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang 语言中的 channel 实现原理

Golang 语言中的 channel 实现原理

作者头像
frank.
发布2021-07-20 11:28:28
3700
发布2021-07-20 11:28:28
举报

01

介绍

Golang 语言社区流传一句口号:

Do not communicate by sharing memory; instead, share memory by communicating.

安全访问共享变量是并发编程的一个难点,在 Golang 语言中,倡导通过通信共享内存,实际上就是使用 channel 传递共享变量,在任何给定时间,只有一个 goroutine 可以访问该变量的值,从而避免发生数据竞争。

关于 channel 的使用方法,我们在之前的文章中「Go 语言学习之 goroutine 和 channel」介绍过,本文我们从 channel 的数据结构和执行逻辑两个方面介绍一下它的实现原理。

02

数据结构

我们看一下 Golang 源码中 channel 的数据结构。

$GOROOT/src/runtime/chan.go

代码语言:javascript
复制
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
}

阅读 channel 的源码,可以发现 channel 的数据结构是 hchan 结构体,包含以下字段:

  • qcount 当前队列中剩余的元素个数
  • datasize 环形队列的长度
  • buf 环形队列的指针
  • elemsize 元素的大小
  • closed 关闭标识
  • elemtype 元素的类型
  • sendx 发送索引位置
  • recvx 接收索引位置
  • recvq 等待接收的协程队列
  • sendq 等待发送的协程队列
  • lock 互斥锁

通过阅读 channel 的数据结构,可以发现 channel 是使用环形队列作为 channel 的缓冲区,datasize 环形队列的长度是在创建 channel 时指定的,通过 sendxrecvx 两个字段分别表示环形队列的队尾和队首,其中,sendx 表示数据写入的位置,recvx 表示数据读取的位置。

字段 recvq 和 sendq 分别表示等待接收的协程队列和等待发送的协程队列,当 channel 缓冲区为空或无缓冲区时,当前协程会被阻塞,分别加入到 recvq 和 sendq 协程队列中,等待其它协程操作 channel 时被唤醒。其中,读阻塞的协程被写协程唤醒,写阻塞的协程被读协程唤醒。

字段 elemtype 和 elemsize 表示 channel 中元素的类型和大小,需要注意的是,一个 channel 只能传递一种类型的值,如果需要传递任意类型的数据,可以使用 interface{} 类型。

字段 lock 是保证同一时间只有一个协程读写 channel。

03

执行逻辑

在 Golang 语言中,可以对 channel 进行读写操作,本小节我们分别介绍一下 channel 的读操作和写操作。

写操作 channel,分为两种情况,第一种是 channel 的缓冲区未写满,直接将数据写入缓冲区,结束 send 操作;第二种是 channel 的缓冲区已写满,此时,当前操作 channel 的协程将会被加入 sendq 等待发送的协程队列,等待被读协程唤醒。

需要注意的是,当 recvq 队列不为空时,证明缓冲区没有数据,但是有协程等待读取数据,此时,数据将不再写入缓冲区,而是会直接把数据传递给 recvq 队列中的第一个协程。

读操作 channel,也分为两种情况,第一种是 channel 的缓冲区中有数据,直接读取缓冲区中的数据,结束 recv 操作;第二种是 channel 的缓冲区中没有数据,此时,当前操作 channel 的协程加入 recvq 等待接收的协程队列,等待被写协程唤醒。

需要注意的是,当 sendq 队列不为空时,并且缓冲区已写满,此时,将直接从 sendq 队列中的第一个协程读取数据。

当 channel 被关闭时,recvq 和 sendq 中的所有协程被唤醒,其中 recvq 中的协程读取到的数据全部是 nil,sendq 中的协程会触发 panic。

04

总结

本文我们在 channel 的数据结构和执行逻辑两个方面介绍了 channel 的实现原理,其中,执行逻辑小节中,重点介绍了 channel 的读写操作。如果读者朋友们想要了解创建 channel 的执行逻辑,可以阅读源码中的函数 func makechan(t *chantype, size int) *hchan

参考资料: https://golang.org/doc/effective_go#sharing https://blog.golang.org/codelab-share

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go语言开发栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档