GO成神之路: 详解channel(二)|Go主题月

171 阅读2分钟

channel的实现

上篇文章我们比较笼统的介绍了一下channel是什么?这篇文章我们从源码来分析channel是如何实现的!

文章中涉及的chan源码都在chan.go文件中

chan的定义

type hchan struct {
	qcount   uint           // 队列中的数据个数
	dataqsiz uint           // 环形队列的大小
	buf      unsafe.Pointer // 环状(datasiz)队列指向的数组地址
	elemsize uint16 // elemtype占的内存大小
	closed   uint32 // chan是否被关闭
	elemtype *_type // chan可接收的数据类型,在chan [type]时被定义的类型
	sendx    uint   // 记录当前需要被sendchan函数发布的数据索引
	recvx    uint   // 记录当前需要被recechan函数消费的数据索引
	recvq    waitq  // 接收队列
	sendq    waitq  // 发送队列
	lock mutex      // 对象锁,用来防止hchan实例被多个线程同时修改
}

上篇文章中我们提到的runtime.sendchan1runtime.chanrecv1函数,Go会将它们分别封装的waitq对象,在hchan结构体中分别对应sendqrecvq字段。

在hchan结构体中以下几个重要字段我们先说明一下:

  • elemtype 这个字段用来记录当前chan支持的数据类型,它是在我们创建chan变量的时候被指定的。

    # go中创建chan的几种方式:
    # 1. 声明
    var c1 chan int
    # 2. 声明并初始化
    c2:=make(chan int)
    
    # 声明与初始化的区别在于是否申请内存
    c1: nil
    c2: 指向某个内存地址
    
  • recvq
    接收队列,当我使用c1:=<-chan这种方式来接收数据时,Go会将这个语法编译为对应的waitq类型,并保存到当前chan实例对应recvq字段中

  • sendq
    发送队列,当我使用chan<-c1这种方式来接收数据时,Go会将这个语法编译为对应的waitq类型,并保存到当前chan实例对应sendq字段中

  • lock
    lock字段用来锁住当前chan实例,因为我们的发送队列与接收队列可能在不同的协程上运行,所以为了避免数据不一致的问题,这里就采用锁的机制来搞定。

其它字段,当我们后面分析具体实现的时候再逐一解释

waitq的定义

waitq类型用于实现send队列与recv队列,这个类型中定义了队列的开头与末尾元素的位置,这也就意味着要访问队列中的元素必须从两端开始遍历,那么Go中的channel是有序的吗?

type waitq struct {
	first *sudog
	last  *sudog
}