Go中的Channel(二) | 青训营笔记

85 阅读3分钟

接收数据的底层原理

<-c 关键字

  • <-c 关键字也是一个语法糖
  • 编译阶段, i<-c 转化为runtime.chanrecv1()
  • 或者 i, ok<-c 转化为runtime.chanrecv2()
  • 最后调用chanrecv()方法

接收情形

  • 有等待的G, 从G中接收

Pasted image 20230522222015.png 接收数据前, 已经有协程在休眠等待发送 而且这个Channel没有缓存 将数据直接从G拷贝过来, 唤醒G 实现:

  1. 判断有G在发送队列等待, 进入recv()
  2. 判断次Channel无缓存
  3. 直接从等待的协程中取走数据, 唤醒G
  • 有等待的协程, 从缓存接收

Pasted image 20230522222424.png 接收数据前, 已经有协程在休眠等待发送 而且这个Channel有缓存 从缓存取走一个数据(先取缓存中的数据, 因为缓存中的数据比较早) 将休眠协程的数据放入缓存, 唤醒协程 实现:

  1. 判断有G在发送队列等待, 进入recv()
  2. 判断此Channel有缓存
  3. 从缓存中取走一个数据
  4. 将G的数据放入缓存, 唤醒G
  • 接收缓存

Pasted image 20230522223027.png 没有协程在休眠等待发送, 但是缓存有内容 从缓存中取走数据 实现:

  1. 判断没有协程在发送队列等待
  2. 判断此Channel有缓存
  3. 从缓存中取走一个数据
  • 阻塞接受

Pasted image 20230522223219.png 没有协程在休眠等待, 而且没有缓存或者缓存为空 自己进入接受队列, 休眠等待 实现:

  1. 判断没有协程在发送队列等待
  2. 判断此Channel无缓存
  3. 把自己包装成sudog
  4. sudog放入接收等待队列, 休眠
  5. 唤醒时, 发送的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可以实现超时特性