3.2 go关键字

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

首先,让我们看一下如果是C代码新建一条线程的实现会是什么样子的。大概会先建一个结构体,结构体里存f、x、y和z的值。然后写一个help函数,将这个结构体指针作为输入,函数体内调用f(x, y, z)。接下来,先填充结构体,然后调用newThread(help, structptr)。其中help是刚刚那个函数,它会调用f(x, y, z)。help函数将作为所有新建线程的入口函数。

这样做有什么问题么?没什么问题…只是这样实现代价有点高,每次调用都会花上不少的指令。其实Go语言中对go关键字的实现会更加hack一些,避免了这么做。

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

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

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

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

  1. MOVL $1, 0(SP)
  2. MOVL $2, 4(SP)
  3. MOVL $3, 8(SP)
  4. PUSHQ $f(SB)
  5. PUSHQ $12
  6. CALL runtime.newproc(SB)
  7. POPQ AX
  8. 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关键字的实现仅仅是一个语法糖衣而已,也就是:

  1. go f(args)

可以看作

  1. runtime.newproc(size, f, args)