golang学习-channle详解

源地址 :https://studygolang.com/articles/11627

 

下面仅作留存

channel的实现

在goroutine运行的过程中, 有时候需要对资源进行等待, channel就是最典型的资源.
channel的数据定义在这里, 其中关键的成员如下:

  • qcount: 当前队列中的元素数量
  • dataqsiz: 队列可以容纳的元素数量, 如果为0表示这个channel无缓冲区
  • buf: 队列的缓冲区, 结构是环形队列
  • elemsize: 元素的大小
  • closed: 是否已关闭
  • elemtype: 元素的类型, 判断是否调用写屏障时使用
  • sendx: 发送元素的序号
  • recvx: 接收元素的序号
  • recvq: 当前等待从channel接收数据的G的链表(实际类型是sudog的链表)
  • sendq: 当前等待发送数据到channel的G的链表(实际类型是sudog的链表)
  • lock: 操作channel时使用的线程锁

发送数据到channel实际调用的是runtime.chansend1函数, chansend1函数调用了chansend函数, 流程是:

  • 检查channel.recvq是否有等待中的接收者的G
    • 如果有, 表示channel无缓冲区或者缓冲区为空
    • 调用send函数
      • 如果sudog.elem不等于nil, 调用sendDirect函数从发送者直接复制元素
      • 等待接收的sudog.elem是指向接收目标的内存的指针, 如果是接收目标是_则elem是nil, 可以省略复制
      • 等待发送的sudog.elem是指向来源目标的内存的指针
      • 复制后调用goready恢复发送者的G
        • 切换到g0调用ready函数, 调用完切换回来
          • 把G的状态由等待中(_Gwaiting)改为待运行(_Grunnable)
          • 把G放到P的本地运行队列
          • 如果当前有空闲的P, 但是无自旋的M(nmspinning等于0), 则唤醒或新建一个M
    • 从发送者拿到数据并唤醒了G后, 就可以从chansend返回了
  • 判断是否可以把元素放到缓冲区中
    • 如果缓冲区有空余的空间, 则把元素放到缓冲区并从chansend返回
  • 无缓冲区或缓冲区已经写满, 发送者的G需要等待
    • 获取当前的g
    • 新建一个sudog
    • 设置sudog.elem = 指向发送内存的指针
    • 设置sudog.g = g
    • 设置sudog.c = channel
    • 设置g.waiting = sudog
    • 把sudog放入channel.sendq
    • 调用goparkunlock函数
      • 调用gopark函数
        • 通过mcall函数调用park_m函数
          • mcall函数和上面说明的一样, 会把当前的状态保存到g.sched, 然后切换到g0和g0的栈空间并执行指定的函数
          • park_m函数首先把G的状态从运行中(_Grunning)改为等待中(_Gwaiting)
          • 然后调用dropg函数解除M和G之间的关联
          • 再调用传入的解锁函数, 这里的解锁函数会对解除channel.lock的锁定
          • 最后调用schedule函数继续调度
  • 从这里恢复表示已经成功发送或者channel已关闭
    • 检查sudog.param是否为nil, 如果为nil表示channel已关闭, 抛出panic
    • 否则释放sudog然后返回

从channel接收数据实际调用的是runtime.chanrecv1函数, chanrecv1函数调用了chanrecv函数, 流程是:

  • 检查channel.sendq中是否有等待中的发送者的G
    • 如果有, 表示channel无缓冲区或者缓冲区已满, 这两种情况需要分别处理(为了保证入出队顺序一致)
    • 调用recv函数
      • 如果无缓冲区, 调用recvDirect函数把元素直接复制给接收者
      • 如果有缓冲区代表缓冲区已满
        • 把队列中下一个要出队的元素直接复制给接收者
        • 把发送的元素复制到队列中刚才出队的位置
        • 这时候缓冲区仍然是满的, 但是发送序号和接收序号都会增加1
      • 复制后调用goready恢复接收者的G, 处理同上
    • 把数据交给接收者并唤醒了G后, 就可以从chanrecv返回了
  • 判断是否可以从缓冲区获取元素
    • 如果缓冲区有元素, 则直接取出该元素并从chanrecv返回
  • 无缓冲区或缓冲区无元素, 接收者的G需要等待
    • 获取当前的g
    • 新建一个sudog
    • 设置sudog.elem = 指向接收内存的指针
    • 设置sudog.g = g
    • 设置sudog.c = channel
    • 设置g.waiting = sudog
    • 把sudog放入channel.recvq
    • 调用goparkunlock函数, 处理同上
  • 从这里恢复表示已经成功接收或者channel已关闭
    • 检查sudog.param是否为nil, 如果为nil表示channel已关闭
    • 和发送不一样的是接收不会抛panic, 会通过返回值通知channel已关闭
    • 释放sudog然后返回

关闭channel实际调用的是closechan函数, 流程是:

  • 设置channel.closed = 1
  • 枚举channel.recvq, 清零它们sudog.elem, 设置sudog.param = nil
  • 枚举channel.sendq, 设置sudog.elem = nil, 设置sudog.param = nil
  • 调用goready函数恢复所有接收者和发送者的G

可以看到如果G需要等待资源时,
会记录G的运行状态到g.sched, 然后把状态改为等待中(_Gwaiting), 再让当前的M继续运行其他G.
等待中的G保存在哪里, 什么时候恢复是等待的资源决定的, 上面对channel的等待会让G放到channel中的链表.

对网络资源的等待可以看netpoll相关的处理, netpoll在不同系统中的处理都不一样, 有兴趣的可以自己看看.

参考链接

https://github.com/golang/go
https://golang.org/s/go11sched
http://supertech.csail.mit.edu/papers/steal.pdf
https://docs.google.com/document/d/1ETuA2IOmnaQ4j81AtTGT40Y4_Jr6_IDASEKg0t0dBR8/edit#heading=h.x4kziklnb8fr
https://blog.altoros.com/golang-part-1-main-concepts-and-project-structure.html
https://blog.altoros.com/golang-internals-part-2-diving-into-the-go-compiler.html
https://blog.altoros.com/golang-internals-part-3-the-linker-and-object-files.html
https://blog.altoros.com/golang-part-4-object-files-and-function-metadata.html
https://blog.altoros.com/golang-internals-part-5-runtime-bootstrap-process.html
https://blog.altoros.com/golang-internals-part-6-bootstrapping-and-memory-allocator-initialization.html
http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64
http://legendtkl.com/categories/golang
http://www.cnblogs.com/diegodu/p/5803202.html
https://www.douban.com/note/300631999/
http://morsmachine.dk/go-scheduler

legendtkl很早就已经开始写golang内部实现相关的文章了, 他的文章很有参考价值, 建议同时阅读他写的内容.
morsmachine写的针对协程的分析也建议参考.
golang中的协程实现非常的清晰, 在这里要再次佩服google工程师的功力, 可以写出这样简单易懂的代码不容易.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值