高并发下的通信方式:Channel管道
内存与通信
- “不要通过共享内存的方式进行通信”
- “而是应该通过通信的方式共享内存”
为什么使用通信来共享内存
- 避免协程竞争和数据冲突的问题。
- 更高级的抽象,降低开发难度,增加程序可读性。
- 模块之间更容易解耦,增强扩展性和可维护性。
如何设计高性能Channel
- 环形缓存可以大幅降低GC的开销。
hchan结构体中的互斥锁成员
- 互斥锁并不是用来排队发送/接收数据的
- 互斥锁保护的是hchan结构本身。
- Channel并不是无锁的。
Channel发送数据的底层原理是什么?
c <- 关键字
- c <- 关键字是一个语法糖
- 编译阶段,会把c <- 转化为runtime.chansend1()
- chansend1()会调用chansend()方法
直接发送
- 发送数据前,已经有G在休眠等待接收。
- 此时缓存肯定是空的,不用考虑缓存。
- 将数据直接拷贝给G的接收变量,唤醒G。
- 从队列里取出一个等待接收的G
- 将数据直接拷贝到接收变量中
- 唤醒G
放入缓存
- 没有G在休眠等待,但是有缓存空间。
- 将数据放入缓存
- 获取可存入的缓存地址。
- 存入数据
- 维护索引
休眠等待
- 没有G在休眠等待,而且没有缓存或满了。
- 自己进入发送队列,休眠等待。
- 把自己包装成sudog
- sudog放入sendq队列
- 休眠并解锁
- 被唤醒后,数据已经被取走,维护其他数据。
总结
- 编译阶段,会把<-转化为runtime.chansend1()
-
- 直接发送时,将数据拷贝到目标变量。
-
- 放入缓存时,将数据放入环形缓存,成功返回。
-
- 休眠等待时,将自己包装成sudog后放入sendq,休眠。
Channel接收数据的底层原理是什么?
<-c关键字
- <-c 关键字是一个语法糖
- 编译时, i <- c转化为runtime.chanrecv1()
- 编译阶段,i,ok <-c 转化为runtime.chanrecv2()
- 最后会调用chanrecv()方法。
有等待的G,从G接收
- 接收数据前,已经有G在休眠等待发送。
- 而且这个Channel没有缓存。
- 将数据直接从G拷贝过来,唤醒G。
- 判断有G在发送队列等待,进入recv()
- 判断此Channel无缓存。
- 直接从等待的G中取走数据,唤醒G。
有等待的G,从缓存接收
- 接收数据前,已经有G在休眠等待发送
- 而且这个Channel有缓存
- 从缓存取走一个数据
- 将休眠G的数据放进缓存,唤醒G。
- 判断有G在发送队列等待,进入recv()
- 判断此Channel有缓存
- 从缓存中取走一个数据
- 将G的数据放入缓存,唤醒G.
从缓存中接收
- 没有G在休眠等待发送,但是缓存有内容
- 直接从缓存取走数据
- 判断没有G在发送队列等待
- 判断此Channel有缓存
- 从缓存中取走一个数据
阻塞接收
- 没有G在休眠等待,而且没有缓存或缓存为空。
- 自己进入接收队列,休眠等待。
- 判断没有G在发送队列等待
- 判断此Channel无缓存
- 将自己包装成sudog
- sudog放入等待队列,休眠。
- 唤醒时,发送的G已经把数据拷贝到位。
非阻塞的Channel怎么做?
func main() {
ch1 := make(chan int, 5)
ch2 := make(chan int)
select {
case <-ch1:
fmt.Println("葛诗颖")
case ch2 <- 1:
fmt.Println("诗诗")
default:
fmt.Println("葛诗颖在刷抖音")
}
}
运行结果
葛诗颖在刷抖音
Process finished with the exit code 0
select 原理
- 在编译后的汇编语言中 同时存在接收、发送、默认路径。
- 首先查看是否有可以即时执行的case。
- 没有的话,有default,执行default。
- 没有default,把自己注册在所有的Channel中,休眠等待。
timer
- timer可以提供一个Channel,定时塞入数据。