Go语言学习 - Chan的工做原理

咱们建立了一大堆线程, 如今咱们想要实现线程间的同步, 这其中的关键就是chan(通道)的使用, 若是没有通道, 你应该怎么去作线程间同步呢? time.Sleep吗?app

Introduction

type hchan struct {
  dataq_size uint            // 缓冲槽大小
  buf        unsafe.Pointer  // 缓冲槽本体
  elem_type  *_type          // 槽内数据类型
}
复制代码

缓冲槽的工做方式就是上图那样, 每当你往通道里写消息, 消息会先存到缓冲槽里, 然后才被取出来. 这种常规的工做模式也叫异步模式, 由于收发工做不是同步进行的, 你能够先发, 发完你走人, 随后收件人再去管道里取.dom

一样还有一种用法是不设置缓冲槽, 或者说你直接把缓冲槽大小设置成0, 这种工做模式下, 收发双方必须同时守在管道旁, 不然先到的人必定会堵塞在管道哪儿, 等待后到的人, 而后通讯才能开始, 这种也叫同步模式异步

仔细想想通道给咱们带来了什么, 若是将通道的功能点拆开, 通道的核心功能点就是: 能够在两个不一样的G之间相互发消息, 同时还有一套阻塞/唤醒的机制.函数

通道的工做模式

首先分析一个关键点, 你须要知道有哪些G正守着这个通道, 而后把管道里的参数拷贝给这些堵塞着的G, 最后去解除他们的阻 . 有了这个前提条件, 通道工做围绕的对象就必定是G,ui

type hchan struct {
  recv_q waitq  // 接收者队列
  send_q waitq  // 发送者队列
}

type waitq struct {
  first *sudog  
  last  *sudog
}

type sudog struct {
  g     *g               // 想要收发消息的g本体
  elem  unsafe.Pointer   // 要发的消息本体
}
复制代码

每一个通道都会维护一个发送者队里以及一个接收者队列, 队列里的元素是一个包装过的G(G本体+消息). 咱们经过一个发送与接收的过程, 先说说同步通道是如何工做的, 异步通道于此相似spa

收发 - 同步模式

chan <- data的操做会被编译器翻译成去执行chansend函数, 执行的对象是名为chan的通道, 携带一个消息结构体. 检查chan的缓冲槽长度为0, 进入通道的同步模式工做:线程

  • 找一个接收者, 检查chan的接收者队列
  • chan.recv_q为空, 按照同步队列的工做模式, 由于没人接消息, 这个G必须阻塞(沉睡), 你这个G, 连同你的消息, 一块儿被打包成一个sudog, 放到chan的发送者队列里
    • 而后经过gopark把你放入沉睡模式
  • 但若是有接收者, 取出这个sudog对象, 而后把这条消息拷贝到sudog.elem里面, 这表明我这条消息已经发给你了.
    • 最后由于这个接收者以前由于没收到消息一直在沉睡中, 经过goready唤醒

ok以上说明了几件事, 通道是如何知道消息应该发给谁, 消息是怎么发送过去的, 以及咱们看到的阻塞效果是怎么实现的.翻译

仔细想一想, 这阻塞? 在某种条件达到之后自动解除阻塞? 这个场景好像在哪里见过? 你小子在暗示sync.WaitGroup!! 等wg.Count变成0了以后自动解除阻塞, wg使用的阻塞效果也正是经过gopark实现的! 这种沉睡/阻塞最大的特色就是, 某个G被放入沉睡之后, 必须由你手动唤醒, 在咱们的场景中这个条件就是找到了接收方, 在wg的场景中这个条件就是wg.Count变成0了, 条件一命中, 我手动马上帮你唤醒并解除阻塞.3d

收发 - 异步模式

想象一下异步模式与同步模式的区别在哪? 惟一的区别, 仅仅是在管道填满了才会产生堵塞, 否则你发完/收完就走人.code

剩下来原理基本同样, chan <- data操做一样被翻译成chansend函数的调用, 发现缓冲槽大小不为0之后进入异步工做模式, 开始检查缓冲槽的剩余舱位

  • 若是还有剩余舱位, 将消息经过memmove拷贝到管道内, 而后若是发现还有接收者正在堵塞中, 经过goready唤醒
  • 若是没有剩余舱位, 经过gopark进入休眠模式, 在被唤醒之后检查有没有数据

关闭

到了这儿你已经对这一套工做模式很是了解了, 所谓的关闭其实就是遍历通道的发送者队列+接收者队列, 在他们的数据区发送一条nil消息, 而后执行goready唤醒他们中的每个人

Select的工做模式

常常与通道一块儿出现的就是Select, Select的功能只是: 从全部的通道case中随机挑一个能用的, 不然就一直堵塞直到出现一个能用的. 咱们解析一下这种特性是怎么作到的.

随机序

type hselect struct {
  ncases     uint16  // 总数
  poll_order *uint16 // 随机序号
  cases      []scase // 按照初始化顺序的case队列
}
type scase struct {
  c    *hchan // case的本体, 一个通道
  kind uint16 // 通道类型
}
复制代码

一个select在初始化的时候, 而后把全部的通道从chan类型包装成scase类型, 添加上一个字段叫作Kind,这个字段能够是"接收者通道"/"发送者通道", 最后还有一个"default"类型通道, 代表这是一个default case.

而后会生成一个随机序号存到poll_order字段中去, 这表明一个随机数, 而后等程序运行到select的位置的时候, 调用select_go函数, 开始找能够用的通道:

for i,_ := range [0...ncases] {
    random_case_id := poll_order[i]
    random_chanel  := cases[random_id]
    if check(random_chanel) {
        return 
    }
}
if default_case != nil {
    execute(default_case)
    return
}
复制代码

咱们按照以上的方法去执行随机序, 在全部的case都遍历完了之后, 若是没用能用的, 检查有没有能用的default用

沉睡的select

咱们已经知道通道的沉睡与唤醒是怎么实现的, 针对select有意思的一点是, 若是子通道被唤醒, 则本身这个selectG也同时被唤醒了. 这点很神奇, 怎么作到的

for i,_ := range [0...ncases] {
    random_case_id := poll_order[i]
    random_chanel  := cases[random_id]
    if random_chanel.kind == recv_chanel {
        random_chanel.recvq.append(selG)
    }
    if random_chanel.kind == send_chanel {
        random_chanel.sendq.append(selG)
    }
}

gopark(selectG)
复制代码

一样的, 咱们也是遍历select下的全部通道, 把本身添加到通道的消息队列中去

  • 若是是它是发送类型通道, 那就在它的发送者队列中添加本身这个selectG
  • 若是是它是接收类型通道, 那就在它的接收者队列中添加本身这个selectG

想想这样作会发生什么, 本身这个SelectG协程会同时出如今不少通道的消息队列里, 其中任何一个通道被goready唤醒的时候, 本身这个SelectG也会被通知到, 本身也会跟这个通道一块儿被唤醒. 这就实现了select会一直堵塞直到其中任何一个通道畅通为止的特性

相关文章
相关标签/搜索