在Go語言中,表達式go f(x, y, z)會啟動一個新的goroutine運行函數(shù)f(x, y, z)。函數(shù)f,變量x、y、z的值是在原goroutine計算的,只有函數(shù)f的執(zhí)行是在新的goroutine中的。顯然,新的goroutine不能和當前go線程用同一個棧,否則會相互覆蓋。所以對go關鍵字的調用協(xié)議與普通函數(shù)調用是不同的。
首先,讓我們看一下如果是C代碼新建一條線程的實現(xiàn)會是什么樣子的。大概會先建一個結構體,結構體里存f、x、y和z的值。然后寫一個help函數(shù),將這個結構體指針作為輸入,函數(shù)體內調用f(x, y, z)。接下來,先填充結構體,然后調用newThread(help, structptr)。其中help是剛剛那個函數(shù),它會調用f(x, y, z)。help函數(shù)將作為所有新建線程的入口函數(shù)。
這樣做有什么問題么?沒什么問題...只是這樣實現(xiàn)代價有點高,每次調用都會花上不少的指令。其實Go語言中對go關鍵字的實現(xiàn)會更加hack一些,避免了這么做。
先看看正常的函數(shù)調用,下面是調用f(1, 2, 3)時的匯編代碼:
MOVL $1, 0(SP)
MOVL $2, 4(SP)
MOVL $3, 8(SP)
CALL f(SB)
首先將參數(shù)1、2、3進棧,然后調用函數(shù)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ā)現(xiàn),前面部分跟普通函數(shù)調用是一樣的,將參數(shù)存儲在正常的位置,并沒有新建一個輔助的結構體。接下來的兩條指令有些不同,將f和12作為參數(shù)進棧而不直接調用f,然后調用函數(shù)runtime.newproc
。
12是參數(shù)占用的大小。runtime.newproc
函數(shù)接受的參數(shù)分別是:參數(shù)大小,新的goroutine是要運行的函數(shù),函數(shù)的n個參數(shù)。
在runtime.newproc
中,會新建一個??臻g,將棧參數(shù)的12個字節(jié)拷貝到新??臻g中并讓棧指針指向參數(shù)。這時的線程狀態(tài)有點像當被調度器剝奪CPU后一樣,寄存器PC、SP會被保存到類似于進程控制塊的一個結構體struct G內。f被存放在了struct G的entry域,后面進行調度器恢復goroutine的運行,新線程將從f開始執(zhí)行。
和前面說的如果用C實現(xiàn)的差別就在于,沒有使用輔助的結構體,而runtime.newproc
實際上就是help函數(shù)。在函數(shù)協(xié)議上,go表達式調用就比普通的函數(shù)調用多四條指令而已,并且在實際上并沒有為go關鍵字設計一套特殊的東西。不得不說這個做法真的非常精妙!
總結一個,go關鍵字的實現(xiàn)僅僅是一個語法糖衣而已,也就是:
go f(args)
可以看作
runtime.newproc(size, f, args)
更多建議: