接收数据的底层原理
<-c 关键字
<-c关键字也是一个语法糖- 编译阶段,
i<-c转化为runtime.chanrecv1() - 或者
i, ok<-c转化为runtime.chanrecv2() - 最后调用
chanrecv()方法
接收情形
- 有等待的G, 从G中接收
接收数据前, 已经有协程在休眠等待发送
而且这个Channel没有缓存
将数据直接从G拷贝过来, 唤醒G
实现:
- 判断有G在发送队列等待, 进入recv()
- 判断次Channel无缓存
- 直接从等待的协程中取走数据, 唤醒G
- 有等待的协程, 从缓存接收
接收数据前, 已经有协程在休眠等待发送
而且这个Channel有缓存
从缓存取走一个数据(先取缓存中的数据, 因为缓存中的数据比较早)
将休眠协程的数据放入缓存, 唤醒协程
实现:
- 判断有G在发送队列等待, 进入recv()
- 判断此Channel有缓存
- 从缓存中取走一个数据
- 将G的数据放入缓存, 唤醒G
- 接收缓存
没有协程在休眠等待发送, 但是缓存有内容
从缓存中取走数据
实现:
- 判断没有协程在发送队列等待
- 判断此Channel有缓存
- 从缓存中取走一个数据
- 阻塞接受
没有协程在休眠等待, 而且没有缓存或者缓存为空
自己进入接受队列, 休眠等待
实现:
- 判断没有协程在发送队列等待
- 判断此Channel无缓存
- 把自己包装成sudog
- sudog放入接收等待队列, 休眠
- 唤醒时, 发送的G已经把数据拷贝到位
总结
- 编译阶段,<-c会转化为chanrecv()
- 有等待的G,且无缓存时,从G接收
- 有等待的G,且有缓存时,从缓存接收
- 无等待的G,且缓存有数据,从缓存接收
- 无等待的G,且缓存无数据,等待喂数据
非阻塞channel
select {
case <- chan1:
// 如果chan1成功读到数据, 则执行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据, 则执行该case处理语句
default:
// 如果上面都没有成功, 则进入default处理流程
}
select原理
- 同时存在接收/发送/默认路径
- 首先查看是否有可以立即执行的case
- 没有的话, 有default, 走default
- 没有default, 把自己注册在所有的channel中, 休眠等待; 等待其中一个case发生就会被唤醒
timer
- timer可以提供一个channel, 定时塞入数据
t := time.NewTimer(time.Second) // 1s后向channel放入数据
<-t.C // 阻塞等待数据
总结
为什么使用Channel
- 相对于无锁, 避免协程竞争和数据冲突的问题
- 相对于加锁, 更高级的抽象,降低开发难度,增加程序可读性; 模块之间更容易解耦,增强扩展性和可维护性
Channel的基本结构
- 一个环形缓存
- 两个链表(发送协程/接收协程)
- 一个互斥锁(保护hchan)
- 一个状态值
Channel数据发送原理
- 直接发送时,将数据直接拷贝到目标变量
- 放入缓存时,将数据放入环形缓存,成功返回
- 休眠等待时,将自己包装后放入sendq, 休眠
Channel数据接收原理
- 有等待的协程, 且无缓存时,从协程接收
- 有等待的协程, 且有缓存时,从缓存接收
- 无等待的协程, 且缓存有数据,从缓存接收
- 无等待的协程, 且缓存无数据,等待喂数据
非阻塞Channel
- 使用select可以使用Channel的非阻塞特性
- 使用timer配合select可以实现超时特性