go内部最常见的设计模式:使用通信的方式进行信息的共享,而不是用共享内存的方式
Goroutine之间的通信方式:Channel
设计原理
有两种方式进行线程之间的通信:
- 共享内存(全局变量)
- 需要通过锁机制来解决竞态问题
- 限制同一时间使用该共享内存的线程数量
- 消息机制
- Go中实现了CSP模型(Communication sequential processes)
- Goroutine、Channel分别对应了实体和媒介
Channel性质
先进先出:
- 多channel时,接受信息、发送信息的顺序与操作channel的顺序保持一致
无锁管道:
- 乐观锁与悲观锁
- 是一种并发控制思想
- 悲观锁:对于数据的操作都会带上锁,防止在此期间被修改
- 数据库写操作
- 乐观锁:只有数据提交时,才采用CAS的方式进行提交以及解冲突
- 数据库MVC并发读取
- 锁机制在休眠与唤醒带来的额外上下文切换代价会降低效率,目前针对三种不同的情况做出了不同的优化
- 同步channel,缓冲区共享数据无关,直接将数据发送给接收方
- 异步channel,基于环形缓冲区,使用传统生产者、消费者模型,还是需要锁机制
- chan struct{}声明的channel,不占用内存空间,无需实现
数据结构
ep表示当前Goroutine的channel操作的变量对象指针,send中为发送的变量,recv中为接收的变量
初始化
根据缓冲区的情况,分为三种分配方式:
- 默认:分别为hchan以及其缓冲区分配内存
- 无缓冲区:只为hchan分配内存空间
- 有缓冲区、且存储的不是指针类型:为hchan、缓冲区分配内存,且1处于同一段连续的内存空间
最后会统一更新elemsize、elemtype、dataqsiz的值
数据发送
- 发送的时候先上锁,防止多线程并发修改
- 检测channel是否关闭,如果已经关闭,panic
- 调用chansend
- 存在等待的接收者时,直接发送给阻塞的接收者
- 从recvx中获取goroutine
- 将数据拷贝到接收变量对应的地址上
- 标记接收Goroutine为可运行的
- 缓冲区存在空余空间的时候,写入缓冲区
- 计算下一个存储的数据的位置
- 调用typedmemmove将数据拷贝到缓冲区
- 增加sendx、qcount计数器
- 不存在缓冲区时、缓冲区已满时,当前goroutine阻塞等待其他接受goroutine接收数据
- 获取当前发送数据时用的Goroutine
- 构建阻塞队列中的最小元素,内部信息包括当前的channel、发送的数据的地址
- 加入发送阻塞队列
- 当前goroutine陷入沉睡
数据接收
实际上除了关闭的情况(向关闭的channel发送消息会panic),其他的情况是对称的
有两种特殊情况:
- channel为空,相当于使当前Goroutine陷入沉睡,且永远不会被唤醒(不会有其他的Goroutine向该channel发送消息)
- 当前channel关闭,且缓冲区没有数据的时候,会清除ep指针中数据并且立刻返回
正常的接受情况也分为几种不同的类型:
- 缓冲区已满、无缓冲区,存在发送阻塞队列
- 无缓冲区
- 将发送队列中的Goroutine的存储的elem拷贝到接收Goroutine对应变量的地址
- 缓冲区已满
- 实际上就是一个替换的过程
- 将原来recv指向的元素拷贝到接收Goroutine对应变量的地址,recv指针递增
- 发送队列头的Goroutine中的元素,代替原recv指针位置元素
- 从缓冲区接收数据
- 缓冲区为空、无缓冲区且发送队列为空
- 将当前Goroutine包装成sudog,放入接收阻塞队列
管道关闭
- 当channel已经关闭或者为空指针都会panic
将所有的接收队列以及发送队列中的Goroutine清除,并且放到Goroutine ready中