源地址 :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
- 切换到g0调用ready函数, 调用完切换回来
- 从发送者拿到数据并唤醒了G后, 就可以从chansend返回了
- 判断是否可以把元素放到缓冲区中
- 如果缓冲区有空余的空间, 则把元素放到缓冲区并从chansend返回
- 无缓冲区或缓冲区已经写满, 发送者的G需要等待
- 获取当前的g
- 新建一个sudog
- 设置sudog.elem = 指向发送内存的指针
- 设置sudog.g = g
- 设置sudog.c = channel
- 设置g.waiting = sudog
- 把sudog放入channel.sendq
- 调用goparkunlock函数
- 从这里恢复表示已经成功发送或者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工程师的功力, 可以写出这样简单易懂的代码不容易.