原文來自博客園作者: q303248153
Golang最大的特色可以說是協(xié)程(goroutine)了, 協(xié)程讓本來很復(fù)雜的異步編程變得簡單, 讓程序員不再需要面對(duì)回調(diào)地獄,
雖然現(xiàn)在引入了協(xié)程的語言越來越多, 但go中的協(xié)程仍然是實(shí)現(xiàn)的是最徹底的.
這篇文章將通過分析golang的源代碼來講解協(xié)程的實(shí)現(xiàn)原理.
這個(gè)系列分析的golang源代碼是Google官方的實(shí)現(xiàn)的1.9.2版本, 不適用于其他版本和gccgo等其他實(shí)現(xiàn),
運(yùn)行環(huán)境是Ubuntu 16.04 LTS 64bit.
核心概念
要理解協(xié)程的實(shí)現(xiàn), 首先需要了解go中的三個(gè)非常重要的概念, 它們分別是G, M和P,
沒有看過golang源代碼的可能會(huì)對(duì)它們感到陌生, 這三項(xiàng)是協(xié)程最主要的組成部分, 它們?cè)趃olang的源代碼中無處不在.
G (goroutine)
G是goroutine的頭文字, goroutine可以解釋為受管理的輕量線程, goroutine使用go關(guān)鍵詞創(chuàng)建.
舉例來說, func main() { go other() }, 這段代碼創(chuàng)建了兩個(gè)goroutine,
一個(gè)是main, 另一個(gè)是other, 注意main本身也是一個(gè)goroutine.
goroutine的新建, 休眠, 恢復(fù), 停止都受到go運(yùn)行時(shí)的管理.
goroutine執(zhí)行異步操作時(shí)會(huì)進(jìn)入休眠狀態(tài), 待操作完成后再恢復(fù), 無需占用系統(tǒng)線程,
goroutine新建或恢復(fù)時(shí)會(huì)添加到運(yùn)行隊(duì)列, 等待M取出并運(yùn)行.
M (machine)
M是machine的頭文字, 在當(dāng)前版本的golang中等同于系統(tǒng)線程.
M可以運(yùn)行兩種代碼:
- go代碼, 即goroutine, M運(yùn)行g(shù)o代碼需要一個(gè)P
- 原生代碼, 例如阻塞的syscall, M運(yùn)行原生代碼不需要P
M會(huì)從運(yùn)行隊(duì)列中取出G, 然后運(yùn)行G, 如果G運(yùn)行完畢或者進(jìn)入休眠狀態(tài), 則從運(yùn)行隊(duì)列中取出下一個(gè)G運(yùn)行, 周而復(fù)始.
有時(shí)候G需要調(diào)用一些無法避免阻塞的原生代碼, 這時(shí)M會(huì)釋放持有的P并進(jìn)入阻塞狀態(tài), 其他M會(huì)取得這個(gè)P并繼續(xù)運(yùn)行隊(duì)列中的G.
go需要保證有足夠的M可以運(yùn)行G, 不讓CPU閑著, 也需要保證M的數(shù)量不能過多.
P (process)
P是process的頭文字, 代表M運(yùn)行G所需要的資源.
一些講解協(xié)程的文章把P理解為cpu核心, 其實(shí)這是錯(cuò)誤的.
雖然P的數(shù)量默認(rèn)等于cpu核心數(shù), 但可以通過環(huán)境變量GOMAXPROC修改, 在實(shí)際運(yùn)行時(shí)P跟cpu核心并無任何關(guān)聯(lián).
P也可以理解為控制go代碼的并行度的機(jī)制,
如果P的數(shù)量等于1, 代表當(dāng)前最多只能有一個(gè)線程(M)執(zhí)行g(shù)o代碼,
如果P的數(shù)量等于2, 代表當(dāng)前最多只能有兩個(gè)線程(M)執(zhí)行g(shù)o代碼.
執(zhí)行原生代碼的線程數(shù)量不受P控制.
因?yàn)橥粫r(shí)間只有一個(gè)線程(M)可以擁有P, P中的數(shù)據(jù)都是鎖自由(lock free)的, 讀寫這些數(shù)據(jù)的效率會(huì)非常的高.
數(shù)據(jù)結(jié)構(gòu)
在講解協(xié)程的工作流程之前, 還需要理解一些內(nèi)部的數(shù)據(jù)結(jié)構(gòu).
G的狀態(tài)
- 空閑中(_Gidle): 表示G剛剛新建, 仍未初始化
- 待運(yùn)行(_Grunnable): 表示G在運(yùn)行隊(duì)列中, 等待M取出并運(yùn)行
- 運(yùn)行中(_Grunning): 表示M正在運(yùn)行這個(gè)G, 這時(shí)候M會(huì)擁有一個(gè)P
- 系統(tǒng)調(diào)用中(_Gsyscall): 表示M正在運(yùn)行這個(gè)G發(fā)起的系統(tǒng)調(diào)用, 這時(shí)候M并不擁有P
- 等待中(_Gwaiting): 表示G在等待某些條件完成, 這時(shí)候G不在運(yùn)行也不在運(yùn)行隊(duì)列中(可能在channel的等待隊(duì)列中)
- 已中止(_Gdead): 表示G未被使用, 可能已執(zhí)行完畢(并在freelist中等待下次復(fù)用)
- 棧復(fù)制中(_Gcopystack): 表示G正在獲取一個(gè)新的??臻g并把原來的內(nèi)容復(fù)制過去(用于防止GC掃描)
M的狀態(tài)
M并沒有像G和P一樣的狀態(tài)標(biāo)記, 但可以認(rèn)為一個(gè)M有以下的狀態(tài):
- 自旋中(spinning): M正在從運(yùn)行隊(duì)列獲取G, 這時(shí)候M會(huì)擁有一個(gè)P
- 執(zhí)行g(shù)o代碼中: M正在執(zhí)行g(shù)o代碼, 這時(shí)候M會(huì)擁有一個(gè)P
- 執(zhí)行原生代碼中: M正在執(zhí)行原生代碼或者阻塞的syscall, 這時(shí)M并不擁有P
- 休眠中: M發(fā)現(xiàn)無待運(yùn)行的G時(shí)會(huì)進(jìn)入休眠, 并添加到空閑M鏈表中, 這時(shí)M并不擁有P
自旋中(spinning)這個(gè)狀態(tài)非常重要, 是否需要喚醒或者創(chuàng)建新的M取決于當(dāng)前自旋中的M的數(shù)量.
P的狀態(tài)
- 空閑中(_Pidle): 當(dāng)M發(fā)現(xiàn)無待運(yùn)行的G時(shí)會(huì)進(jìn)入休眠, 這時(shí)M擁有的P會(huì)變?yōu)榭臻e并加到空閑P鏈表中
- 運(yùn)行中(_Prunning): 當(dāng)M擁有了一個(gè)P后, 這個(gè)P的狀態(tài)就會(huì)變?yōu)檫\(yùn)行中, M運(yùn)行G會(huì)使用這個(gè)P中的資源
- 系統(tǒng)調(diào)用中(_Psyscall): 當(dāng)go調(diào)用原生代碼, 原生代碼又反過來調(diào)用go代碼時(shí), 使用的P會(huì)變?yōu)榇藸顟B(tài)
- GC停止中(_Pgcstop): 當(dāng)gc停止了整個(gè)世界(STW)時(shí), P會(huì)變?yōu)榇藸顟B(tài)
- 已中止(_Pdead): 當(dāng)P的數(shù)量在運(yùn)行時(shí)改變, 且數(shù)量減少時(shí)多余的P會(huì)變?yōu)榇藸顟B(tài)
本地運(yùn)行隊(duì)列
在go中有多個(gè)運(yùn)行隊(duì)列可以保存待運(yùn)行(_Grunnable)的G, 它們分別是各個(gè)P中的本地運(yùn)行隊(duì)列和全局運(yùn)行隊(duì)列.
入隊(duì)待運(yùn)行的G時(shí)會(huì)優(yōu)先加到當(dāng)前P的本地運(yùn)行隊(duì)列, M獲取待運(yùn)行的G時(shí)也會(huì)優(yōu)先從擁有的P的本地運(yùn)行隊(duì)列獲取,
本地運(yùn)行隊(duì)列入隊(duì)和出隊(duì)不需要使用線程鎖.
本地運(yùn)行隊(duì)列有數(shù)量限制, 當(dāng)數(shù)量達(dá)到256個(gè)時(shí)會(huì)入隊(duì)到全局運(yùn)行隊(duì)列.
本地運(yùn)行隊(duì)列的數(shù)據(jù)結(jié)構(gòu)是環(huán)形隊(duì)列, 由一個(gè)256長度的數(shù)組和兩個(gè)序號(hào)(head, tail)組成.
當(dāng)M從P的本地運(yùn)行隊(duì)列獲取G時(shí), 如果發(fā)現(xiàn)本地隊(duì)列為空會(huì)嘗試從其他P盜取一半的G過來,
這個(gè)機(jī)制叫做Work Stealing, 詳見后面的代碼分析.
全局運(yùn)行隊(duì)列
全局運(yùn)行隊(duì)列保存在全局變量sched中, 全局運(yùn)行隊(duì)列入隊(duì)和出隊(duì)需要使用線程鎖.
全局運(yùn)行隊(duì)列的數(shù)據(jù)結(jié)構(gòu)是鏈表, 由兩個(gè)指針(head, tail)組成.
空閑M鏈表
當(dāng)M發(fā)現(xiàn)無待運(yùn)行的G時(shí)會(huì)進(jìn)入休眠, 并添加到空閑M鏈表中, 空閑M鏈表保存在全局變量sched.
進(jìn)入休眠的M會(huì)等待一個(gè)信號(hào)量(m.park), 喚醒休眠的M會(huì)使用這個(gè)信號(hào)量.
go需要保證有足夠的M可以運(yùn)行G, 是通過這樣的機(jī)制實(shí)現(xiàn)的:
- 入隊(duì)待運(yùn)行的G后, 如果當(dāng)前無自旋的M但是有空閑的P, 就喚醒或者新建一個(gè)M
- 當(dāng)M離開自旋狀態(tài)并準(zhǔn)備運(yùn)行出隊(duì)的G時(shí), 如果當(dāng)前無自旋的M但是有空閑的P, 就喚醒或者新建一個(gè)M
- 當(dāng)M離開自旋狀態(tài)并準(zhǔn)備休眠時(shí), 會(huì)在離開自旋狀態(tài)后再次檢查所有運(yùn)行隊(duì)列, 如果有待運(yùn)行的G則重新進(jìn)入自旋狀態(tài)
因?yàn)?入隊(duì)待運(yùn)行的G"和"M離開自旋狀態(tài)"會(huì)同時(shí)進(jìn)行, go會(huì)使用這樣的檢查順序:
入隊(duì)待運(yùn)行的G => 內(nèi)存屏障 => 檢查當(dāng)前自旋的M數(shù)量 => 喚醒或者新建一個(gè)M
減少當(dāng)前自旋的M數(shù)量 => 內(nèi)存屏障 => 檢查所有運(yùn)行隊(duì)列是否有待運(yùn)行的G => 休眠
這樣可以保證不會(huì)出現(xiàn)待運(yùn)行的G入隊(duì)了, 也有空閑的資源P, 但無M去執(zhí)行的情況.
空閑P鏈表
當(dāng)P的本地運(yùn)行隊(duì)列中的所有G都運(yùn)行完畢, 又不能從其他地方拿到G時(shí),
擁有P的M會(huì)釋放P并進(jìn)入休眠狀態(tài), 釋放的P會(huì)變?yōu)榭臻e狀態(tài)并加到空閑P鏈表中, 空閑P鏈表保存在全局變量sched
下次待運(yùn)行的G入隊(duì)時(shí)如果發(fā)現(xiàn)有空閑的P, 但是又沒有自旋中的M時(shí)會(huì)喚醒或者新建一個(gè)M, M會(huì)擁有這個(gè)P, P會(huì)重新變?yōu)檫\(yùn)行中的狀態(tài).
工作流程(概覽)
下圖是協(xié)程可能出現(xiàn)的工作狀態(tài), 圖中有4個(gè)P, 其中M1~M3正在運(yùn)行G并且運(yùn)行后會(huì)從擁有的P的運(yùn)行隊(duì)列繼續(xù)獲取G:

只看這張圖可能有點(diǎn)難以想象實(shí)際的工作流程, 這里我根據(jù)實(shí)際的代碼再講解一遍:
package main
import (
"fmt"
"time"
)
func printNumber(from, to int, c chan int) {
for x := from; x <= to; x++ {
fmt.Printf("%d\n", x)
time.Sleep(1 * time.Millisecond)
}
c <- 0
}
func main() {
c := make(chan int, 3)
go printNumber(1, 3, c)
go printNumber(4, 6, c)
_ = <- c
_ = <- c
}
程序啟動(dòng)時(shí)會(huì)先創(chuàng)建一個(gè)G, 指向的是main(實(shí)際是runtime.main而不是main.main, 后面解釋):
圖中的虛線指的是G待運(yùn)行或者開始運(yùn)行的地址, 不是當(dāng)前運(yùn)行的地址.

M會(huì)取得這個(gè)G并運(yùn)行:

這時(shí)main會(huì)創(chuàng)建一個(gè)新的channel, 并啟動(dòng)兩個(gè)新的G:

接下來G: main會(huì)從channel獲取數(shù)據(jù), 因?yàn)楂@取不到, G會(huì)保存狀態(tài)并變?yōu)榈却?_Gwaiting)并添加到channel的隊(duì)列:

因?yàn)?code>G: main保存了運(yùn)行狀態(tài), 下次運(yùn)行時(shí)將會(huì)從_ = <- c繼續(xù)運(yùn)行.
接下來M會(huì)從運(yùn)行隊(duì)列獲取到G: printNumber并運(yùn)行:

printNumber會(huì)打印數(shù)字, 完成后向channel寫數(shù)據(jù),
寫數(shù)據(jù)時(shí)發(fā)現(xiàn)channel中有正在等待的G, 會(huì)把數(shù)據(jù)交給這個(gè)G, 把G變?yōu)榇\(yùn)行(_Grunnable)并重新放入運(yùn)行隊(duì)列:

接下來M會(huì)運(yùn)行下一個(gè)G: printNumber, 因?yàn)閯?chuàng)建channel時(shí)指定了大小為3的緩沖區(qū), 可以直接把數(shù)據(jù)寫入緩沖區(qū)而無需等待:

然后printNumber運(yùn)行完畢, 運(yùn)行隊(duì)列中就只剩下G: main了:

最后M把G: main取出來運(yùn)行, 會(huì)從上次中斷的位置_ <- c繼續(xù)運(yùn)行:

第一個(gè)_ <- c的結(jié)果已經(jīng)在前面設(shè)置過了, 這條語句會(huì)執(zhí)行成功.
第二個(gè)_ <- c在獲取時(shí)會(huì)發(fā)現(xiàn)channel中有已緩沖的0, 于是結(jié)果就是這個(gè)0, 不需要等待.
最后main執(zhí)行完畢, 程序結(jié)束.
有人可能會(huì)好奇如果最后再加一個(gè)_ <- c會(huì)變成什么結(jié)果, 這時(shí)因?yàn)樗蠫都進(jìn)入等待狀態(tài), go會(huì)檢測(cè)出來并報(bào)告死鎖:
fatal error: all goroutines are asleep - deadlock!
開始代碼分析
關(guān)于概念的講解到此結(jié)束, 從這里開始會(huì)分析go中的實(shí)現(xiàn)代碼, 我們需要先了解一些基礎(chǔ)的內(nèi)容.
匯編代碼
從以下的go代碼:
package main
import (
"fmt"
"time"
)
func printNumber(from, to int, c chan int) {
for x := from; x <= to; x++ {
fmt.Printf("%d\n", x)
time.Sleep(1 * time.Millisecond)
}
c <- 0
}
func main() {
c := make(chan int, 3)
go printNumber(1, 3, c)
go printNumber(4, 6, c)
_, _ = <- c, <- c
}
可以生成以下的匯編代碼(平臺(tái)是linux x64, 使用的是默認(rèn)選項(xiàng), 即啟用優(yōu)化和內(nèi)聯(lián)):
(lldb) di -n main.main
hello`main.main:
hello[0x401190] <+0>: movq %fs:-0x8, %rcx
hello[0x401199] <+9>: cmpq 0x10(%rcx), %rsp
hello[0x40119d] <+13>: jbe 0x401291 ; <+257> at hello.go:16
hello[0x4011a3] <+19>: subq $0x40, %rsp
hello[0x4011a7] <+23>: leaq 0xb3632(%rip), %rbx ; runtime.rodata + 38880
hello[0x4011ae] <+30>: movq %rbx, (%rsp)
hello[0x4011b2] <+34>: movq $0x3, 0x8(%rsp)
hello[0x4011bb] <+43>: callq 0x4035a0 ; runtime.makechan at chan.go:49
hello[0x4011c0] <+48>: movq 0x10(%rsp), %rax
hello[0x4011c5] <+53>: movq $0x1, 0x10(%rsp)
hello[0x4011ce] <+62>: movq $0x3, 0x18(%rsp)
hello[0x4011d7] <+71>: movq %rax, 0x38(%rsp)
hello[0x4011dc] <+76>: movq %rax, 0x20(%rsp)
hello[0x4011e1] <+81>: movl $0x18, (%rsp)
hello[0x4011e8] <+88>: leaq 0x129c29(%rip), %rax ; main.printNumber.f
hello[0x4011ef] <+95>: movq %rax, 0x8(%rsp)
hello[0x4011f4] <+100>: callq 0x430cd0 ; runtime.newproc at proc.go:2657
hello[0x4011f9] <+105>: movq $0x4, 0x10(%rsp)
hello[0x401202] <+114>: movq $0x6, 0x18(%rsp)
hello[0x40120b] <+123>: movq 0x38(%rsp), %rbx
hello[0x401210] <+128>: movq %rbx, 0x20(%rsp)
hello[0x401215] <+133>: movl $0x18, (%rsp)
hello[0x40121c] <+140>: leaq 0x129bf5(%rip), %rax ; main.printNumber.f
hello[0x401223] <+147>: movq %rax, 0x8(%rsp)
hello[0x401228] <+152>: callq 0x430cd0 ; runtime.newproc at proc.go:2657
hello[0x40122d] <+157>: movq $0x0, 0x30(%rsp)
hello[0x401236] <+166>: leaq 0xb35a3(%rip), %rbx ; runtime.rodata + 38880
hello[0x40123d] <+173>: movq %rbx, (%rsp)
hello[0x401241] <+177>: movq 0x38(%rsp), %rbx
hello[0x401246] <+182>: movq %rbx, 0x8(%rsp)
hello[0x40124b] <+187>: leaq 0x30(%rsp), %rbx
hello[0x401250] <+192>: movq %rbx, 0x10(%rsp)
hello[0x401255] <+197>: callq 0x4043c0 ; runtime.chanrecv1 at chan.go:354
hello[0x40125a] <+202>: movq $0x0, 0x28(%rsp)
hello[0x401263] <+211>: leaq 0xb3576(%rip), %rbx ; runtime.rodata + 38880
hello[0x40126a] <+218>: movq %rbx, (%rsp)
hello[0x40126e] <+222>: movq 0x38(%rsp), %rbx
hello[0x401273] <+227>: movq %rbx, 0x8(%rsp)
hello[0x401278] <+232>: leaq 0x28(%rsp), %rbx
hello[0x40127d] <+237>: movq %rbx, 0x10(%rsp)
hello[0x401282] <+242>: callq 0x4043c0 ; runtime.chanrecv1 at chan.go:354
hello[0x401287] <+247>: movq 0x28(%rsp), %rbx
hello[0x40128c] <+252>: addq $0x40, %rsp
hello[0x401290] <+256>: retq
hello[0x401291] <+257>: callq 0x4538d0 ; runtime.morestack_noctxt at asm_amd64.s:365
hello[0x401296] <+262>: jmp 0x401190 ; <+0> at hello.go:16
hello[0x40129b] <+267>: int3
hello[0x40129c] <+268>: int3
hello[0x40129d] <+269>: int3
hello[0x40129e] <+270>: int3
hello[0x40129f] <+271>: int3
(lldb) di -n main.printNumber
hello`main.printNumber:
hello[0x401000] <+0>: movq %fs:-0x8, %rcx
hello[0x401009] <+9>: leaq -0x8(%rsp), %rax
hello[0x40100e] <+14>: cmpq 0x10(%rcx), %rax
hello[0x401012] <+18>: jbe 0x401185 ; <+389> at hello.go:8
hello[0x401018] <+24>: subq $0x88, %rsp
hello[0x40101f] <+31>: xorps %xmm0, %xmm0
hello[0x401022] <+34>: movups %xmm0, 0x60(%rsp)
hello[0x401027] <+39>: movq 0x90(%rsp), %rax
hello[0x40102f] <+47>: movq 0x98(%rsp), %rbp
hello[0x401037] <+55>: cmpq %rbp, %rax
hello[0x40103a] <+58>: jg 0x40112f ; <+303> at hello.go:13
hello[0x401040] <+64>: movq %rax, 0x40(%rsp)
hello[0x401045] <+69>: movq %rax, 0x48(%rsp)
hello[0x40104a] <+74>: xorl %ebx, %ebx
hello[0x40104c] <+76>: movq %rbx, 0x60(%rsp)
hello[0x401051] <+81>: movq %rbx, 0x68(%rsp)
hello[0x401056] <+86>: leaq 0x60(%rsp), %rbx
hello[0x40105b] <+91>: cmpq $0x0, %rbx
hello[0x40105f] <+95>: je 0x40117e ; <+382> at hello.go:10
hello[0x401065] <+101>: movq $0x1, 0x78(%rsp)
hello[0x40106e] <+110>: movq $0x1, 0x80(%rsp)
hello[0x40107a] <+122>: movq %rbx, 0x70(%rsp)
hello[0x40107f] <+127>: leaq 0xb73fa(%rip), %rbx ; runtime.rodata + 54400
hello[0x401086] <+134>: movq %rbx, (%rsp)
hello[0x40108a] <+138>: leaq 0x48(%rsp), %rbx
hello[0x40108f] <+143>: movq %rbx, 0x8(%rsp)
hello[0x401094] <+148>: movq $0x0, 0x10(%rsp)
hello[0x40109d] <+157>: callq 0x40bb90 ; runtime.convT2E at iface.go:128
hello[0x4010a2] <+162>: movq 0x18(%rsp), %rcx
hello[0x4010a7] <+167>: movq 0x20(%rsp), %rax
hello[0x4010ac] <+172>: movq 0x70(%rsp), %rbx
hello[0x4010b1] <+177>: movq %rcx, 0x50(%rsp)
hello[0x4010b6] <+182>: movq %rcx, (%rbx)
hello[0x4010b9] <+185>: movq %rax, 0x58(%rsp)
hello[0x4010be] <+190>: cmpb $0x0, 0x19ea1b(%rip) ; time.initdone.
hello[0x4010c5] <+197>: jne 0x401167 ; <+359> at hello.go:10
hello[0x4010cb] <+203>: movq %rax, 0x8(%rbx)
hello[0x4010cf] <+207>: leaq 0xfb152(%rip), %rbx ; go.string.* + 560
hello[0x4010d6] <+214>: movq %rbx, (%rsp)
hello[0x4010da] <+218>: movq $0x3, 0x8(%rsp)
hello[0x4010e3] <+227>: movq 0x70(%rsp), %rbx
hello[0x4010e8] <+232>: movq %rbx, 0x10(%rsp)
hello[0x4010ed] <+237>: movq 0x78(%rsp), %rbx
hello[0x4010f2] <+242>: movq %rbx, 0x18(%rsp)
hello[0x4010f7] <+247>: movq 0x80(%rsp), %rbx
hello[0x4010ff] <+255>: movq %rbx, 0x20(%rsp)
hello[0x401104] <+260>: callq 0x45ad70 ; fmt.Printf at print.go:196
hello[0x401109] <+265>: movq $0xf4240, (%rsp) ; imm = 0xF4240
hello[0x401111] <+273>: callq 0x442a50 ; time.Sleep at time.go:48
hello[0x401116] <+278>: movq 0x40(%rsp), %rax
hello[0x40111b] <+283>: incq %rax
hello[0x40111e] <+286>: movq 0x98(%rsp), %rbp
hello[0x401126] <+294>: cmpq %rbp, %rax
hello[0x401129] <+297>: jle 0x401040 ; <+64> at hello.go:10
hello[0x40112f] <+303>: movq $0x0, 0x48(%rsp)
hello[0x401138] <+312>: leaq 0xb36a1(%rip), %rbx ; runtime.rodata + 38880
hello[0x40113f] <+319>: movq %rbx, (%rsp)
hello[0x401143] <+323>: movq 0xa0(%rsp), %rbx
hello[0x40114b] <+331>: movq %rbx, 0x8(%rsp)
hello[0x401150] <+336>: leaq 0x48(%rsp), %rbx
hello[0x401155] <+341>: movq %rbx, 0x10(%rsp)
hello[0x40115a] <+346>: callq 0x403870 ; runtime.chansend1 at chan.go:99
hello[0x40115f] <+351>: addq $0x88, %rsp
hello[0x401166] <+358>: retq
hello[0x401167] <+359>: leaq 0x8(%rbx), %r8
hello[0x40116b] <+363>: movq %r8, (%rsp)
hello[0x40116f] <+367>: movq %rax, 0x8(%rsp)
hello[0x401174] <+372>: callq 0x40f090 ; runtime.writebarrierptr at mbarrier.go:129
hello[0x401179] <+377>: jmp 0x4010cf ; <+207> at hello.go:10
hello[0x40117e] <+382>: movl %eax, (%rbx)
hello[0x401180] <+384>: jmp 0x401065 ; <+101> at hello.go:10
hello[0x401185] <+389>: callq 0x4538d0 ; runtime.morestack_noctxt at asm_amd64.s:365
hello[0x40118a] <+394>: jmp 0x401000 ; <+0> at hello.go:8
hello[0x40118f] <+399>: int3
這些匯編代碼現(xiàn)在看不懂也沒關(guān)系, 下面會(huì)從這里取出一部分來解釋.
調(diào)用規(guī)范
不同平臺(tái)對(duì)于函數(shù)有不同的調(diào)用規(guī)范.
例如32位通過棧傳遞參數(shù), 通過eax寄存器傳遞返回值.
64位windows通過rcx, rdx, r8, r9傳遞前4個(gè)參數(shù), 通過棧傳遞第5個(gè)開始的參數(shù), 通過eax寄存器傳遞返回值.
64位linux, unix通過rdi, rsi, rdx, rcx, r8, r9傳遞前6個(gè)參數(shù), 通過棧傳遞第7個(gè)開始的參數(shù), 通過eax寄存器傳遞返回值.
go并不使用這些調(diào)用規(guī)范(除非涉及到與原生代碼交互), go有一套獨(dú)自的調(diào)用規(guī)范.
go的調(diào)用規(guī)范非常的簡單, 所有參數(shù)都通過棧傳遞, 返回值也通過棧傳遞,
例如這樣的函數(shù):
type MyStruct struct { X int; P *int }
func someFunc(x int, s MyStruct) (int, MyStruct) { ... }
調(diào)用函數(shù)時(shí)的棧的內(nèi)容如下:

可以看得出參數(shù)和返回值都從低位到高位排列, go函數(shù)可以有多個(gè)返回值的原因也在于此. 因?yàn)榉祷刂刀纪ㄟ^棧傳遞了.
需要注意的這里的"返回地址"是x86和x64上的, arm的返回地址會(huì)通過LR寄存器保存, 內(nèi)容會(huì)和這里的稍微不一樣.
另外注意的是和c不一樣, 傳遞構(gòu)造體時(shí)整個(gè)構(gòu)造體的內(nèi)容都會(huì)復(fù)制到棧上, 如果構(gòu)造體很大將會(huì)影響性能.
TLS
TLS的全稱是Thread-local storage, 代表每個(gè)線程的中的本地?cái)?shù)據(jù).
例如標(biāo)準(zhǔn)c中的errno就是一個(gè)典型的TLS變量, 每個(gè)線程都有一個(gè)獨(dú)自的errno, 寫入它不會(huì)干擾到其他線程中的值.
go在實(shí)現(xiàn)協(xié)程時(shí)非常依賴TLS機(jī)制, 會(huì)用于獲取系統(tǒng)線程中當(dāng)前的G和G所屬的M的實(shí)例.
因?yàn)間o并不使用glibc, 操作TLS會(huì)使用系統(tǒng)原生的接口, 以linux x64為例,
go在新建M時(shí)會(huì)調(diào)用arch_prctl這個(gè)syscall設(shè)置FS寄存器的值為M.tls的地址,
運(yùn)行中每個(gè)M的FS寄存器都會(huì)指向它們對(duì)應(yīng)的M實(shí)例的tls, linux內(nèi)核調(diào)度線程時(shí)FS寄存器會(huì)跟著線程一起切換,
這樣go代碼只需要訪問FS寄存器就可以存取線程本地的數(shù)據(jù).
上面的匯編代碼中的
hello[0x401000] <+0>: movq %fs:-0x8, %rcx
會(huì)把指向當(dāng)前的G的指針從TLS移動(dòng)到rcx寄存器中.
棧擴(kuò)張
因?yàn)間o中的協(xié)程是stackful coroutine, 每一個(gè)goroutine都需要有自己的??臻g,
棧空間的內(nèi)容在goroutine休眠時(shí)需要保留, 待休眠完成后恢復(fù)(這時(shí)整個(gè)調(diào)用樹都是完整的).
這樣就引出了一個(gè)問題, goroutine可能會(huì)同時(shí)存在很多個(gè), 如果每一個(gè)goroutine都預(yù)先分配一個(gè)足夠的??臻g那么go就會(huì)使用過多的內(nèi)存.
為了避免這個(gè)問題, go在一開始只為goroutine分配一個(gè)很小的??臻g, 它的大小在當(dāng)前版本是2K.
當(dāng)函數(shù)發(fā)現(xiàn)??臻g不足時(shí), 會(huì)申請(qǐng)一塊新的??臻g并把原來的棧內(nèi)容復(fù)制過去.
上面的匯編代碼中的
hello[0x401000] <+0>: movq %fs:-0x8, %rcx
hello[0x401009] <+9>: leaq -0x8(%rsp), %rax
hello[0x40100e] <+14>: cmpq 0x10(%rcx), %rax
hello[0x401012] <+18>: jbe 0x401185 ; <+389> at hello.go:8
會(huì)檢查比較rsp減去一定值以后是否比g.stackguard0小, 如果小于等于則需要調(diào)到下面調(diào)用morestack_noctxt函數(shù).
細(xì)心的可能會(huì)發(fā)現(xiàn)比較的值跟實(shí)際減去的值不一致, 這是因?yàn)閟tackguard0下面會(huì)預(yù)留一小部分空間, 編譯時(shí)確定不超過預(yù)留的空間可以省略比對(duì).
寫屏障(Write Barrier)
因?yàn)間o支持并行GC, GC的掃描和go代碼可以同時(shí)運(yùn)行, 這樣帶來的問題是GC掃描的過程中g(shù)o代碼有可能改變了對(duì)象的依賴樹,
例如開始掃描時(shí)發(fā)現(xiàn)根對(duì)象A和B, B擁有C的指針, GC先掃描A, 然后B把C的指針交給A, GC再掃描B, 這時(shí)C就不會(huì)被掃描到.
為了避免這個(gè)問題, go在GC的標(biāo)記階段會(huì)啟用寫屏障(Write Barrier).
啟用了寫屏障(Write Barrier)后, 當(dāng)B把C的指針交給A時(shí), GC會(huì)認(rèn)為在這一輪的掃描中C的指針是存活的,
即使A可能會(huì)在稍后丟掉C, 那么C就在下一輪回收.
寫屏障只針對(duì)指針啟用, 而且只在GC的標(biāo)記階段啟用, 平時(shí)會(huì)直接把值寫入到目標(biāo)地址:
關(guān)于寫屏障的詳細(xì)將在下一篇(GC篇)分析.
值得一提的是CoreCLR的GC也有寫屏障的機(jī)制, 但作用跟這里的不一樣(用于標(biāo)記跨代引用).
閉包(Closure)
閉包這個(gè)概念本身應(yīng)該不需要解釋, 我們實(shí)際看一看go是如何實(shí)現(xiàn)閉包的:
package main
import (
"fmt"
)
func executeFn(fn func() int) int {
return fn();
}
func main() {
a := 1
b := 2
c := executeFn(func() int {
a += b
return a
})
fmt.Printf("%d %d %d\n", a, b, c)
}
這段代碼的輸出結(jié)果是3 2 3, 熟悉go的應(yīng)該不會(huì)感到意外.
main函數(shù)執(zhí)行executeFn函數(shù)的匯編代碼如下:
hello[0x4a096f] <+47>: movq $0x1, 0x40(%rsp) ; 變量a等于1
hello[0x4a0978] <+56>: leaq 0x151(%rip), %rax ; 寄存器rax等于匿名函數(shù)main.main.func1的地址
hello[0x4a097f] <+63>: movq %rax, 0x60(%rsp) ; 變量rsp+0x60等于匿名函數(shù)的地址
hello[0x4a0984] <+68>: leaq 0x40(%rsp), %rax ; 寄存器rax等于變量a的地址
hello[0x4a0989] <+73>: movq %rax, 0x68(%rsp) ; 變量rsp+0x68等于變量a的地址
hello[0x4a098e] <+78>: movq $0x2, 0x70(%rsp) ; 變量rsp+0x70等于2(變量b的值)
hello[0x4a0997] <+87>: leaq 0x60(%rsp), %rax ; 寄存器rax等于地址rsp+0x60
hello[0x4a099c] <+92>: movq %rax, (%rsp) ; 第一個(gè)參數(shù)等于地址rsp+0x60
hello[0x4a09a0] <+96>: callq 0x4a08f0 ; 執(zhí)行main.executeFn
hello[0x4a09a5] <+101>: movq 0x8(%rsp), %rax ; 寄存器rax等于返回值
我們可以看到傳給executeFn的是一個(gè)指針, 指針指向的內(nèi)容是[匿名函數(shù)的地址, 變量a的地址, 變量b的值].
變量a傳地址的原因是匿名函數(shù)中對(duì)a進(jìn)行了修改, 需要反映到原來的a上.
executeFn函數(shù)執(zhí)行閉包的匯編代碼如下:
hello[0x4a08ff] <+15>: subq $0x10, %rsp ; 在棧上分配0x10的空間
hello[0x4a0903] <+19>: movq %rbp, 0x8(%rsp) ; 把原來的寄存器rbp移到變量rsp+0x8
hello[0x4a0908] <+24>: leaq 0x8(%rsp), %rbp ; 把變量rsp+0x8的地址移到寄存器rbp
hello[0x4a090d] <+29>: movq 0x18(%rsp), %rdx ; 把第一個(gè)參數(shù)(閉包)的指針移到寄存器rdx
hello[0x4a0912] <+34>: movq (%rdx), %rax ; 把閉包中函數(shù)的指針移到寄存器rax
hello[0x4a0915] <+37>: callq *%rax ; 調(diào)用閉包中的函數(shù)
hello[0x4a0917] <+39>: movq (%rsp), %rax ; 把返回值移到寄存器rax
hello[0x4a091b] <+43>: movq %rax, 0x20(%rsp) ; 把寄存器rax移到返回值中(參數(shù)后面)
hello[0x4a0920] <+48>: movq 0x8(%rsp), %rbp ; 把變量rsp+0x8的值恢復(fù)寄存器rbp(恢復(fù)原rbp)
hello[0x4a0925] <+53>: addq $0x10, %rsp ; 釋放??臻g
hello[0x4a0929] <+57>: retq ; 從函數(shù)返回
可以看到調(diào)用閉包時(shí)參數(shù)并不通過棧傳遞, 而是通過寄存器rdx傳遞, 閉包的匯編代碼如下:
hello[0x455660] <+0>: movq 0x8(%rdx), %rax ; 第一個(gè)參數(shù)移到寄存器rax(變量a的指針)
hello[0x455664] <+4>: movq (%rax), %rcx ; 把寄存器rax指向的值移到寄存器rcx(變量a的值)
hello[0x455667] <+7>: addq 0x10(%rdx), %rcx ; 添加第二個(gè)參數(shù)到寄存器rcx(變量a的值+變量b的值)
hello[0x45566b] <+11>: movq %rcx, (%rax) ; 把寄存器rcx移到寄存器rax指向的值(相加的結(jié)果保存回變量a)
hello[0x45566e] <+14>: movq %rcx, 0x8(%rsp) ; 把寄存器rcx移到返回結(jié)果
hello[0x455673] <+19>: retq ; 從函數(shù)返回
閉包的傳遞可以總結(jié)如下:
- 閉包的內(nèi)容是[匿名函數(shù)的地址, 傳給匿名函數(shù)的參數(shù)(不定長)...]
- 傳遞閉包給其他函數(shù)時(shí)會(huì)傳遞指向"閉包的內(nèi)容"的指針
- 調(diào)用閉包時(shí)會(huì)把指向"閉包的內(nèi)容"的指針放到寄存器rdx(在go內(nèi)部這個(gè)指針稱為"上下文")
- 閉包會(huì)從寄存器rdx取出參數(shù)
- 如果閉包修改了變量, 閉包中的參數(shù)會(huì)是指針而不是值, 修改時(shí)會(huì)修改到原來的位置上
閉包+goroutine
細(xì)心的可能會(huì)發(fā)現(xiàn)在上面的例子中, 閉包的內(nèi)容在棧上, 如果不是直接調(diào)用executeFn而是go executeFn呢?
把上面的代碼改為go executeFn(func() ...)可以生成以下的匯編代碼:
hello[0x455611] <+33>: leaq 0xb4a8(%rip), %rax ; 寄存器rax等于類型信息
hello[0x455618] <+40>: movq %rax, (%rsp) ; 第一個(gè)參數(shù)等于類型信息
hello[0x45561c] <+44>: callq 0x40d910 ; 調(diào)用runtime.newobject
hello[0x455621] <+49>: movq 0x8(%rsp), %rax ; 寄存器rax等于返回值(這里稱為新對(duì)象a)
hello[0x455626] <+54>: movq %rax, 0x28(%rsp) ; 變量rsp+0x28等于新對(duì)象a
hello[0x45562b] <+59>: movq $0x1, (%rax) ; 新對(duì)象a的值等于1
hello[0x455632] <+66>: leaq 0x136e7(%rip), %rcx ; 寄存器rcx等于類型信息
hello[0x455639] <+73>: movq %rcx, (%rsp) ; 第一個(gè)參數(shù)等于類型信息
hello[0x45563d] <+77>: callq 0x40d910 ; 調(diào)用runtime.newobject
hello[0x455642] <+82>: movq 0x8(%rsp), %rax ; 寄存器rax等于返回值(這里稱為新對(duì)象fn)
hello[0x455647] <+87>: leaq 0x82(%rip), %rcx ; 寄存器rcx等于匿名函數(shù)main.main.func1的地址
hello[0x45564e] <+94>: movq %rcx, (%rax) ; 新對(duì)象fn+0的值等于main.main.func1的地址
hello[0x455651] <+97>: testb (%rax), %al ; 確保新對(duì)象fn不等于nil
hello[0x455653] <+99>: movl 0x78397(%rip), %ecx ; 寄存器ecx等于當(dāng)前是否啟用寫屏障
hello[0x455659] <+105>: leaq 0x8(%rax), %rdx ; 寄存器rdx等于新對(duì)象fn+0x8的地址
hello[0x45565d] <+109>: testl %ecx, %ecx ; 判斷當(dāng)前是否啟用寫屏障
hello[0x45565f] <+111>: jne 0x455699 ; 啟用寫屏障時(shí)調(diào)用后面的邏輯
hello[0x455661] <+113>: movq 0x28(%rsp), %rcx ; 寄存器rcx等于新對(duì)象a
hello[0x455666] <+118>: movq %rcx, 0x8(%rax) ; 設(shè)置新對(duì)象fn+0x8的值等于新對(duì)象a
hello[0x45566a] <+122>: movq $0x2, 0x10(%rax) ; 設(shè)置新對(duì)象fn+0x10的值等于2(變量b的值)
hello[0x455672] <+130>: movq %rax, 0x10(%rsp) ; 第三個(gè)參數(shù)等于新對(duì)象fn(額外參數(shù))
hello[0x455677] <+135>: movl $0x10, (%rsp) ; 第一個(gè)參數(shù)等于0x10(函數(shù)+參數(shù)的大小)
hello[0x45567e] <+142>: leaq 0x22fb3(%rip), %rax ; 第二個(gè)參數(shù)等于一個(gè)常量構(gòu)造體的地址
hello[0x455685] <+149>: movq %rax, 0x8(%rsp) ; 這個(gè)構(gòu)造體的類型是funcval, 值是executeFn的地址
hello[0x45568a] <+154>: callq 0x42e690 ; 調(diào)用runtime.newproc創(chuàng)建新的goroutine
我們可以看到goroutine+閉包的情況更復(fù)雜, 首先go會(huì)通過逃逸分析算出變量a和閉包會(huì)逃逸到外面,
這時(shí)go會(huì)在heap上分配變量a和閉包, 上面調(diào)用的兩次newobject就是分別對(duì)變量a和閉包的分配.
在創(chuàng)建goroutine時(shí), 首先會(huì)傳入函數(shù)+參數(shù)的大小(上面是8+8=16), 然后傳入函數(shù)+參數(shù), 上面的參數(shù)即閉包的地址.
m0和g0
go中還有特殊的M和G, 它們是m0和g0.
m0是啟動(dòng)程序后的主線程, 這個(gè)m對(duì)應(yīng)的實(shí)例會(huì)在全局變量m0中, 不需要在heap上分配,
m0負(fù)責(zé)執(zhí)行初始化操作和啟動(dòng)第一個(gè)g, 在之后m0就和其他的m一樣了.
g0是僅用于負(fù)責(zé)調(diào)度的G, g0不指向任何可執(zhí)行的函數(shù), 每個(gè)m都會(huì)有一個(gè)自己的g0,
在調(diào)度或系統(tǒng)調(diào)用時(shí)會(huì)使用g0的棧空間, 全局變量的g0是m0的g0.
如果上面的內(nèi)容都了解, 就可以開始看golang的源代碼了.
程序初始化
go程序的入口點(diǎn)是runtime.rt0_go, 流程是:
- 分配??臻g, 需要2個(gè)本地變量+2個(gè)函數(shù)參數(shù), 然后向8對(duì)齊
- 把傳入的argc和argv保存到棧上
- 更新g0中的stackguard的值, stackguard用于檢測(cè)??臻g是否不足, 需要分配新的??臻g
- 獲取當(dāng)前cpu的信息并保存到各個(gè)全局變量
- 調(diào)用_cgo_init如果函數(shù)存在
- 初始化當(dāng)前線程的TLS, 設(shè)置FS寄存器為m0.tls+8(獲取時(shí)會(huì)-8)
- 測(cè)試TLS是否工作
- 設(shè)置g0到TLS中, 表示當(dāng)前的g是g0
- 設(shè)置m0.g0 = g0
- 設(shè)置g0.m = m0
- 調(diào)用runtime.check做一些檢查
- 調(diào)用runtime.args保存?zhèn)魅氲腶rgc和argv到全局變量
- 調(diào)用runtime.osinit根據(jù)系統(tǒng)執(zhí)行不同的初始化
- 這里(linux x64)設(shè)置了全局變量ncpu等于cpu核心數(shù)量
- 調(diào)用runtime.schedinit執(zhí)行共同的初始化
- 這里的處理比較多, 會(huì)初始化??臻g分配器, GC, 按cpu核心數(shù)量或GOMAXPROCS的值生成P等
- 生成P的處理在procresize中
- 調(diào)用runtime.newproc創(chuàng)建一個(gè)新的goroutine, 指向的是
runtime.main- runtime.newproc這個(gè)函數(shù)在創(chuàng)建普通的goroutine時(shí)也會(huì)使用, 在下面的"go的實(shí)現(xiàn)"中會(huì)詳細(xì)講解
- 調(diào)用runtime·mstart啟動(dòng)m0
- 啟動(dòng)后m0會(huì)不斷從運(yùn)行隊(duì)列獲取G并運(yùn)行, runtime.mstart調(diào)用后不會(huì)返回
- runtime.mstart這個(gè)函數(shù)是m的入口點(diǎn)(不僅僅是m0), 在下面的"調(diào)度器的實(shí)現(xiàn)"中會(huì)詳細(xì)講解
第一個(gè)被調(diào)度的G會(huì)運(yùn)行runtime.main, 流程是:
- 標(biāo)記主函數(shù)已調(diào)用, 設(shè)置mainStarted = true
- 啟動(dòng)一個(gè)新的M執(zhí)行sysmon函數(shù), 這個(gè)函數(shù)會(huì)監(jiān)控全局的狀態(tài)并對(duì)運(yùn)行時(shí)間過長的G進(jìn)行搶占
- 要求G必須在當(dāng)前M(系統(tǒng)主線程)上執(zhí)行
- 調(diào)用runtime_init函數(shù)
- 調(diào)用gcenable函數(shù)
- 調(diào)用main.init函數(shù), 如果函數(shù)存在
- 不再要求G必須在當(dāng)前M上運(yùn)行
- 如果程序是作為c的類庫編譯的, 在這里返回
- 調(diào)用main.main函數(shù)
- 如果當(dāng)前發(fā)生了panic, 則等待panic處理
- 調(diào)用exit(0)退出程序
G M P的定義
G里面比較重要的成員如下
- stack: 當(dāng)前g使用的??臻g, 有l(wèi)o和hi兩個(gè)成員
- stackguard0: 檢查??臻g是否足夠的值, 低于這個(gè)值會(huì)擴(kuò)張棧, 0是go代碼使用的
- stackguard1: 檢查??臻g是否足夠的值, 低于這個(gè)值會(huì)擴(kuò)張棧, 1是原生代碼使用的
- m: 當(dāng)前g對(duì)應(yīng)的m
- sched: g的調(diào)度數(shù)據(jù), 當(dāng)g中斷時(shí)會(huì)保存當(dāng)前的pc和rsp等值到這里, 恢復(fù)運(yùn)行時(shí)會(huì)使用這里的值
- atomicstatus: g的當(dāng)前狀態(tài)
- schedlink: 下一個(gè)g, 當(dāng)g在鏈表結(jié)構(gòu)中會(huì)使用
- preempt: g是否被搶占中
- lockedm: g是否要求要回到這個(gè)M執(zhí)行, 有的時(shí)候g中斷了恢復(fù)會(huì)要求使用原來的M執(zhí)行
M里面比較重要的成員如下
- g0: 用于調(diào)度的特殊g, 調(diào)度和執(zhí)行系統(tǒng)調(diào)用時(shí)會(huì)切換到這個(gè)g
- curg: 當(dāng)前運(yùn)行的g
- p: 當(dāng)前擁有的P
- nextp: 喚醒M時(shí), M會(huì)擁有這個(gè)P
- park: M休眠時(shí)使用的信號(hào)量, 喚醒M時(shí)會(huì)通過它喚醒
- schedlink: 下一個(gè)m, 當(dāng)m在鏈表結(jié)構(gòu)中會(huì)使用
- mcache: 分配內(nèi)存時(shí)使用的本地分配器, 和p.mcache一樣(擁有P時(shí)會(huì)復(fù)制過來)
- lockedg: lockedm的對(duì)應(yīng)值
P里面比較重要的成員如下
- status: p的當(dāng)前狀態(tài)
- link: 下一個(gè)p, 當(dāng)p在鏈表結(jié)構(gòu)中會(huì)使用
- m: 擁有這個(gè)P的M
- mcache: 分配內(nèi)存時(shí)使用的本地分配器
- runqhead: 本地運(yùn)行隊(duì)列的出隊(duì)序號(hào)
- runqtail: 本地運(yùn)行隊(duì)列的入隊(duì)序號(hào)
- runq: 本地運(yùn)行隊(duì)列的數(shù)組, 可以保存256個(gè)G
- gfree: G的自由列表, 保存變?yōu)開Gdead后可以復(fù)用的G實(shí)例
- gcBgMarkWorker: 后臺(tái)GC的worker函數(shù), 如果它存在M會(huì)優(yōu)先執(zhí)行它
- gcw: GC的本地工作隊(duì)列, 詳細(xì)將在下一篇(GC篇)分析
go的實(shí)現(xiàn)
使用go命令創(chuàng)建goroutine時(shí), go會(huì)把go命令編譯為對(duì)runtime.newproc的調(diào)用, 堆棧的結(jié)構(gòu)如下:

第一個(gè)參數(shù)是funcval + 額外參數(shù)的長度, 第二個(gè)參數(shù)是funcval, 后面的都是傳遞給goroutine中執(zhí)行的函數(shù)的額外參數(shù).
funcval的定義在這里, fn是指向函數(shù)機(jī)器代碼的指針.
runtime.newproc的處理如下:
- 計(jì)算額外參數(shù)的地址argp
- 獲取調(diào)用端的地址(返回地址)pc
- 使用systemstack調(diào)用newproc1
systemstack會(huì)切換當(dāng)前的g到g0, 并且使用g0的??臻g, 然后調(diào)用傳入的函數(shù), 再切換回原來的g和原來的棧空間.
切換到g0后會(huì)假裝返回地址是mstart, 這樣traceback的時(shí)候可以在mstart停止.
這里傳給systemstack的是一個(gè)閉包, 調(diào)用時(shí)會(huì)把閉包的地址放到寄存器rdx, 具體可以參考上面對(duì)閉包的分析.
runtime.newproc1的處理如下:
- 調(diào)用getg獲取當(dāng)前的g, 會(huì)編譯為讀取FS寄存器(TLS), 這里會(huì)獲取到g0
- 設(shè)置g對(duì)應(yīng)的m的locks++, 禁止搶占
- 獲取m擁有的p
- 新建一個(gè)g
- 把參數(shù)復(fù)制到g的棧上
- 把返回地址復(fù)制到g的棧上, 這里的返回地址是goexit, 表示調(diào)用完目標(biāo)函數(shù)后會(huì)調(diào)用goexit
- 設(shè)置g的調(diào)度數(shù)據(jù)(sched)
- 設(shè)置sched.sp等于參數(shù)+返回地址后的rsp地址
- 設(shè)置sched.pc等于目標(biāo)函數(shù)的地址, 查看gostartcallfn和gostartcall
- 設(shè)置sched.g等于g
- 設(shè)置g的狀態(tài)為待運(yùn)行(_Grunnable)
- 調(diào)用runqput把g放到運(yùn)行隊(duì)列
- 首先隨機(jī)把g放到p.runnext, 如果放到runnext則入隊(duì)原來在runnext的g
- 然后嘗試把g放到P的"本地運(yùn)行隊(duì)列"
- 如果本地運(yùn)行隊(duì)列滿了則調(diào)用runqputslow把g放到"全局運(yùn)行隊(duì)列"
- runqputslow會(huì)把本地運(yùn)行隊(duì)列中一半的g放到全局運(yùn)行隊(duì)列, 這樣下次就可以繼續(xù)用快速的本地運(yùn)行隊(duì)列了
-
如果當(dāng)前有空閑的P, 但是無自旋的M(nmspinning等于0), 并且主函數(shù)已執(zhí)行則喚醒或新建一個(gè)M
- 這一步非常重要, 用于保證當(dāng)前有足夠的M運(yùn)行G, 具體請(qǐng)查看上面的"空閑M鏈表"
- 喚醒或新建一個(gè)M會(huì)通過wakep函數(shù)
- 首先交換nmspinning到1, 成功再繼續(xù), 多個(gè)線程同時(shí)執(zhí)行wakep只有一個(gè)會(huì)繼續(xù)
- 調(diào)用startm函數(shù)
- 調(diào)用pidleget從"空閑P鏈表"獲取一個(gè)空閑的P
- 調(diào)用mget從"空閑M鏈表"獲取一個(gè)空閑的M
- 如果沒有空閑的M, 則調(diào)用newm新建一個(gè)M
- newm會(huì)新建一個(gè)m的實(shí)例, m的實(shí)例包含一個(gè)g0, 然后調(diào)用newosproc動(dòng)一個(gè)系統(tǒng)線程
- newosproc會(huì)調(diào)用syscall clone創(chuàng)建一個(gè)新的線程
- 線程創(chuàng)建后會(huì)設(shè)置TLS, 設(shè)置TLS中當(dāng)前的g為g0, 然后執(zhí)行mstart
- 調(diào)用notewakeup(&mp.park)喚醒線程
創(chuàng)建goroutine的流程就這么多了, 接下來看看M是如何調(diào)度的.
調(diào)度器的實(shí)現(xiàn)
M啟動(dòng)時(shí)會(huì)調(diào)用mstart函數(shù), m0在初始化后調(diào)用, 其他的的m在線程啟動(dòng)后調(diào)用.
mstart函數(shù)的處理如下:
- 調(diào)用getg獲取當(dāng)前的g, 這里會(huì)獲取到g0
- 如果g未分配棧則從當(dāng)前的棧空間(系統(tǒng)??臻g)上分配, 也就是說g0會(huì)使用系統(tǒng)棧空間
- 調(diào)用mstart1函數(shù)
調(diào)用schedule函數(shù)后就進(jìn)入了調(diào)度循環(huán), 整個(gè)流程可以簡單總結(jié)為:
schedule函數(shù)獲取g => [必要時(shí)休眠] => [喚醒后繼續(xù)獲取] => execute函數(shù)執(zhí)行g(shù) => 執(zhí)行后返回到goexit => 重新執(zhí)行schedule函數(shù)
schedule函數(shù)的處理如下:
- 如果當(dāng)前GC需要停止整個(gè)世界(STW), 則調(diào)用stopm休眠當(dāng)前的M
- 如果M擁有的P中指定了需要在安全點(diǎn)運(yùn)行的函數(shù)(P.runSafePointFn), 則運(yùn)行它
- 快速獲取待運(yùn)行的G, 以下處理如果有一個(gè)獲取成功后面就不會(huì)繼續(xù)獲取
- 如果當(dāng)前GC正在標(biāo)記階段, 則查找有沒有待運(yùn)行的GC Worker, GC Worker也是一個(gè)G
- 為了公平起見, 每61次調(diào)度從全局運(yùn)行隊(duì)列獲取一次G, (一直從本地獲取可能導(dǎo)致全局運(yùn)行隊(duì)列中的G不被運(yùn)行)
- 從P的本地運(yùn)行隊(duì)列中獲取G, 調(diào)用runqget函數(shù)
- 快速獲取失敗時(shí), 調(diào)用findrunnable函數(shù)獲取待運(yùn)行的G, 會(huì)阻塞到獲取成功為止
- 如果當(dāng)前GC需要停止整個(gè)世界(STW), 則調(diào)用stopm休眠當(dāng)前的M
- 如果M擁有的P中指定了需要在安全點(diǎn)運(yùn)行的函數(shù)(P.runSafePointFn), 則運(yùn)行它
- 如果有析構(gòu)器待運(yùn)行則使用"運(yùn)行析構(gòu)器的G"
- 從P的本地運(yùn)行隊(duì)列中獲取G, 調(diào)用runqget函數(shù)
- 從全局運(yùn)行隊(duì)列獲取G, 調(diào)用globrunqget函數(shù), 需要上鎖
- 從網(wǎng)絡(luò)事件反應(yīng)器獲取G, 函數(shù)netpoll會(huì)獲取哪些fd可讀可寫或已關(guān)閉, 然后返回等待fd相關(guān)事件的G
- 如果獲取不到G, 則執(zhí)行Work Stealing
- 調(diào)用runqsteal嘗試從其他P的本地運(yùn)行隊(duì)列盜取一半的G
- 如果還是獲取不到G, 就需要休眠M(jìn)了, 接下來是休眠的步驟
- 再次檢查當(dāng)前GC是否在標(biāo)記階段, 在則查找有沒有待運(yùn)行的GC Worker, GC Worker也是一個(gè)G
- 再次檢查如果當(dāng)前GC需要停止整個(gè)世界, 或者P指定了需要再安全點(diǎn)運(yùn)行的函數(shù), 則跳到findrunnable的頂部重試
- 再次檢查全局運(yùn)行隊(duì)列中是否有G, 有則獲取并返回
- 釋放M擁有的P, P會(huì)變?yōu)榭臻e(_Pidle)狀態(tài)
- 把P添加到"空閑P鏈表"中
- 讓M離開自旋狀態(tài), 這里的處理非常重要, 參考上面的"空閑M鏈表"
- 首先減少表示當(dāng)前自旋中的M的數(shù)量的全局變量nmspinning
- 再次檢查所有P的本地運(yùn)行隊(duì)列, 如果不為空則讓M重新進(jìn)入自旋狀態(tài), 并跳到findrunnable的頂部重試
- 再次檢查有沒有待運(yùn)行的GC Worker, 有則讓M重新進(jìn)入自旋狀態(tài), 并跳到findrunnable的頂部重試
- 再次檢查網(wǎng)絡(luò)事件反應(yīng)器是否有待運(yùn)行的G, 這里對(duì)netpoll的調(diào)用會(huì)阻塞, 直到某個(gè)fd收到了事件
- 如果最終還是獲取不到G, 調(diào)用stopm休眠當(dāng)前的M
- 喚醒后跳到findrunnable的頂部重試
- 成功獲取到一個(gè)待運(yùn)行的G
-
讓M離開自旋狀態(tài), 調(diào)用resetspinning, 這里的處理和上面的不一樣
- 如果當(dāng)前有空閑的P, 但是無自旋的M(nmspinning等于0), 則喚醒或新建一個(gè)M
- 上面離開自旋狀態(tài)是為了休眠M(jìn), 所以會(huì)再次檢查所有隊(duì)列然后休眠
- 這里離開自選狀態(tài)是為了執(zhí)行G, 所以會(huì)檢查是否有空閑的P, 有則表示可以再開新的M執(zhí)行G
- 如果G要求回到指定的M(例如上面的runtime.main)
- 調(diào)用startlockedm函數(shù)把G和P交給該M, 自己進(jìn)入休眠
- 從休眠喚醒后跳到schedule的頂部重試
- 調(diào)用execute函數(shù)執(zhí)行G
execute函數(shù)的處理如下:
- 調(diào)用getg獲取當(dāng)前的g
- 把G的狀態(tài)由待運(yùn)行(_Grunnable)改為運(yùn)行中(_Grunning)
- 設(shè)置G的stackguard, ??臻g不足時(shí)可以擴(kuò)張
- 增加P中記錄的調(diào)度次數(shù)(對(duì)應(yīng)上面的每61次優(yōu)先獲取一次全局運(yùn)行隊(duì)列)
- 設(shè)置g.m.curg = g
- 設(shè)置g.m = m
- 調(diào)用gogo函數(shù)
- 這個(gè)函數(shù)會(huì)根據(jù)g.sched中保存的狀態(tài)恢復(fù)各個(gè)寄存器的值并繼續(xù)運(yùn)行g(shù)
- 首先針對(duì)g.sched.ctxt調(diào)用寫屏障(GC標(biāo)記指針存活), ctxt中一般會(huì)保存指向[函數(shù)+參數(shù)]的指針
- 設(shè)置TLS中的g為g.sched.g, 也就是g自身
- 設(shè)置rsp寄存器為g.sched.rsp
- 設(shè)置rax寄存器為g.sched.ret
- 設(shè)置rdx寄存器為g.sched.ctxt (上下文)
- 設(shè)置rbp寄存器為g.sched.rbp
- 清空sched中保存的信息
- 跳轉(zhuǎn)到g.sched.pc
- 因?yàn)榍懊鎰?chuàng)建goroutine的newproc1函數(shù)把返回地址設(shè)為了goexit, 函數(shù)運(yùn)行完畢返回時(shí)將會(huì)調(diào)用goexit函數(shù)
g.sched.pc在G首次運(yùn)行時(shí)會(huì)指向目標(biāo)函數(shù)的第一條機(jī)器指令,
如果G被搶占或者等待資源而進(jìn)入休眠, 在休眠前會(huì)保存狀態(tài)到g.sched,
g.sched.pc會(huì)變?yōu)閱拘押笮枰^續(xù)執(zhí)行的地址, "保存狀態(tài)"的實(shí)現(xiàn)將在下面講解.
目標(biāo)函數(shù)執(zhí)行完畢后會(huì)調(diào)用goexit函數(shù), goexit函數(shù)會(huì)調(diào)用goexit1函數(shù), goexit1函數(shù)會(huì)通過mcall調(diào)用goexit0函數(shù).
mcall這個(gè)函數(shù)就是用于實(shí)現(xiàn)"保存狀態(tài)"的, 處理如下:
- 設(shè)置g.sched.pc等于當(dāng)前的返回地址
- 設(shè)置g.sched.sp等于寄存器rsp的值
- 設(shè)置g.sched.g等于當(dāng)前的g
- 設(shè)置g.sched.bp等于寄存器rbp的值
- 切換TLS中當(dāng)前的g等于m.g0
- 設(shè)置寄存器rsp等于g0.sched.sp, 使用g0的棧空間
- 設(shè)置第一個(gè)參數(shù)為原來的g
- 設(shè)置rdx寄存器為指向函數(shù)地址的指針(上下文)
- 調(diào)用指定的函數(shù), 不會(huì)返回
mcall這個(gè)函數(shù)保存當(dāng)前的運(yùn)行狀態(tài)到g.sched, 然后切換到g0和g0的??臻g, 再調(diào)用指定的函數(shù).
回到g0的??臻g這個(gè)步驟非常重要, 因?yàn)檫@個(gè)時(shí)候g已經(jīng)中斷, 繼續(xù)使用g的??臻g且其他M喚醒了這個(gè)g將會(huì)產(chǎn)生災(zāi)難性的后果.
G在中斷或者結(jié)束后都會(huì)通過mcall回到g0的棧空間繼續(xù)調(diào)度, 從goexit調(diào)用的mcall的保存狀態(tài)其實(shí)是多余的, 因?yàn)镚已經(jīng)結(jié)束了.
goexit1函數(shù)會(huì)通過mcall調(diào)用goexit0函數(shù), goexit0函數(shù)調(diào)用時(shí)已經(jīng)回到了g0的棧空間, 處理如下:
- 把G的狀態(tài)由運(yùn)行中(_Grunning)改為已中止(_Gdead)
- 清空G的成員
- 調(diào)用dropg函數(shù)解除M和G之間的關(guān)聯(lián)
- 調(diào)用gfput函數(shù)把G放到P的自由列表中, 下次創(chuàng)建G時(shí)可以復(fù)用
- 調(diào)用schedule函數(shù)繼續(xù)調(diào)度
G結(jié)束后回到schedule函數(shù), 這樣就結(jié)束了一個(gè)調(diào)度循環(huán).
不僅只有G結(jié)束會(huì)重新開始調(diào)度, G被搶占或者等待資源也會(huì)重新進(jìn)行調(diào)度, 下面繼續(xù)來看這兩種情況.
搶占的實(shí)現(xiàn)
上面我提到了runtime.main會(huì)創(chuàng)建一個(gè)額外的M運(yùn)行sysmon函數(shù), 搶占就是在sysmon中實(shí)現(xiàn)的.
sysmon會(huì)進(jìn)入一個(gè)無限循環(huán), 第一輪回休眠20us, 之后每次休眠時(shí)間倍增, 最終每一輪都會(huì)休眠10ms.
sysmon中有netpool(獲取fd事件), retake(搶占), forcegc(按時(shí)間強(qiáng)制執(zhí)行g(shù)c), scavenge heap(釋放自由列表中多余的項(xiàng)減少內(nèi)存占用)等處理.
retake函數(shù)負(fù)責(zé)處理搶占, 流程是:
- 枚舉所有的P
- 如果P在系統(tǒng)調(diào)用中(_Psyscall), 且經(jīng)過了一次sysmon循環(huán)(20us~10ms), 則搶占這個(gè)P
- 調(diào)用handoffp解除M和P之間的關(guān)聯(lián)
- 如果P在運(yùn)行中(_Prunning), 且經(jīng)過了一次sysmon循環(huán)并且G運(yùn)行時(shí)間超過forcePreemptNS(10ms), 則搶占這個(gè)P
- 調(diào)用preemptone函數(shù)
- 設(shè)置g.preempt = true
- 設(shè)置g.stackguard0 = stackPreempt
- 調(diào)用preemptone函數(shù)
- 如果P在系統(tǒng)調(diào)用中(_Psyscall), 且經(jīng)過了一次sysmon循環(huán)(20us~10ms), 則搶占這個(gè)P
為什么設(shè)置了stackguard就可以實(shí)現(xiàn)搶占?
因?yàn)檫@個(gè)值用于檢查當(dāng)前??臻g是否足夠, go函數(shù)的開頭會(huì)比對(duì)這個(gè)值判斷是否需要擴(kuò)張棧.
stackPreempt是一個(gè)特殊的常量, 它的值會(huì)比任何的棧地址都要大, 檢查時(shí)一定會(huì)觸發(fā)棧擴(kuò)張.
棧擴(kuò)張調(diào)用的是morestack_noctxt函數(shù), morestack_noctxt函數(shù)清空rdx寄存器并調(diào)用morestack函數(shù).
morestack函數(shù)會(huì)保存G的狀態(tài)到g.sched, 切換到g0和g0的??臻g, 然后調(diào)用newstack函數(shù).
newstack函數(shù)判斷g.stackguard0等于stackPreempt, 就知道這是搶占觸發(fā)的, 這時(shí)會(huì)再檢查一遍是否要搶占:
- 如果M被鎖定(函數(shù)的本地變量中有P), 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運(yùn)行G
- 如果M正在分配內(nèi)存, 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運(yùn)行G
- 如果M設(shè)置了當(dāng)前不能搶占, 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運(yùn)行G
- 如果M的狀態(tài)不是運(yùn)行中, 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運(yùn)行G
即使這一次搶占失敗, 因?yàn)間.preempt等于true, runtime中的一些代碼會(huì)重新設(shè)置stackPreempt以重試下一次的搶占.
如果判斷可以搶占, 則繼續(xù)判斷是否GC引起的, 如果是則對(duì)G的??臻g執(zhí)行標(biāo)記處理(掃描根對(duì)象)然后繼續(xù)運(yùn)行,
如果不是GC引起的則調(diào)用gopreempt_m函數(shù)完成搶占.
gopreempt_m函數(shù)會(huì)調(diào)用goschedImpl函數(shù), goschedImpl函數(shù)的流程是:
- 把G的狀態(tài)由運(yùn)行中(_Grunnable)改為待運(yùn)行(_Grunnable)
- 調(diào)用dropg函數(shù)解除M和G之間的關(guān)聯(lián)
- 調(diào)用globrunqput把G放到全局運(yùn)行隊(duì)列
- 調(diào)用schedule函數(shù)繼續(xù)調(diào)度
因?yàn)槿诌\(yùn)行隊(duì)列的優(yōu)先度比較低, 各個(gè)M會(huì)經(jīng)過一段時(shí)間再去重新獲取這個(gè)G執(zhí)行,
搶占機(jī)制保證了不會(huì)有一個(gè)G長時(shí)間的運(yùn)行導(dǎo)致其他G無法運(yùn)行的情況發(fā)生.
channel的實(shí)現(xiàn)
在goroutine運(yùn)行的過程中, 有時(shí)候需要對(duì)資源進(jìn)行等待, channel就是最典型的資源.
channel的數(shù)據(jù)定義在這里, 其中關(guān)鍵的成員如下:
- qcount: 當(dāng)前隊(duì)列中的元素?cái)?shù)量
- dataqsiz: 隊(duì)列可以容納的元素?cái)?shù)量, 如果為0表示這個(gè)channel無緩沖區(qū)
- buf: 隊(duì)列的緩沖區(qū), 結(jié)構(gòu)是環(huán)形隊(duì)列
- elemsize: 元素的大小
- closed: 是否已關(guān)閉
- elemtype: 元素的類型, 判斷是否調(diào)用寫屏障時(shí)使用
- sendx: 發(fā)送元素的序號(hào)
- recvx: 接收元素的序號(hào)
- recvq: 當(dāng)前等待從channel接收數(shù)據(jù)的G的鏈表(實(shí)際類型是sudog的鏈表)
- sendq: 當(dāng)前等待發(fā)送數(shù)據(jù)到channel的G的鏈表(實(shí)際類型是sudog的鏈表)
- lock: 操作channel時(shí)使用的線程鎖
發(fā)送數(shù)據(jù)到channel實(shí)際調(diào)用的是runtime.chansend1函數(shù), chansend1函數(shù)調(diào)用了chansend函數(shù), 流程是:
- 檢查channel.recvq是否有等待中的接收者的G
- 如果有, 表示channel無緩沖區(qū)或者緩沖區(qū)為空
- 調(diào)用send函數(shù)
- 如果sudog.elem不等于nil, 調(diào)用sendDirect函數(shù)從發(fā)送者直接復(fù)制元素
- 等待接收的sudog.elem是指向接收目標(biāo)的內(nèi)存的指針, 如果是接收目標(biāo)是
_則elem是nil, 可以省略復(fù)制 - 等待發(fā)送的sudog.elem是指向來源目標(biāo)的內(nèi)存的指針
- 復(fù)制后調(diào)用goready恢復(fù)發(fā)送者的G
- 切換到g0調(diào)用ready函數(shù), 調(diào)用完切換回來
- 把G的狀態(tài)由等待中(_Gwaiting)改為待運(yùn)行(_Grunnable)
- 把G放到P的本地運(yùn)行隊(duì)列
- 如果當(dāng)前有空閑的P, 但是無自旋的M(nmspinning等于0), 則喚醒或新建一個(gè)M
- 切換到g0調(diào)用ready函數(shù), 調(diào)用完切換回來
- 從發(fā)送者拿到數(shù)據(jù)并喚醒了G后, 就可以從chansend返回了
- 判斷是否可以把元素放到緩沖區(qū)中
- 如果緩沖區(qū)有空余的空間, 則把元素放到緩沖區(qū)并從chansend返回
- 無緩沖區(qū)或緩沖區(qū)已經(jīng)寫滿, 發(fā)送者的G需要等待
- 獲取當(dāng)前的g
- 新建一個(gè)sudog
- 設(shè)置sudog.elem = 指向發(fā)送內(nèi)存的指針
- 設(shè)置sudog.g = g
- 設(shè)置sudog.c = channel
- 設(shè)置g.waiting = sudog
- 把sudog放入channel.sendq
- 調(diào)用goparkunlock函數(shù)
- 調(diào)用gopark函數(shù)
- 通過mcall函數(shù)調(diào)用park_m函數(shù)
- mcall函數(shù)和上面說明的一樣, 會(huì)把當(dāng)前的狀態(tài)保存到g.sched, 然后切換到g0和g0的??臻g并執(zhí)行指定的函數(shù)
- park_m函數(shù)首先把G的狀態(tài)從運(yùn)行中(_Grunning)改為等待中(_Gwaiting)
- 然后調(diào)用dropg函數(shù)解除M和G之間的關(guān)聯(lián)
- 再調(diào)用傳入的解鎖函數(shù), 這里的解鎖函數(shù)會(huì)對(duì)解除channel.lock的鎖定
- 最后調(diào)用schedule函數(shù)繼續(xù)調(diào)度
- 通過mcall函數(shù)調(diào)用park_m函數(shù)
- 調(diào)用gopark函數(shù)
- 從這里恢復(fù)表示已經(jīng)成功發(fā)送或者channel已關(guān)閉
- 檢查sudog.param是否為nil, 如果為nil表示channel已關(guān)閉, 拋出panic
- 否則釋放sudog然后返回
從channel接收數(shù)據(jù)實(shí)際調(diào)用的是runtime.chanrecv1函數(shù), chanrecv1函數(shù)調(diào)用了chanrecv函數(shù), 流程是:
- 檢查channel.sendq中是否有等待中的發(fā)送者的G
- 如果有, 表示channel無緩沖區(qū)或者緩沖區(qū)已滿, 這兩種情況需要分別處理(為了保證入出隊(duì)順序一致)
- 調(diào)用recv函數(shù)
- 如果無緩沖區(qū), 調(diào)用recvDirect函數(shù)把元素直接復(fù)制給接收者
- 如果有緩沖區(qū)代表緩沖區(qū)已滿
- 把隊(duì)列中下一個(gè)要出隊(duì)的元素直接復(fù)制給接收者
- 把發(fā)送的元素復(fù)制到隊(duì)列中剛才出隊(duì)的位置
- 這時(shí)候緩沖區(qū)仍然是滿的, 但是發(fā)送序號(hào)和接收序號(hào)都會(huì)增加1
- 復(fù)制后調(diào)用goready恢復(fù)接收者的G, 處理同上
- 把數(shù)據(jù)交給接收者并喚醒了G后, 就可以從chanrecv返回了
- 判斷是否可以從緩沖區(qū)獲取元素
- 如果緩沖區(qū)有元素, 則直接取出該元素并從chanrecv返回
- 無緩沖區(qū)或緩沖區(qū)無元素, 接收者的G需要等待
- 獲取當(dāng)前的g
- 新建一個(gè)sudog
- 設(shè)置sudog.elem = 指向接收內(nèi)存的指針
- 設(shè)置sudog.g = g
- 設(shè)置sudog.c = channel
- 設(shè)置g.waiting = sudog
- 把sudog放入channel.recvq
- 調(diào)用goparkunlock函數(shù), 處理同上
- 從這里恢復(fù)表示已經(jīng)成功接收或者channel已關(guān)閉
- 檢查sudog.param是否為nil, 如果為nil表示channel已關(guān)閉
- 和發(fā)送不一樣的是接收不會(huì)拋panic, 會(huì)通過返回值通知channel已關(guān)閉
- 釋放sudog然后返回
關(guān)閉channel實(shí)際調(diào)用的是closechan函數(shù), 流程是:
- 設(shè)置channel.closed = 1
- 枚舉channel.recvq, 清零它們sudog.elem, 設(shè)置sudog.param = nil
- 枚舉channel.sendq, 設(shè)置sudog.elem = nil, 設(shè)置sudog.param = nil
- 調(diào)用goready函數(shù)恢復(fù)所有接收者和發(fā)送者的G
可以看到如果G需要等待資源時(shí),
會(huì)記錄G的運(yùn)行狀態(tài)到g.sched, 然后把狀態(tài)改為等待中(_Gwaiting), 再讓當(dāng)前的M繼續(xù)運(yùn)行其他G.
等待中的G保存在哪里, 什么時(shí)候恢復(fù)是等待的資源決定的, 上面對(duì)channel的等待會(huì)讓G放到channel中的鏈表.
對(duì)網(wǎng)絡(luò)資源的等待可以看netpoll相關(guān)的處理, netpoll在不同系統(tǒng)中的處理都不一樣, 有興趣的可以自己看看.
參考鏈接
https://github.com/golang/go
https://golang.org/s/go11sched
http://supertech.csail.mit.edu/papers/steal.pdf
https://docs.google.com/document/d/1ETuA2IOmnaQ4j81AtTGT40Y4_Jr6_IDASEKg0t0dBR8/edit#heading=h.x4kziklnb8fr
https://blog.altoros.com/golang-part-1-main-concepts-and-project-structure.html
https://blog.altoros.com/golang-internals-part-2-diving-into-the-go-compiler.html
https://blog.altoros.com/golang-internals-part-3-the-linker-and-object-files.html
https://blog.altoros.com/golang-part-4-object-files-and-function-metadata.html
https://blog.altoros.com/golang-internals-part-5-runtime-bootstrap-process.html
https://blog.altoros.com/golang-internals-part-6-bootstrapping-and-memory-allocator-initialization.html
http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64
http://legendtkl.com/categories/golang
http://www.cnblogs.com/diegodu/p/5803202.html
https://www.douban.com/note/300631999/
http://morsmachine.dk/go-scheduler
legendtkl很早就已經(jīng)開始寫golang內(nèi)部實(shí)現(xiàn)相關(guān)的文章了, 他的文章很有參考價(jià)值, 建議同時(shí)閱讀他寫的內(nèi)容.
morsmachine寫的針對(duì)協(xié)程的分析也建議參考.
golang中的協(xié)程實(shí)現(xiàn)非常的清晰, 在這里要再次佩服google工程師的功力, 可以寫出這樣簡單易懂的代碼不容易.