goroutine的主要特征是创建它们的初始内存成本很低廉(大约4k)以及根据需要动态增长和缩减占用的资源。

 

go关键字

       在Go语言中,表达式go f(x, y, z)会启动一个新的goroutine运行函数f(x, y, z)。函数f,变量x、y、z的值是在原goroutine计算的,只有函数f的执行是在新的goroutine中的。显然,新的goroutine不能和当前go线程用同一个栈,否则会相互覆盖。所以对go关键字的调用协议与普通函数调用是不同的。

 

先看看正常的函数调用,下面是调用f(1, 2, 3)时的汇编代码:

MOVL $1, 0(SP)
MOVL $2, 4(SP)
MOVL $3, 8(SP)
CALL f(SB)

首先将参数1、2、3进栈,然后调用函数f

下面是go f(1, 2, 3)生成的代码:

MOVL $1, 0(SP)
MOVL $2, 4(SP)
MOVL $3, 8(SP)
PUSHQ $f(SB)
PUSHQ $12
CALL runtime.newproc(SB)
POPQ AX
POPQ AX

      对比一个会发现,前面部分跟普通函数调用是一样的,将参数存储在正常的位置,并没有新建一个辅助的结构体。接下来的两条指令有些不同,将f和12作为参数进栈而不直接调用f,然后调用函数 runtime.newproc 。

     12是参数占用的大小。 runtime.newproc 函数接受的参数分别是:参数大小,新的goroutine是要运行的函数,函数的n个参数。

      在 runtime.newproc 中,会新建一个栈空间,将栈参数的12个字节拷贝到新栈空间中并让栈指针指向参数。这时的线程状态有点像当被调度器剥夺CPU后一样,寄存器PC、SP会被保存到类似于进程控制块的一个结构体struct G内。f被存放在了struct G的entry域,后面进行调度器恢复goroutine的运行,新线程将从f开始执行。和前面说的如果用C实现的差别就在于,没有使用辅助的结构体,而 runtime.newproc 实际上就是help函数。在函数协议上,go表达式调用就比普通的函数调用多四条指令而已,并且在实际上并没有为go关键字设计一套特殊的东西。不得不说这个做法真的非常精妙!

 

总结一个,go关键字的实现仅仅是一个语法糖衣而已,也就是:

     go f(args)

可以看作

    runtime.newproc(size, f, args)

size-参数大小,f-新的goroutine要运行的函数,args-参数

注 新版本的是:runtime.newproc(siz int32, fn *funcval)

 

连续栈技术

      goroutine可以初始时只给栈分配很小的空间,然后随着使用过程中的需要自动地增长。基本原理如下:

      每次执行函数调用时Go的runtime都会进行检测,若当前栈的大小不够用,则会触发“中断”,从当前函数进入到Go的运行时库,Go的运行时库会保存此时的函数上下文环境,然后分配一个新的足够大的栈空间,将旧栈的内容拷贝到新栈中,并做一些设置,使得当函数恢复运行时,函数会在新分配的栈中继续执行,这个函数会觉得自己使用的是一块大小“无限”的栈空间。 

本文主要摘自:《深入解析go内核实现》