前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go专家01,chan实现原理

Go专家01,chan实现原理

作者头像
微客鸟窝
发布2021-08-18 15:46:53
4240
发布2021-08-18 15:46:53
举报
文章被收录于专栏:Go语言指北Go语言指北

在之前的文章中,我们有介绍过 channel 的使用,传送门。比较经典的一句话就是:

❝在 Go 语言中,提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是提倡通过 channel 发送接收消息的方式进行数据传递。 ❞

这里我们再更加深层次的了解下 chan 。

chan 数据结构

src/runtime/chan.go 中定义了 channel 的数据结构如下:

代码语言:javascript
复制
type hchan struct {
 qcount   uint  // 队列中的总元素个数
 dataqsiz uint  // 环形队列大小,即可存放元素的个数
 buf      unsafe.Pointer // 环形队列指针
 elemsize uint16  //每个元素的大小
 closed   uint32  //标识关闭状态
 elemtype *_type // 元素类型
 sendx    uint   // 发送索引,元素写入时存放到队列中的位置

 recvx    uint   // 接收索引,元素从队列的该位置读出
 recvq    waitq  // 等待读消息的goroutine队列
 sendq    waitq  // 等待写消息的goroutine队列
 lock mutex  //互斥锁,chan不允许并发读写
}

环形队列

chan 内部实现了一个环形队列来作为缓冲区,队列的长度是在创建 chan 的时候所指定的。

如下图,我们创建一个可缓存6个元素的 channel 示意图:

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

等待队列

  • 从 channel 中读取数据,如果 channel 的缓冲区为空,或者没有缓冲区,那么当前的 goroutine 会被阻塞。
  • 向 channel 中写入数据,如果 channel 的缓冲区已满,或者没有缓冲区,那么当前的 goroutine 会被阻塞。
  • 被阻塞的 goroutine 会挂在 channel 的等待队列中。
    • 因为读所导致的阻塞,会被向 channel 写入数据的 goroutine 所唤醒。
    • 因为写所导致的阻塞,会被从 channel 读取数据的 goroutine 所唤醒。

如下图,为没有缓冲区的 channel,recvq 中有几个 goroutine 阻塞等待读数据。

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

类型信息

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

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

我们知道,channel 是并发安全的,即一个channel同时仅允许被一个goroutine读写。

channel 读写

创建 channel

创建 channel 其实就是初始化 hchan 结构,其类型信息和缓冲区长度由 make 语句传入,buf 的大小则与元素大小和缓冲区长度来共同决定。

创建 channel 的伪代码:

代码语言:javascript
复制
func makechan(t *chantype, size int) *hchan{
  var c *hchan
  c = new(hchan)
  c.buf = malloc(元素类型大小*size)
  c.elemsize = 元素类型大小
  c.elemtype = 元素类型
  c.dataqsiz = size
  
  return c
}

向 channel 写数据

过程如下:

  1. 若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
  2. 若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
  3. 若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。

从 channel 读数据

过程如下:

  1. 若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程;
  2. 如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程;
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  4. 将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒;

关闭 channel

关闭 channel 时会将 recvq 中的 G 全部唤醒,,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。

panic 出现的场景还有:

  1. 关闭值为 nil 的 channel
  2. 关闭已经关闭的 channel
  3. 向已经关闭的 channel 中写数据

用法

单项 channel

只能发送或者只能接收的 channel 为单向 channel。

单向 channel 声明

只需要在基础声明中增加操作符即可:

代码语言:javascript
复制
send := make(ch<- int) //只能发送数据给channel
receive := make(<-ch int) //只能从channel中接收数据

示例:

代码语言:javascript
复制
package main

import (
 "fmt"
)
//只能发送通道
func send(s chan<- string){
 s <- "微客鸟窝"
}
//只能接收通道
func receive(r <-chan string){
 str := <-r
 fmt.Println("str:",str)
}
func main() {
 //创建一个双向通道
 ch := make(chan string)
 go send(ch)
 receive(ch)
}

//运行结果: str: 微客鸟窝

select

select 可以实现多路复用,即同时监听多个 channel。

  • 发现哪个 channel 有数据产生,就执行相应的 case 分支
  • 如果同时有多个 case 分支可以执行,则会随机选择一个
  • 如果一个 case 分支都不可执行,则 select 会一直等待

示例:

代码语言:javascript
复制
package main

import (
 "fmt"
)

func main() {
 ch := make(chan int, 1)
 for i := 0; i < 10; i++ {
  select {
  case x := <-ch:
   fmt.Println(x)
  case ch <- i:
   fmt.Println("--", i)
  }
 }
}

运行结果:

代码语言:javascript
复制
-- 0
0
-- 2
2
-- 4
4
-- 6
6
-- 8
8

❝select 的 case 语句读 channel 不会阻塞,尽管 channel 中没有数据。这是由于 case 语句编译后调用读 channel 时会明确传入不阻塞的参数,此时读不到数据时不会将当前 goroutine 加入到等待队列,而是直接返回。 ❞

range

通过 range 可以持续从 channel 中读取数据,类似于遍历,当 channel 中没有数据时会阻塞当前 goroutine ,与读 channel 时阻塞处理机制一样。

示例:

代码语言:javascript
复制
for ch := range chanName {
  fmt.Printf("chan: %d\n", ch)
}

❝注意:如果向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞。 ❞


有什么问题,可以公众号内回复或加我微信交流。

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

本文分享自 微客鸟窝 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • chan 数据结构
    • 环形队列
      • 等待队列
        • 类型信息
          • channel 读写
            • 创建 channel
              • 向 channel 写数据
                • 从 channel 读数据
                  • 关闭 channel
                  • 用法
                    • 单项 channel
                      • select
                        • range
                        相关产品与服务
                        数据保险箱
                        数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档