深入剖析 Golang Channel 底层实现:从 hchan 结构到并发通信原理

183 阅读3分钟

前言

channel是golang用于实现协程间通信和channel+select实现组合逻辑的类型安全的管道。

channel底层结构hchan

type hchan struct{
//前7个字段是缓冲区,实现是一个环形队列,但其实本质是一个数组,数组有buf、len、cap
    qcount int  // 环形队列的len
    dataqsiz int //环形队列的cap
    buf unsafe.Pointer //环形队列 本质是一个数组 写到最后一个元素时,会用取余做环
// 环形队列的底层元素实现-元素类型和大小
    elemsize uint16 //元素大小
    elemtype *_type //元素类型
// 发送、接收元素和环形队列的关系
    sendx  uint   //已发送元素在环形队列的位置
    recvx  uint   //已接收元素在环形队列的位置

// channel状态
    closed uint32 //关闭状态

//两个队列和锁 无缓冲区的只有这三个
    recvq waitq //接收者的等待队列-双向链表
    sendq waitq //发送者的等待队列-双向链表
    lock mutex  //锁用于保护hchan中的字段和通道上被阻塞的sudogs中的多个字段
}

//recvq和sendq是等待队列,waitq是一个双向链表
type sudog struct{ 
    g *g //指向协程的指针
    elem unsafe.Pointer //指向数据元素的地址
}

对比slice分析

type slice struct{
    arr unsafe.Pointer
    len int
    cap int
}

slice底层结构并不需要元素类型和大小,因为slice底层通过编译器提前生成好了

    elemsize uint16 //元素大小
    elemtype *_type //元素类型

channel怎么保证线程安全?

通过hchan中的lock字段,能保证线程安全。

在chanrecv chansend chanclose的安全检查判断后,都会加锁,阻塞发送、接收、关闭等操作,保证线程安全。

有无缓存channel的区别

有缓冲channel可以预先存储数据,没有接收方,也可以写入一定量的数据。 无缓冲channel,必须要有接收方,没有写入方,会阻塞。

底层实现

其实发送和接收都差不多,就是数据的入和出操作。

无缓冲channel总体流程:

  1. 都要先加锁。
  2. 如果是入数据,关注有没有接收者;如果是出数据,关注有没有发送者
  3. 如果没有对应的接收/发送,gopark, gorutine状态变为阻塞态
  4. 如果有,把数据赋值到另外一个gorutine,并且调goready把另一个gorutine的状态变成就绪态

【chansend操作】:向channel中发送数据 1. 先加锁, 2.1.1 从c.recvq.dequeue(), 从接收者队列获取一个元素 2.1.2 把数据copy接收者的sudog 3. 如果没有,初始化sudog,c.sendq.enqueue()

【chanrecv操作】:从channel中读数据 1. 先加锁, 2.1 从c.sendq.dequeue(),从发送者队列接收一个元素 2.2 如果没有,初始化一个sudog,放到当前channel的sudog里的elem, 并且c.recvq.enqueue() 2.3 调用gopark,接收者的gorutine变为阻塞态

有缓冲channel总体流程:

其实差不多,只是在步骤2之前,会先看一次缓冲区,有没有数据要出/入,没有的话就阻塞等待

什么是channel的同步读写?异步读写?阻塞读写?

同步读写:发送后,接收者就直接读到

异步读写:经过缓冲区的读写

阻塞:channel空间满了或数据空了,发送者/接收者都只能等着

什么是channel的close(广播通知)

  1. 加锁
  2. 先去循环所有接收者、发送者队列,并将sudog.elem置空,同时把各自的gorutine都加入到glist里
  3. 解锁
  4. goready glist的每个gopark过的gorutine,让他们跑起来
  5. 如果接收,就只能接收到0值
  6. 如果发送,只能报错