想来接触 Go 也有段时间了,虽然对 Go 语言的语言生态支持还不是太满意,例如库的版本管理,go get 要梯子,基本数据结构都不全(Set 结构还得 Map 来手动)。但是,怎么说,Go 总体上来说还是不错的,例如强类型比写 Python 更规范一些,内存管理没有 C 那么困难,基本上可以做到随便 new 而不考虑 free,同时还有一个比较爽的特性应该就是 Goroutine 了。

其实 Goroutine 一上手心中就有所察觉,这货不就是协程么。但是,人家就是要嘴硬,我这不是协程,你们协程不是抢占式的,得靠自觉,有的人饿死也不给你机会;而且你们协程还是单线程的,一个核累死一个核闲着也不会帮忙。而我 Goroutine 就不一样了,我们是有组织有纪律的,该让位就让位,该帮忙得帮忙,共同实现有事一起担,没事一起乐的和谐内核。

虽然听上去挺扯的,但是从 Go 语言的设计上来说确认人家是可以做得挺好的,其实想想也是应该的,毕竟这是人家做到语言层面的东西,就拿 Python 来说,虽然在 Python2 的时候有 greenlet,但是这毕竟不是亲儿子啊,是别人强行加上的一个补丁啊,功能上和自己怀上的孩子肯定是有所差别的;如果要说 Python3 中 async 的话,也许可以尝试一波,但是你生态跟得上么?且不说很多人的 Python2 愿不愿意升到 Python3,就算升到 Python3 了你的依赖库支持了么?我的代码整体结构得跟着你变么,这都是很尴尬的一些事情。

Goroutine 的机制

OK,不扯那么多了,就来说说 Goroutine 的实现思路吧,先说一下,我还没看过 Go 的底层代码,我的这些知识都是建立在我阅读了一些别人的博客文章之外,包含 Go 的官方说明和这份(算论文?)记录之后总结的。

说起 Goroutine 的机制,应该网络上大部分文章都是 G、M、P 模型咯(图摘自:Go scheduler: Ms, Ps & Gs):

其实无非就是调用 go 关键字之后就构建了一个 G 数据结构,丢进了 P 的调度队列,然后交给 M 进行真正的系统线程执行(详见我讲 OS 的文章:线程是系统调度的最小单元)。这些有很多可以讲,也没啥好讲,因为我没有深入看过代码,所以我就不讲了,因为没有发言权啊。。好吧,那我能讲啥,到这结束呗。

怎么控制 Goroutine

虽然没看过源代码,但是对于协程的调度还是有一些了解的,所以我不妨结合 goroutine 底层用的非阻塞 + epoll 来聊聊协程是如何进行调度的。可能这个问题一下子不好理解,我这么描述一个场景吧,看下下面这段 python 伪代码:

假设我们有这么一个 http 服务器,运行在单线程上,你需要并发服务好多个客户端,这里的 db.readdb.write 都需要 block 线程,那么如何能够在很小的延迟接受新的客户端请求,并在处理完之后尽可能及时得响应回去给客户端呢?这其实就是一个简单的协程调度模型。

这里可能有很多解决方式,我要描述的是一种非阻塞IO + epoll 模型的方式,这里可能阻塞的地方有四个,分别是:

因为这些地方都涉及到 IO,其实从底层实现来说我们可以认为这里有 3 个 socket,前面两个 db 操作是对同一个 socket 的 read/writenextCmdwrite 操作是对 httpServer 的 read/write, nextRecord 是对 client 连接过来的 socket 进行 read 操作。这样的话,我们将这些 socket 都设置为非阻塞的,那么这会变成怎么样呢?还是上 Python 伪代码:

这里有点意思的地方在于有点像状态机,需要时刻知道每个 socket 的状态,以及当前状态遇到哪些数据和信号时应该给予什么反应。有一个地方需要说明的是,因为是非阻塞,所以让 epoll 收到一个 socket 的 READ OK 的时候,这个时候的读其实是很快的内存拷贝的,可以认为这时候读是非阻塞的了。通过这样的变通,会发现全程 CPU 都会在运转,而不会被等待 DB 的某个协程占用了。

但是从这个简单的例子中也可以发现,如果要我们自己用代码去写着实是费尽得不行,于是乎在 C 语言中就出现了 Libevent 之流,应该还有 python 移植的 Libev 和 Libuv 这些。Python 移植之后,因为做了一些封装,所以我们可以像构建线程一些来进行协程开发了,但是,还是有一些不爽的地方啊,例如 monkey_patch,例如无法对 C 库进行很好得支持。

反观 Go 语言就不一样啦,因为是语言级支持,所以基本上只要是 Go 的库就是走得这个特性(我不敢保证全都是,毕竟 Go 可以很简单得和 C 集成),所以用得也格外爽,不需要 (new Thread()).start() 这种语法了,直接一句 go function() 了事。