3.2 go关键字
在Go语言中,表达式go f(x,y,z)会启动一个新的goroutine运行函数f(x,y,z)。函数f,变量xyz的值是在原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)时的汇编代码:
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)