看了不少代码了,也自己搭了几个demo玩玩。感觉golang的设计和编程思路的确有独到的地方(虽然我还是不喜欢将设计模式强加于人的感觉)。这次主要聊一下goroutine, 和golang的特殊异步I/O模型。

Goroutine 是一个轻量级的线程

基本概念

  • 每个线程到保存着一个runqueue,即所有注册在该线程上的goroutine
  • 每个goroutine都有自己的stack,且一般情况下是互相隔离的(也就是说不会有cross-goroutine stack read/write) *每个 Goroutine的stack都是可扩展和收缩的,一个goroutine结构体本身一般占用4KB左右
    • Stack有跳跃和连续两种类型。跳跃类型好比一个linkedlist,连续类型好比一个vector/arraylist。各有各的优缺点

  • Goroutine context swithcing 的开销一般是系统线程切换的20%左右。(看一篇blog说的,数据不够官方)
    • gopark 在当前线程中,挂起当前运行的goroutine,在runqueue中找到下一个应当被执行的goroutine并开始运行。(挂起goroutine的逻辑基本上就是将当前goroutine的栈指针和计数器等等保存起来)。
    • goready 会wakeup一个goroutine,将该goroutine放入某一个线程的runqueue当中,于是在未来的某个时刻该goroutine就会被执行。
  • 这个部分内容挺多,以后可以再深入的总结一下

package net 中的异步I/O模型

net.goconn.Write(buffer []byte)buffer
conn.Writewhile (没有全部写入)kernel {继续写}
syscall.EAGAINman 2 write
[EAGAIN] The file is marked for non-blocking I/O, and no data could be written immediately.
fd.pd.waiteWritegopark
package net

这样的包装非常有意思,一方面它让API保持同步的语义,但又通过runtime对goroutine的调度使得本来阻塞的操作并不会阻塞真正的系统线程。总的来说系统线程仍然得到了充分利用,而且用户的代码仍然是同步的,不用去处理回调函数。

goroutine-per-connection 设计模式的限制

net packagegoroutine-per-connection
package net
package netwrite
goroutine-per-connection

大多数情况下,这种设计模式也不会有什么问题,特别是对于客户端来说。但对于中心化的服务器,假设单实例并发连接数超过1M(不算过分),单单处理这些连接就需要 2 * 1M = 2M 个 goroutine,总计大约 4KB * 2M = 8GB 的内存,完全还没有算这些连接上的各种buffer,状态机,还有业务逻辑所需要的开销。所以内存的baseline一下就会被拉高很多

其实不仅仅是内存,大量goroutine对于CPU的开销也会提高。过多的goroutine必然会导致频繁的Goroutine上下文切换。即使goroutine是一个轻量级的线程,这么多的goroutine上下文切换的开销仍然是非常大的。

解决方案

gneteviopackage net