专栏首页Go语言指北Go专家01,chan实现原理

Go专家01,chan实现原理

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

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

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

chan 数据结构

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

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 的伪代码:

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 声明

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

send := make(ch<- int) //只能发送数据给channel
receive := make(<-ch int) //只能从channel中接收数据

示例:

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 会一直等待

示例:

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

运行结果:

-- 0
0
-- 2
2
-- 4
4
-- 6
6
-- 8
8

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

range

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

示例:

for ch := range chanName {
  fmt.Printf("chan: %d\n", ch)
}

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


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

本文分享自微信公众号 - 微客鸟窝(gophpython),作者:有码无尘

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 现代的服务端技术栈:Golang/Protobuf/gRPC

    原题:Introduction to the Modern Server-side Stack — Golang, Protobuf, and gRPC

    yuanyi928
  • fasthttp中的协程池实现

    fasthttp中的协程池实现 协程池可以控制并行度,复用协程。fasthttp 比 net/http 效率高很多倍的重要原因,就是利用了协程池。实现并不复杂,...

    李海彬
  • token bucket限流算法原理及代码

    以上是bucket4j给出的一个简单实现,用于理解token bucket算法 这个算法没有采用线程去refill token,因为bucket太多的话,线程...

    JavaEdge
  • Go - chan 通道

    在 go 关键字后面加一个函数,就可以创建一个线程,函数可以为已经写好的函数,也可以是匿名函数。

    新亮
  • go 学习笔记之初识 go 语言

    > 摘录自 github: https://github.com/golang/go,其中官网(国外): https://golang.org 和官网(国内):...

    雪之梦技术驿站
  • 深度解密Go语言之channel

    大家好啊!“深度解密 Go 语言”系列好久未见,我们今天讲 channel,预祝阅读愉快!在开始正文之前,我们先说些题外话。

    梦醒人间
  • 44. goroutine、channel、time的例子 | 厚土Go学习笔记

    格式化时间样式,利用 goroutine 实现获取和格式化当前时间,并且通过 channel 返回到主函数并打印出来。 在 go 语言中,时间格式化有一个标准时...

    李海彬
  • 【Golang语言社区投稿】golang高并发基于协程,通道的任务池

    要点: 封装了协程模型基于select模型的通道传递; 支持同步和异步添加任务;由于golang无函数指针,任务函数利用了go 反射机制支持可变参的入参 开发者...

    李海彬
  • 如何编译调试Go runtime源码

    有朋友问我阅读源码,该怎么调试?这次我们简单看看如何编译调试 Go 的 runtime 源码,感兴趣的朋友可以自己手动操作一下。

    luozhiyun
  • 如何编译调试Go runtime源码

    有朋友问我阅读源码,该怎么调试?这次我们简单看看如何编译调试 Go 的 runtime 源码,感兴趣的朋友可以自己手动操作一下。

    luozhiyun
  • 01 . RPC简介原理及用Go实现一个RPC

    常见_youmen
  • Golang常见的坑笔记

    Format 的时候 时间必须是 2006-01-02 15:04:05 ,奇葩时间。

    solate
  • go 语言中的并发特性

    以前我们写并发的程序一般是用多线程来实现,自己维护一个线程池,在恰当的时候创建、销毁、分配资源。

    机智的程序员小熊
  • Go语言如何并发超时处理详解

    大家都知道golang并没有在语言层次上提供超时操作,但可以通过一些小技巧实现超时。下面来一起看看吧,有需要的朋友们可以参考借鉴。 实现原理: 并发一个函数,等...

    李海彬
  • Go语言如何并发超时处理详解

    大家都知道golang并没有在语言层次上提供超时操作,但可以通过一些小技巧实现超时。下面来一起看看吧,有需要的朋友们可以参考借鉴。 实现原理: 并发一个函数,等...

    李海彬
  • PHP 协程:Go + Chan + Defer

    Swoole4提供了强大的PHP CSP协程编程模式。底层提供了3个关键词,可以方便地实现各类功能。

    猿哥
  • go语言中函数参数传值还是传引用的思考

    算起来这些年大大小小也用过一些不同编程语言,但平时开发还是以C++为主,得益于C++精确的语义控制,我可以在编写代码的时候精准地控制每一行代码的行为,以达到预期...

    tyriqchen
  • Go 语言并发编程系列(五)—— 通道类型篇:基本语法和缓冲通道

    在上篇教程中,学院君给大家演示了如何通过通道(channel)传递消息实现 Go 协程间的通信, 接下来,我们将通过几篇教程的篇幅来系统了解通道类型及其使用,从...

    学院君
  • 知道创宇区块链安全实验室|深入理解以太坊交易处理机制

    区块链是一个以"去中心化"、"去信任化"方式集体维护的分布式账本,这里的"分布式"不仅体现在数据的分布式存储,也体现在数据的分布式记录,即由系统参与者共同维护,...

    Al1ex

扫码关注云+社区

领取腾讯云代金券