本文最后更新于 2024-07-20,文章内容可能已经过时。

概念

Go中的channel是一个队列,遵循先进先出的原则,负责协程之间的通信(Go建议不要使用共享内存来通信,而要通过通道来实现内存共享,CSP(Communicating Sequential Process)并发模型,就是根据goroutine和channel实现的)

使用场景

  • 停止信号监听

  • 定时任务

  • 生产和消费解偶

  • 控制并发数量

底层数据结构

通过var声明或者make创建的channel变量是存储在函数栈帧上的指针,占用8个字节,指向堆上的chan结构体。

源码包 src/runtime/chan.go 定义了hchan的数据结构:

type hchan struct {
	qcount   uint           // total data in the queue  环形队列里面的总的数据量 
	dataqsiz uint           // size of the circular queue // 环形队列大小 就是 make chan时 申请的大小
	buf      unsafe.Pointer // points to an array of dataqsiz elements // 指向环形队列的指针
	elemsize uint16         // 储存的元素类型占空间大小
	closed   uint32         // chan 状态 1 关闭 0 未关闭
	elemtype *_type         // element type // 元素类型   // make chan是 指定的类型 不过这个类型要进行运行时转换 但对应关系是这样的 
	sendx    uint           // send index // 发送索引
	recvx    uint           // receive index //  获取索引
	recvq    waitq          // list of recv waiters // 获取协程等待队列
	sendq    waitq          // list of send waiters // 发送携程等待队列

	lock mutex // 锁
}

等待队列:

双向链表,每个节点是一个sudog结构体变量,记录哪个协程在等待,sudo g,等待的是哪个channel,等待的是那些数据。

type waitq struct {
	first *sudog //指向等待队列中第一个等待的 sudog 结构体的指针
	last  *sudog //指向等待队列中最后一个等待的 sudog 结构体的指针
}

type sudog struct {
    g            *g            // 关联的 goroutine
    next         *sudog        // 下一个 sudog 结构体
    prev         *sudog        // 上一个 sudog 结构体
    elem         unsafe.Pointer // 元素指针
    acquiretime  int64         // 获取时间
    releasetime  int64         // 释放时间
    ticket       uint32        // 票据(用于调度)
    isSelect     bool          // 是否处于 select 操作
    success      bool          // 操作是否成功
    parent       *sudog        // 父 sudog(用于嵌套等待)
    waitlink     *sudog        // 等待链接(用于等待队列)
    waittail     *sudog        // 等待尾部(用于等待队列)
    c            *hchan        // 关联的通道
}

操作

创建

ch:=make(chan int)
ch:=make(chan int,3)

创建时会做一些检查:

  • 元素大小不超过64k

  • 元素对齐大小不超过maxAlign,8字节

  • 计算出来的内存是否超过限制

创建时的策略:

  • 无缓冲会直接给chan分配内存

  • 有缓冲,并且元素不包含指针,会给hchan和底层数组分配一段连续的地址

  • 有缓冲,并且元素包含指针,会给hchan和底层数组分别分配地址

发送

runtime.chansend

默认为阻塞式发送

非阻塞式发送通过select发送

Block参数true 或者false控制

1,发送会类型检查,数据竞争,同时读写

有读等待队列不为空,会发送给第一个等待的chan

为空,直接写入到循环数组,buf没满发送到队尾。满了就回阻塞,就发送给写等待队列。

接收

Runtime chanrecev

直接读取,忽略返回值,接收返回值,接受ok判断,forr遍历读取。

非阻塞式也是通过select

1检查

2接受

如果有写等待队列有,就会

无缓冲直接读

有缓冲,把循环队列的队首拷贝到

关闭

close(ch)

todo....