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中