基础回顾

并发编程里面的核心组件,让多个goroutine之间可以安全的传递数据并且同步进行

无缓冲的channel:同步通信

发送方执行ch <- val 后会阻塞,直到有一个接收方执行 <- ch

接收方同理

严格同步的特性使无缓冲channel可以协调协程之间执行顺序

有缓冲的channel:异步通信

发送方发送数据后会判断channel是否满,没满就不阻塞,继续进行下去;满了就阻塞

接收方同理

有缓冲的channel适合处理突发流量或解耦生产者与消费者的执行速度,从而提高整体吞吐量

底层数据结构:hchan

hchan的核心结构

type hchan struct {
    qcount   uint           // 当前缓冲区中的元素数量
    dataqsiz uint           // 缓冲区的总容量(make(chan T, size)中的size)
    buf      unsafe.Pointer // 指向环形缓冲区的指针
    elemsize uint16         // 单个元素的大小(用于内存计算)
    closed   uint32         // 标记Channel是否已关闭
    sendx    uint           // 下一个发送位置的索引(环形缓冲区)
    recvx    uint           // 下一个接收位置的索引(环形缓冲区)
    recvq    waitq          // 等待接收的goroutine队列
    sendq    waitq          // 等待发送的goroutine队列
    lock     mutex          // 互斥锁,保护所有字段
}

环形缓冲区(buf)

有缓冲的ch的数据存储区域,采用环形队列设计避免内存频繁分配

sendx和recvx分别记录下一次发送和接收的为止,当qcount=datasize时缓冲区满,发送方阻塞;当qcount=0的时候缓冲区空,接收方阻塞

等待队列

链表,分别存储因channel操作阻塞的协程

互斥锁

任何对hchan的访问都必须先获得这把锁

发送和接收的详细流程

发送的完整流程

  1. 获取ch锁,确保线程安全,因为多个协程可能同时访问同一个ch,会造成数据竞争
  2. 检查是否有等待的接收者,如果不为空就直接绕过缓冲区将值递给等待队列第一个接收者唤醒它,释放锁,这种绕开缓冲区的操作是为了避免写入缓冲区的开销
  3. 如果没有等待的接收者但缓冲区没满就写进缓冲区,相关参数调整,然后释放锁
  4. 如果没有接收者而且缓冲区满了就将发送方包装为sudog,加入发送等待队列,挂起当前的协程,让出cpu
  5. 释放锁

接收的完整流程

  1. 获取ch锁
  2. 检查是否有等待的发送方,取出第一个发送者,如果没有缓存就直接拷贝数据到接收变量,有就从缓冲区头部取数据,同时发送者写数据到缓冲区尾部,唤醒发送者协程,释放锁
  3. 如果没有发送者,检查缓冲区有数据没,有就从游标处读,相关参数调整,释放锁
  4. 没有发送者而且缓冲区也没数据就直接包装接收者协程丢尽等待队列,挂起接收者协程让出cpu
  5. 处理channel关闭,如果channel已经关闭(closed==1),如果缓冲区有数据就正常读取,没有就返回该类型零值

无缓冲channel必须先启动接收方,不然就死锁

select

基础讲解

select是go专门为channel设计的多路复用控制结构,类似网络编程中的select系统调用

核心作用是同时监听多个channel操作,当有多个channel都可能有数据收发的时候,select可以选择其中一个可执行的case进行操作,而不是按顺序逐个尝试。比如同时监听数据流入,超时信号,取消信号等

执行机制

随机选择,如果多个case同时满足条件就随机选择一个执行,避免饥饿问题,没有就执行default,如果没有default就阻塞等待