管道 vs 共享内存
声明方法
make(chan int)无缓冲make(chan bool, 0)// 无缓冲make(chan string, 2)// 有缓冲
基本用法
ch <- x// 发送数据x = <- ch// 接收数据, 赋给x<- ch// 接收数据, 并丢弃
内存与通信
- 不要通过共享内存的方式进行通信
- 而是应该通过通信的方式共享内存
为什么使用管道
- 避免协程竞争和数据冲突的问题
- 更高级的抽象,降低开发难度,增加程序可读性
- 模块之间更容易解耦,增强扩展性和可维护性
如何设计
缓存区
type hchan struct {
// 指示channel是开放还是关闭
closed uint32 // 0开启, 1关闭
// 组成一个缓存区
qcount uint
dataqsiz uint
buf unsafe.Pointer
elemsize uint16
elemtype *_type
// 两个队列
recvx uint
recvq waitq // 链表
sendx uint
sendq waitq
// 保护hchan所有字段
lock mutex
}
- 环形缓存可以大幅降低GC的开销
- 互斥锁并不是用来排队发送/接收数据
- 互斥锁保护的hchan结构体本身
- Channel并不是无锁的
发送数据的底层原理
c<-关键字
c<-关键字是一个语法糖- 编译阶段, 会把
c<-转化为runtime.chansend1() - chansend1()会调用chansend()方法
发送情形
直接发送
发送数据前, 已经有G在休眠等待接收
此时缓存肯定是空的, 不用考虑缓存
将数据直接拷贝给G的接收变量, 唤醒G
实现:
1. 从队列里去除一个等待接收的G
2. 将数据直接拷贝到接收变量中
3. 唤醒G
放入缓存
没有G在休眠等待, 但是有缓存空间 将数据放入缓存 实现:
- 获取可存入的缓存地址
- 存入数据
- 维护索引
休眠等待
没有G在休眠等待, 而且没有缓存或满了 自己进入发送队列, 休眠等待 实现:
- 把自己包装成sudog
- sudog放入sendq队列
- 休眠并解锁
- 被唤醒后, 数据已经被取走, 维护其它数据
总结
- 编译阶段,会把<-转化为runtime.chansend1()
- 1.直接发送时,将数据直接拷贝到目标变量
- 2.放入缓存时,将数据放入环形缓存,成功返回
- 3.休眠等待时,将自己包装后放入sendq,休眠