本文转载自微信公众号「吴亲强的深夜食堂」,作者吴亲库里。转载本文请联系吴亲强的深夜食堂公众号。
躺的太久,该起床了。
宁可我卷死别人,不能让别人卷我。
之前断断续续看过Go几个模块的源码,可从未下笔,导致有些细节记不起来了。打算写一系列文章重新记录。
channel源码解析的文章太多了。一篇文章的长篇大论大部分人没耐心看完,所以我打算分开写,最后附上完整的ppt。
当然这其中不会涉及过多细节源码,因为有时候,细节是魔鬼。
介绍
channel一些基础介绍这里就不过多涉及了,都1202年了,我不相信用过Go的人没用过channel。
当然下图也涵盖了大部分使用姿势。
有一道使用channel进行任务编排的经典的题。题目如下:
有四个goroutine,编号为 1、2、3、4。每秒钟会有一个 goroutine打印自己的编号。请你实现这个程序,让输出的编号总是按照 1、2、3、4、1、2、3、4、……的顺序打印出来。就像这样,
可以自己先思考下,代码也可以通过后台回复击鼓传花获取。
原理解析
从一个简单的例子说起。
创建一个main.go文件,代码如下,
我们来看看这段代码编译以后长啥样。
想得到go程序的汇编代码并不难。
可以使用go tool compile -N -l -S main.go生成汇编代码:
或者使用go tool compile -N -l main.go先编译出代码,然后再使用go tool objdump main.o反汇编出代码。
还可以通过go build -gcflags -S main.go同样可以得到汇编的代码。
上面两种我就不演示了,可以自行实验。他们之中flag的具体含义也可以自行了解。
如果你觉得上面要自己敲代码比较麻烦,我推荐一个更加直接可视化的工具。
综上,从编译的代码我们可以看出,上述初始化一个channel,实际上调用的是runtime.makechan。
- ch := make(chan struct{})
图片从函数中,我们能知道最终返回一个runtime.hchan的指针。
runtime.hchan结构。
我们先来解释hchan结构体各个字段的含义,之后在案例介绍中会更加详细的说明他们的作用。
先来看qcount和dataqsiz有什么区别?
你去银行办事,银行有5个办事窗口,那么dataqsiz就等于5。在这里体现的是channel的容量为5。去银行的时候,当前有3个窗口有人正在办事,那么qcount就等于3,体现channel当前有3个数据元素。那么此时银行还可以再接待2个客户,对应还可以往channel发送2个数据元素。
其他字段现在看看说明就行了,后面会细讲。
到这里我们就知道创建一个channel本质上就是得到一个runtime.hchan的指针,后续对此chan的操作,无非就是对结构体字段进行相对应的操作。
同时我们也能猜出,为啥channel能在不同的g中传递消息,而对于使用者来说不用担心并发的问题。
其实就是hchan内部使用互斥锁来保证了并发安全。
最后我们来看一下runtime.makechan函数核心实现,当然注释已经很明白了。
可以看到创建的时候有一段switch分支代码,那么什么情况下会走对应的case呢?
根据上面的信息,我们可以得出,
- 如果创建一个无缓冲channel ,那么只需要为runtime.hchan本身分配一段内存空间即可。
- 如果创建的缓冲channel存储的类型不是指针类型,会为当前channel和存储类型元素的缓冲区,分配一块连续的内存空间。
- 在默认情况下(缓冲channel存储类型包含指针),会单独为runtime.hchan和缓冲区分配内存。
总结
这篇我们主要介绍了如何获取go程序的汇编代码,通过汇编代码知道创建channel的具体函数runtime.makechan。
同时我们还知道不同的创建姿势会导致走向不同的内存空间分配逻辑。
最后通过创建函数我们知道channel在程序运行时是使用runtime.hchan来表示。
下一篇我们继续。