前言
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总体流程:
- 都要先加锁。
- 如果是入数据,关注有没有接收者;如果是出数据,关注有没有发送者
- 如果没有对应的接收/发送,gopark, gorutine状态变为阻塞态
- 如果有,把数据赋值到另外一个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(广播通知)
- 加锁
- 先去循环所有接收者、发送者队列,并将sudog.elem置空,同时把各自的gorutine都加入到glist里
- 解锁
- goready glist的每个gopark过的gorutine,让他们跑起来
- 如果接收,就只能接收到0值
- 如果发送,只能报错