核心概念
要理解協程的實現, 首先須要瞭解go中的三個很是重要的概念, 它們分別是G, M和P,
沒有看過golang源代碼的可能會對它們感到陌生, 這三項是協程最主要的組成部分, 它們在golang的源代碼中無處不在.php
G (goroutine)
G是goroutine的頭文字, goroutine能夠解釋爲受管理的輕量線程, goroutine使用go
關鍵詞建立.html
舉例來講, func main() { go other() }
, 這段代碼建立了兩個goroutine,
一個是main, 另外一個是other, 注意main自己也是一個goroutine.linux
goroutine的新建, 休眠, 恢復, 中止都受到go運行時的管理.
goroutine執行異步操做時會進入休眠狀態, 待操做完成後再恢復, 無需佔用系統線程,
goroutine新建或恢復時會添加到運行隊列, 等待M取出並運行.git
M (machine)
M是machine的頭文字, 在當前版本的golang中等同於系統線程.
M能夠運行兩種代碼:github
go代碼, 即goroutine, M運行go代碼須要一個Pgolang
原生代碼, 例如阻塞的syscall, M運行原生代碼不須要Pweb
M會從運行隊列中取出G, 而後運行G, 若是G運行完畢或者進入休眠狀態, 則從運行隊列中取出下一個G運行, 周而復始.
有時候G須要調用一些沒法避免阻塞的原生代碼, 這時M會釋放持有的P並進入阻塞狀態, 其餘M會取得這個P並繼續運行隊列中的G.
go須要保證有足夠的M能夠運行G, 不讓CPU閒着, 也須要保證M的數量不能過多.算法
P (process)
P是process的頭文字, 表明M運行G所須要的資源.
一些講解協程的文章把P理解爲cpu核心, 其實這是錯誤的.
雖然P的數量默認等於cpu核心數, 但能夠經過環境變量GOMAXPROC
修改, 在實際運行時P跟cpu核心並沒有任何關聯.bootstrap
P也能夠理解爲控制go代碼的並行度的機制,
若是P的數量等於1, 表明當前最多隻能有一個線程(M)執行go代碼,
若是P的數量等於2, 表明當前最多隻能有兩個線程(M)執行go代碼.
執行原生代碼的線程數量不受P控制.windows
由於同一時間只有一個線程(M)能夠擁有P, P中的數據都是鎖自由(lock free)的, 讀寫這些數據的效率會很是的高.
數據結構
在講解協程的工做流程以前, 還須要理解一些內部的數據結構.
G的狀態
空閒中(_Gidle): 表示G剛剛新建, 仍未初始化
待運行(_Grunnable): 表示G在運行隊列中, 等待M取出並運行
運行中(_Grunning): 表示M正在運行這個G, 這時候M會擁有一個P
系統調用中(_Gsyscall): 表示M正在運行這個G發起的系統調用, 這時候M並不擁有P
等待中(_Gwaiting): 表示G在等待某些條件完成, 這時候G不在運行也不在運行隊列中(可能在channel的等待隊列中)
已停止(_Gdead): 表示G未被使用, 可能已執行完畢(並在freelist中等待下次複用)
棧複製中(_Gcopystack): 表示G正在獲取一個新的棧空間並把原來的內容複製過去(用於防止GC掃描)
M的狀態
M並無像G和P同樣的狀態標記, 但能夠認爲一個M有如下的狀態:
自旋中(spinning): M正在從運行隊列獲取G, 這時候M會擁有一個P
執行go代碼中: M正在執行go代碼, 這時候M會擁有一個P
執行原生代碼中: M正在執行原生代碼或者阻塞的syscall, 這時M並不擁有P
休眠中: M發現無待運行的G時會進入休眠, 並添加到空閒M鏈表中, 這時M並不擁有P
自旋中(spinning)這個狀態很是重要, 是否須要喚醒或者建立新的M取決於當前自旋中的M的數量.
P的狀態
空閒中(_Pidle): 當M發現無待運行的G時會進入休眠, 這時M擁有的P會變爲空閒並加到空閒P鏈表中
運行中(_Prunning): 當M擁有了一個P後, 這個P的狀態就會變爲運行中, M運行G會使用這個P中的資源
系統調用中(_Psyscall): 當go調用原生代碼, 原生代碼又反過來調用go代碼時, 使用的P會變爲此狀態
GC中止中(_Pgcstop): 當gc中止了整個世界(STW)時, P會變爲此狀態
已停止(_Pdead): 當P的數量在運行時改變, 且數量減小時多餘的P會變爲此狀態
本地運行隊列
在go中有多個運行隊列能夠保存待運行(_Grunnable)的G, 它們分別是各個P中的本地運行隊列和全局運行隊列.
入隊待運行的G時會優先加到當前P的本地運行隊列, M獲取待運行的G時也會優先從擁有的P的本地運行隊列獲取,
本地運行隊列入隊和出隊不須要使用線程鎖.
本地運行隊列有數量限制, 當數量達到256個時會入隊到全局運行隊列.
本地運行隊列的數據結構是環形隊列, 由一個256長度的數組和兩個序號(head, tail)組成.
當M從P的本地運行隊列獲取G時, 若是發現本地隊列爲空會嘗試從其餘P盜取一半的G過來,
這個機制叫作Work Stealing, 詳見後面的代碼分析.
全局運行隊列
全局運行隊列保存在全局變量sched
中, 全局運行隊列入隊和出隊須要使用線程鎖.
全局運行隊列的數據結構是鏈表, 由兩個指針(head, tail)組成.
空閒M鏈表
當M發現無待運行的G時會進入休眠, 並添加到空閒M鏈表中, 空閒M鏈表保存在全局變量sched
.
進入休眠的M會等待一個信號量(m.park), 喚醒休眠的M會使用這個信號量.
go須要保證有足夠的M能夠運行G, 是經過這樣的機制實現的:
入隊待運行的G後, 若是當前無自旋的M可是有空閒的P, 就喚醒或者新建一個M
當M離開自旋狀態並準備運行出隊的G時, 若是當前無自旋的M可是有空閒的P, 就喚醒或者新建一個M
當M離開自旋狀態並準備休眠時, 會在離開自旋狀態後再次檢查全部運行隊列, 若是有待運行的G則從新進入自旋狀態
由於"入隊待運行的G"和"M離開自旋狀態"會同時進行, go會使用這樣的檢查順序:
入隊待運行的G => 內存屏障 => 檢查當前自旋的M數量 => 喚醒或者新建一個M
減小當前自旋的M數量 => 內存屏障 => 檢查全部運行隊列是否有待運行的G => 休眠
這樣能夠保證不會出現待運行的G入隊了, 也有空閒的資源P, 但無M去執行的狀況.
空閒P鏈表
當P的本地運行隊列中的全部G都運行完畢, 又不能從其餘地方拿到G時,
擁有P的M會釋放P並進入休眠狀態, 釋放的P會變爲空閒狀態並加到空閒P鏈表中, 空閒P鏈表保存在全局變量sched
下次待運行的G入隊時若是發現有空閒的P, 可是又沒有自旋中的M時會喚醒或者新建一個M, M會擁有這個P, P會從新變爲運行中的狀態.
工做流程(概覽)
下圖是協程可能出現的工做狀態, 圖中有4個P, 其中M1~M3正在運行G而且運行後會從擁有的P的運行隊列繼續獲取G:
只看這張圖可能有點不可思議實際的工做流程, 這裏我根據實際的代碼再講解一遍:
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
}
程序啓動時會先建立一個G, 指向的是main(實際是runtime.main而不是main.main, 後面解釋):
圖中的虛線指的是G待運行或者開始運行的地址, 不是當前運行的地址.
M會取得這個G並運行:
這時main會建立一個新的channel, 並啓動兩個新的G:
接下來G: main
會從channel獲取數據, 由於獲取不到, G會保存狀態並變爲等待中(_Gwaiting)並添加到channel的隊列:
由於G: main
保存了運行狀態, 下次運行時將會從_ = <- c
繼續運行.
接下來M會從運行隊列獲取到G: printNumber
並運行:
printNumber會打印數字, 完成後向channel寫數據,
寫數據時發現channel中有正在等待的G, 會把數據交給這個G, 把G變爲待運行(_Grunnable)並從新放入運行隊列:
接下來M會運行下一個G: printNumber
, 由於建立channel時指定了大小爲3的緩衝區, 能夠直接把數據寫入緩衝區而無需等待:
而後printNumber運行完畢, 運行隊列中就只剩下G: main
了:
最後M把G: main
取出來運行, 會從上次中斷的位置_ <- c
繼續運行:
第一個_ <- c
的結果已經在前面設置過了, 這條語句會執行成功.
第二個_ <- c
在獲取時會發現channel中有已緩衝的0, 因而結果就是這個0, 不須要等待.
最後main執行完畢, 程序結束.
有人可能會好奇若是最後再加一個_ <- c
會變成什麼結果, 這時由於全部G都進入等待狀態, go會檢測出來並報告死鎖:
fatal error: all goroutines are asleep - deadlock!
開始代碼分析
關於概念的講解到此結束, 從這裏開始會分析go中的實現代碼, 咱們須要先了解一些基礎的內容.
彙編代碼
從如下的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
}
能夠生成如下的彙編代碼(平臺是linux x64, 使用的是默認選項, 即啓用優化和內聯):
(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
這些彙編代碼如今看不懂也不要緊, 下面會從這裏取出一部分來解釋.
調用規範
不一樣平臺對於函數有不一樣的調用規範.
例如32位經過棧傳遞參數, 經過eax寄存器傳遞返回值.
64位windows經過rcx, rdx, r8, r9傳遞前4個參數, 經過棧傳遞第5個開始的參數, 經過eax寄存器傳遞返回值.
64位linux, unix經過rdi, rsi, rdx, rcx, r8, r9傳遞前6個參數, 經過棧傳遞第7個開始的參數, 經過eax寄存器傳遞返回值.
go並不使用這些調用規範(除非涉及到與原生代碼交互), go有一套獨自的調用規範.
go的調用規範很是的簡單, 全部參數都經過棧傳遞, 返回值也經過棧傳遞,
例如這樣的函數:
type MyStruct struct { X int; P *int }
func someFunc(x int, s MyStruct) (int, MyStruct) { ... }
調用函數時的棧的內容以下:
能夠看得出參數和返回值都從低位到高位排列, go函數能夠有多個返回值的緣由也在於此. 由於返回值都經過棧傳遞了.
須要注意的這裏的"返回地址"是x86和x64上的, arm的返回地址會經過LR寄存器保存, 內容會和這裏的稍微不同.
另外注意的是和c不同, 傳遞構造體時整個構造體的內容都會複製到棧上, 若是構造體很大將會影響性能.
TLS
TLS的全稱是Thread-local storage, 表明每一個線程的中的本地數據.
例如標準c中的errno就是一個典型的TLS變量, 每一個線程都有一個獨自的errno, 寫入它不會干擾到其餘線程中的值.
go在實現協程時很是依賴TLS機制, 會用於獲取系統線程中當前的G和G所屬的M的實例.
由於go並不使用glibc, 操做TLS會使用系統原生的接口, 以linux x64爲例,
go在新建M時會調用arch_prctl這個syscall設置FS寄存器的值爲M.tls的地址,
運行中每一個M的FS寄存器都會指向它們對應的M實例的tls, linux內核調度線程時FS寄存器會跟着線程一塊兒切換,
這樣go代碼只須要訪問FS寄存器就能夠存取線程本地的數據.
上面的彙編代碼中的
hello[0x401000] <+0>: movq %fs:-0x8, %rcx
會把指向當前的G的指針從TLS移動到rcx寄存器中.
棧擴張
由於go中的協程是stackful coroutine, 每個goroutine都須要有本身的棧空間,
棧空間的內容在goroutine休眠時須要保留, 待休眠完成後恢復(這時整個調用樹都是完整的).
這樣就引出了一個問題, goroutine可能會同時存在不少個, 若是每個goroutine都預先分配一個足夠的棧空間那麼go就會使用過多的內存.
爲了不這個問題, go在一開始只爲goroutine分配一個很小的棧空間, 它的大小在當前版本是2K.
當函數發現棧空間不足時, 會申請一塊新的棧空間並把原來的棧內容複製過去.
上面的彙編代碼中的
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
會檢查比較rsp減去必定值之後是否比g.stackguard0小, 若是小於等於則須要調到下面調用morestack_noctxt函數.
細心的可能會發現比較的值跟實際減去的值不一致, 這是由於stackguard0下面會預留一小部分空間, 編譯時肯定不超過預留的空間能夠省略比對.
寫屏障(Write Barrier)
由於go支持並行GC, GC的掃描和go代碼能夠同時運行, 這樣帶來的問題是GC掃描的過程當中go代碼有可能改變了對象的依賴樹,
例如開始掃描時發現根對象A和B, B擁有C的指針, GC先掃描A, 而後B把C的指針交給A, GC再掃描B, 這時C就不會被掃描到.
爲了不這個問題, go在GC的標記階段會啓用寫屏障(Write Barrier).
啓用了寫屏障(Write Barrier)後, 當B把C的指針交給A時, GC會認爲在這一輪的掃描中C的指針是存活的,
即便A可能會在稍後丟掉C, 那麼C就在下一輪迴收.
寫屏障只針對指針啓用, 並且只在GC的標記階段啓用, 平時會直接把值寫入到目標地址:
關於寫屏障的詳細將在下一篇(GC篇)分析.
值得一提的是CoreCLR的GC也有寫屏障的機制, 做用跟這裏說明的同樣.
閉包(Closure)
閉包這個概念自己應該不須要解釋, 咱們實際看一看go是如何實現閉包的:
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)
}
這段代碼的輸出結果是3 2 3
, 熟悉go的應該不會感到意外.
main函數執行executeFn函數的彙編代碼以下:
hello[0x4a096f] <+47>: movq $0x1, 0x40(%rsp) ; 變量a等於1
hello[0x4a0978] <+56>: leaq 0x151(%rip), %rax ; 寄存器rax等於匿名函數main.main.func1的地址
hello[0x4a097f] <+63>: movq %rax, 0x60(%rsp) ; 變量rsp+0x60等於匿名函數的地址
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) ; 第一個參數等於地址rsp+0x60
hello[0x4a09a0] <+96>: callq 0x4a08f0 ; 執行main.executeFn
hello[0x4a09a5] <+101>: movq 0x8(%rsp), %rax ; 寄存器rax等於返回值
咱們能夠看到傳給executeFn的是一個指針, 指針指向的內容是[匿名函數的地址, 變量a的地址, 變量b的值]
.
變量a傳地址的緣由是匿名函數中對a進行了修改, 須要反映到原來的a上.
executeFn函數執行閉包的彙編代碼以下:
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 ; 把第一個參數(閉包)的指針移到寄存器rdx
hello[0x4a0912] <+34>: movq (%rdx), %rax ; 把閉包中函數的指針移到寄存器rax
hello[0x4a0915] <+37>: callq *%rax ; 調用閉包中的函數
hello[0x4a0917] <+39>: movq (%rsp), %rax ; 把返回值移到寄存器rax
hello[0x4a091b] <+43>: movq %rax, 0x20(%rsp) ; 把寄存器rax移到返回值中(參數後面)
hello[0x4a0920] <+48>: movq 0x8(%rsp), %rbp ; 把變量rsp+0x8的值恢復寄存器rbp(恢復原rbp)
hello[0x4a0925] <+53>: addq $0x10, %rsp ; 釋放棧空間
hello[0x4a0929] <+57>: retq ; 從函數返回
能夠看到調用閉包時參數並不經過棧傳遞, 而是經過寄存器rdx傳遞, 閉包的彙編代碼以下:
hello[0x455660] <+0>: movq 0x8(%rdx), %rax ; 第一個參數移到寄存器rax(變量a的指針)
hello[0x455664] <+4>: movq (%rax), %rcx ; 把寄存器rax指向的值移到寄存器rcx(變量a的值)
hello[0x455667] <+7>: addq 0x10(%rdx), %rcx ; 添加第二個參數到寄存器rcx(變量a的值+變量b的值)
hello[0x45566b] <+11>: movq %rcx, (%rax) ; 把寄存器rcx移到寄存器rax指向的值(相加的結果保存回變量a)
hello[0x45566e] <+14>: movq %rcx, 0x8(%rsp) ; 把寄存器rcx移到返回結果
hello[0x455673] <+19>: retq ; 從函數返回
閉包的傳遞能夠總結以下:
閉包的內容是[匿名函數的地址, 傳給匿名函數的參數(不定長)...]
傳遞閉包給其餘函數時會傳遞指向"閉包的內容"的指針
調用閉包時會把指向"閉包的內容"的指針放到寄存器rdx(在go內部這個指針稱爲"上下文")
閉包會從寄存器rdx取出參數
若是閉包修改了變量, 閉包中的參數會是指針而不是值, 修改時會修改到原來的位置上
閉包+goroutine
細心的可能會發如今上面的例子中, 閉包的內容在棧上, 若是不是直接調用executeFn而是go executeFn呢?
把上面的代碼改成go executeFn(func() ...)
能夠生成如下的彙編代碼:
hello[0x455611] <+33>: leaq 0xb4a8(%rip), %rax ; 寄存器rax等於類型信息
hello[0x455618] <+40>: movq %rax, (%rsp) ; 第一個參數等於類型信息
hello[0x45561c] <+44>: callq 0x40d910 ; 調用runtime.newobject
hello[0x455621] <+49>: movq 0x8(%rsp), %rax ; 寄存器rax等於返回值(這裏稱爲新對象a)
hello[0x455626] <+54>: movq %rax, 0x28(%rsp) ; 變量rsp+0x28等於新對象a
hello[0x45562b] <+59>: movq $0x1, (%rax) ; 新對象a的值等於1
hello[0x455632] <+66>: leaq 0x136e7(%rip), %rcx ; 寄存器rcx等於類型信息
hello[0x455639] <+73>: movq %rcx, (%rsp) ; 第一個參數等於類型信息
hello[0x45563d] <+77>: callq 0x40d910 ; 調用runtime.newobject
hello[0x455642] <+82>: movq 0x8(%rsp), %rax ; 寄存器rax等於返回值(這裏稱爲新對象fn)
hello[0x455647] <+87>: leaq 0x82(%rip), %rcx ; 寄存器rcx等於匿名函數main.main.func1的地址
hello[0x45564e] <+94>: movq %rcx, (%rax) ; 新對象fn+0的值等於main.main.func1的地址
hello[0x455651] <+97>: testb (%rax), %al ; 確保新對象fn不等於nil
hello[0x455653] <+99>: movl 0x78397(%rip), %ecx ; 寄存器ecx等於當前是否啓用寫屏障
hello[0x455659] <+105>: leaq 0x8(%rax), %rdx ; 寄存器rdx等於新對象fn+0x8的地址
hello[0x45565d] <+109>: testl %ecx, %ecx ; 判斷當前是否啓用寫屏障
hello[0x45565f] <+111>: jne 0x455699 ; 啓用寫屏障時調用後面的邏輯
hello[0x455661] <+113>: movq 0x28(%rsp), %rcx ; 寄存器rcx等於新對象a
hello[0x455666] <+118>: movq %rcx, 0x8(%rax) ; 設置新對象fn+0x8的值等於新對象a
hello[0x45566a] <+122>: movq $0x2, 0x10(%rax) ; 設置新對象fn+0x10的值等於2(變量b的值)
hello[0x455672] <+130>: movq %rax, 0x10(%rsp) ; 第三個參數等於新對象fn(額外參數)
hello[0x455677] <+135>: movl $0x10, (%rsp) ; 第一個參數等於0x10(函數+參數的大小)
hello[0x45567e] <+142>: leaq 0x22fb3(%rip), %rax ; 第二個參數等於一個常量構造體的地址
hello[0x455685] <+149>: movq %rax, 0x8(%rsp) ; 這個構造體的類型是funcval, 值是executeFn的地址
hello[0x45568a] <+154>: callq 0x42e690 ; 調用runtime.newproc建立新的goroutine
咱們能夠看到goroutine+閉包的狀況更復雜, 首先go會經過逃逸分析算出變量a和閉包會逃逸到外面,
這時go會在heap上分配變量a和閉包, 上面調用的兩次newobject就是分別對變量a和閉包的分配.
在建立goroutine時, 首先會傳入函數+參數的大小(上面是8+8=16), 而後傳入函數+參數, 上面的參數即閉包的地址.
m0和g0
go中還有特殊的M和G, 它們是m0和g0.
m0是啓動程序後的主線程, 這個m對應的實例會在全局變量m0中, 不須要在heap上分配,
m0負責執行初始化操做和啓動第一個g, 在以後m0就和其餘的m同樣了.
g0是僅用於負責調度的G, g0不指向任何可執行的函數, 每一個m都會有一個本身的g0,
在調度或系統調用時會使用g0的棧空間, 全局變量的g0是m0的g0.
若是上面的內容都瞭解, 就能夠開始看golang的源代碼了.
程序初始化
go程序的入口點是runtime.rt0_go, 流程是:
分配棧空間, 須要2個本地變量+2個函數參數, 而後向8對齊
把傳入的argc和argv保存到棧上
更新g0中的stackguard的值, stackguard用於檢測棧空間是否不足, 須要分配新的棧空間
獲取當前cpu的信息並保存到各個全局變量
調用_cgo_init若是函數存在
初始化當前線程的TLS, 設置FS寄存器爲m0.tls+8(獲取時會-8)
測試TLS是否工做
設置g0到TLS中, 表示當前的g是g0
設置m0.g0 = g0
設置g0.m = m0
調用runtime.check作一些檢查
調用runtime.args保存傳入的argc和argv到全局變量
調用runtime.osinit根據系統執行不一樣的初始化
這裏(linux x64)設置了全局變量ncpu等於cpu核心數量
調用runtime.schedinit執行共同的初始化
這裏的處理比較多, 會初始化棧空間分配器, GC, 按cpu核心數量或GOMAXPROCS的值生成P等
生成P的處理在procresize中
調用runtime.newproc建立一個新的goroutine, 指向的是
runtime.main
runtime.newproc這個函數在建立普通的goroutine時也會使用, 在下面的"go的實現"中會詳細講解
調用runtime·mstart啓動m0
啓動後m0會不斷從運行隊列獲取G並運行, runtime.mstart調用後不會返回
runtime.mstart這個函數是m的入口點(不只僅是m0), 在下面的"調度器的實現"中會詳細講解
第一個被調度的G會運行runtime.main, 流程是:
標記主函數已調用, 設置mainStarted = true
啓動一個新的M執行sysmon函數, 這個函數會監控全局的狀態並對運行時間過長的G進行搶佔
要求G必須在當前M(系統主線程)上執行
調用runtime_init函數
調用gcenable函數
調用main.init函數, 若是函數存在
再也不要求G必須在當前M上運行
若是程序是做爲c的類庫編譯的, 在這裏返回
調用main.main函數
若是當前發生了panic, 則等待panic處理
調用exit(0)退出程序
G M P的定義
G的定義在這裏.
M的定義在這裏.
P的定義在這裏.
G裏面比較重要的成員以下
stack: 當前g使用的棧空間, 有lo和hi兩個成員
stackguard0: 檢查棧空間是否足夠的值, 低於這個值會擴張棧, 0是go代碼使用的
stackguard1: 檢查棧空間是否足夠的值, 低於這個值會擴張棧, 1是原生代碼使用的
m: 當前g對應的m
sched: g的調度數據, 當g中斷時會保存當前的pc和rsp等值到這裏, 恢復運行時會使用這裏的值
atomicstatus: g的當前狀態
schedlink: 下一個g, 當g在鏈表結構中會使用
preempt: g是否被搶佔中
lockedm: g是否要求要回到這個M執行, 有的時候g中斷了恢復會要求使用原來的M執行
M裏面比較重要的成員以下
g0: 用於調度的特殊g, 調度和執行系統調用時會切換到這個g
curg: 當前運行的g
p: 當前擁有的P
nextp: 喚醒M時, M會擁有這個P
park: M休眠時使用的信號量, 喚醒M時會經過它喚醒
schedlink: 下一個m, 當m在鏈表結構中會使用
mcache: 分配內存時使用的本地分配器, 和p.mcache同樣(擁有P時會複製過來)
lockedg: lockedm的對應值
P裏面比較重要的成員以下
status: p的當前狀態
link: 下一個p, 當p在鏈表結構中會使用
m: 擁有這個P的M
mcache: 分配內存時使用的本地分配器
runqhead: 本地運行隊列的出隊序號
runqtail: 本地運行隊列的入隊序號
runq: 本地運行隊列的數組, 能夠保存256個G
gfree: G的自由列表, 保存變爲_Gdead後能夠複用的G實例
gcBgMarkWorker: 後臺GC的worker函數, 若是它存在M會優先執行它
gcw: GC的本地工做隊列, 詳細將在下一篇(GC篇)分析
go的實現
使用go命令建立goroutine時, go會把go命令編譯爲對runtime.newproc的調用, 堆棧的結構以下:
第一個參數是funcval + 額外參數的長度, 第二個參數是funcval, 後面的都是傳遞給goroutine中執行的函數的額外參數.
funcval的定義在這裏, fn是指向函數機器代碼的指針.
runtime.newproc的處理以下:
計算額外參數的地址argp
獲取調用端的地址(返回地址)pc
使用systemstack調用newproc1
systemstack會切換當前的g到g0, 而且使用g0的棧空間, 而後調用傳入的函數, 再切換回原來的g和原來的棧空間.
切換到g0後會僞裝返回地址是mstart, 這樣traceback的時候能夠在mstart中止.
這裏傳給systemstack的是一個閉包, 調用時會把閉包的地址放到寄存器rdx, 具體能夠參考上面對閉包的分析.
runtime.newproc1的處理以下:
調用getg獲取當前的g, 會編譯爲讀取FS寄存器(TLS), 這裏會獲取到g0
設置g對應的m的locks++, 禁止搶佔
獲取m擁有的p
新建一個g
首先調用gfget從p.gfree獲取g, 若是以前有g被回收在這裏就能夠複用
獲取不到時調用malg分配一個g, 初始的棧空間大小是2K
須要先設置g的狀態爲已停止(_Gdead), 這樣gc不會去掃描這個g的未初始化的棧
把參數複製到g的棧上
把返回地址複製到g的棧上, 這裏的返回地址是goexit, 表示調用完目標函數後會調用goexit
設置g的調度數據(sched)
設置sched.sp等於參數+返回地址後的rsp地址
設置sched.pc等於目標函數的地址, 查看gostartcallfn和gostartcall
設置sched.g等於g
設置g的狀態爲待運行(_Grunnable)
調用runqput把g放到運行隊列
runqputslow會把本地運行隊列中一半的g放到全局運行隊列, 這樣下次就能夠繼續用快速的本地運行隊列了
首先隨機把g放到p.runnext, 若是放到runnext則入隊原來在runnext的g
而後嘗試把g放到P的"本地運行隊列"
若是本地運行隊列滿了則調用runqputslow把g放到"全局運行隊列"
若是當前有空閒的P, 可是無自旋的M(nmspinning等於0), 而且主函數已執行則喚醒或新建一個M
首先交換nmspinning到1, 成功再繼續, 多個線程同時執行wakep只有一個會繼續
調用startm函數
newm會新建一個m的實例, m的實例包含一個g0, 而後調用newosproc動一個系統線程
newosproc會調用syscall clone建立一個新的線程
線程建立後會設置TLS, 設置TLS中當前的g爲g0, 而後執行mstart
調用pidleget從"空閒P鏈表"獲取一個空閒的P
調用mget從"空閒M鏈表"獲取一個空閒的M
若是沒有空閒的M, 則調用newm新建一個M
調用notewakeup(&mp.park)喚醒線程
這一步很是重要, 用於保證當前有足夠的M運行G, 具體請查看上面的"空閒M鏈表"
喚醒或新建一個M會經過wakep函數
建立goroutine的流程就這麼多了, 接下來看看M是如何調度的.
調度器的實現
M啓動時會調用mstart函數, m0在初始化後調用, 其餘的的m在線程啓動後調用.
mstart函數的處理以下:
調用getg獲取當前的g, 這裏會獲取到g0
若是g未分配棧則從當前的棧空間(系統棧空間)上分配, 也就是說g0會使用系統棧空間
調用mstart1函數
調用gosave函數保存當前的狀態到g0的調度數據中, 之後每次調度都會從這個棧地址開始
調用asminit函數, 不作任何事情
調用minit函數, 設置當前線程能夠接收的信號(signal)
調用schedule函數
調用schedule函數後就進入了調度循環, 整個流程能夠簡單總結爲:
schedule函數獲取g => [必要時休眠] => [喚醒後繼續獲取] => execute函數執行g => 執行後返回到goexit => 從新執行schedule函數
schedule函數的處理以下:
若是當前GC須要中止整個世界(STW), 則調用stopm休眠當前的M
若是M擁有的P中指定了須要在安全點運行的函數(P.runSafePointFn), 則運行它
快速獲取待運行的G, 如下處理若是有一個獲取成功後面就不會繼續獲取
若是當前GC正在標記階段, 則查找有沒有待運行的GC Worker, GC Worker也是一個G
爲了公平起見, 每61次調度從全局運行隊列獲取一次G, (一直從本地獲取可能致使全局運行隊列中的G不被運行)
從P的本地運行隊列中獲取G, 調用runqget函數
快速獲取失敗時, 調用findrunnable函數獲取待運行的G, 會阻塞到獲取成功爲止
再次檢查當前GC是否在標記階段, 在則查找有沒有待運行的GC Worker, GC Worker也是一個G
再次檢查若是當前GC須要中止整個世界, 或者P指定了須要再安全點運行的函數, 則跳到findrunnable的頂部重試
再次檢查全局運行隊列中是否有G, 有則獲取並返回
釋放M擁有的P, P會變爲空閒(_Pidle)狀態
把P添加到"空閒P鏈表"中
讓M離開自旋狀態, 這裏的處理很是重要, 參考上面的"空閒M鏈表"
首先減小表示當前自旋中的M的數量的全局變量nmspinning
再次檢查全部P的本地運行隊列, 若是不爲空則讓M從新進入自旋狀態, 並跳到findrunnable的頂部重試
再次檢查有沒有待運行的GC Worker, 有則讓M從新進入自旋狀態, 並跳到findrunnable的頂部重試
再次檢查網絡事件反應器是否有待運行的G, 這裏對netpoll的調用會阻塞, 直到某個fd收到了事件
若是最終仍是獲取不到G, 調用stopm休眠當前的M
喚醒後跳到findrunnable的頂部重試
調用runqsteal嘗試從其餘P的本地運行隊列盜取一半的G
若是當前GC須要中止整個世界(STW), 則調用stopm休眠當前的M
若是M擁有的P中指定了須要在安全點運行的函數(P.runSafePointFn), 則運行它
若是有析構器待運行則使用"運行析構器的G"
從P的本地運行隊列中獲取G, 調用runqget函數
從全局運行隊列獲取G, 調用globrunqget函數, 須要上鎖
從網絡事件反應器獲取G, 函數netpoll會獲取哪些fd可讀可寫或已關閉, 而後返回等待fd相關事件的G
若是獲取不到G, 則執行Work Stealing
若是仍是獲取不到G, 就須要休眠M了, 接下來是休眠的步驟
成功獲取到一個待運行的G
讓M離開自旋狀態, 調用resetspinning, 這裏的處理和上面的不同
若是當前有空閒的P, 可是無自旋的M(nmspinning等於0), 則喚醒或新建一個M
上面離開自旋狀態是爲了休眠M, 因此會再次檢查全部隊列而後休眠
這裏離開自選狀態是爲了執行G, 因此會檢查是否有空閒的P, 有則表示能夠再開新的M執行G
若是G要求回到指定的M(例如上面的runtime.main)
調用startlockedm函數把G和P交給該M, 本身進入休眠
從休眠喚醒後跳到schedule的頂部重試
調用execute函數執行G
execute函數的處理以下:
調用getg獲取當前的g
把G的狀態由待運行(_Grunnable)改成運行中(_Grunning)
設置G的stackguard, 棧空間不足時能夠擴張
增長P中記錄的調度次數(對應上面的每61次優先獲取一次全局運行隊列)
設置g.m.curg = g
設置g.m = m
調用gogo函數
這個函數會根據g.sched中保存的狀態恢復各個寄存器的值並繼續運行g
首先針對g.sched.ctxt調用寫屏障(GC標記指針存活), ctxt中通常會保存指向[函數+參數]的指針
設置TLS中的g爲g.sched.g, 也就是g自身
設置rsp寄存器爲g.sched.rsp
設置rax寄存器爲g.sched.ret
設置rdx寄存器爲g.sched.ctxt (上下文)
設置rbp寄存器爲g.sched.rbp
清空sched中保存的信息
跳轉到g.sched.pc
由於前面建立goroutine的newproc1函數把返回地址設爲了goexit, 函數運行完畢返回時將會調用goexit函數
g.sched.pc在G首次運行時會指向目標函數的第一條機器指令,
若是G被搶佔或者等待資源而進入休眠, 在休眠前會保存狀態到g.sched,
g.sched.pc會變爲喚醒後須要繼續執行的地址, "保存狀態"的實現將在下面講解.
目標函數執行完畢後會調用goexit函數, goexit函數會調用goexit1函數, goexit1函數會經過mcall調用goexit0函數.
mcall這個函數就是用於實現"保存狀態"的, 處理以下:
設置g.sched.pc等於當前的返回地址
設置g.sched.sp等於寄存器rsp的值
設置g.sched.g等於當前的g
設置g.sched.bp等於寄存器rbp的值
切換TLS中當前的g等於m.g0
設置寄存器rsp等於g0.sched.sp, 使用g0的棧空間
設置第一個參數爲原來的g
設置rdx寄存器爲指向函數地址的指針(上下文)
調用指定的函數, 不會返回
mcall這個函數保存當前的運行狀態到g.sched, 而後切換到g0和g0的棧空間, 再調用指定的函數.
回到g0的棧空間這個步驟很是重要, 由於這個時候g已經中斷, 繼續使用g的棧空間且其餘M喚醒了這個g將會產生災難性的後果.
G在中斷或者結束後都會經過mcall回到g0的棧空間繼續調度, 從goexit調用的mcall的保存狀態實際上是多餘的, 由於G已經結束了.
goexit1函數會經過mcall調用goexit0函數, goexit0函數調用時已經回到了g0的棧空間, 處理以下:
把G的狀態由運行中(_Grunning)改成已停止(_Gdead)
清空G的成員
調用dropg函數解除M和G之間的關聯
調用gfput函數把G放到P的自由列表中, 下次建立G時能夠複用
調用schedule函數繼續調度
G結束後回到schedule函數, 這樣就結束了一個調度循環.
不只只有G結束會從新開始調度, G被搶佔或者等待資源也會從新進行調度, 下面繼續來看這兩種狀況.
搶佔的實現
上面我提到了runtime.main會建立一個額外的M運行sysmon函數, 搶佔就是在sysmon中實現的.
sysmon會進入一個無限循環, 第一輪迴休眠20us, 以後每次休眠時間倍增, 最終每一輪都會休眠10ms.
sysmon中有netpool(獲取fd事件), retake(搶佔), forcegc(按時間強制執行gc), scavenge heap(釋放自由列表中多餘的項減小內存佔用)等處理.
retake函數負責處理搶佔, 流程是:
枚舉全部的P
調用preemptone函數
設置g.preempt = true
設置g.stackguard0 = stackPreempt
調用handoffp解除M和P之間的關聯
若是P在系統調用中(_Psyscall), 且通過了一次sysmon循環(20us~10ms), 則搶佔這個P
若是P在運行中(_Prunning), 且通過了一次sysmon循環而且G運行時間超過forcePreemptNS(10ms), 則搶佔這個P
爲何設置了stackguard就能夠實現搶佔?
由於這個值用於檢查當前棧空間是否足夠, go函數的開頭會比對這個值判斷是否須要擴張棧.
stackPreempt是一個特殊的常量, 它的值會比任何的棧地址都要大, 檢查時必定會觸發棧擴張.
棧擴張調用的是morestack_noctxt函數, morestack_noctxt函數清空rdx寄存器並調用morestack函數.
morestack函數會保存G的狀態到g.sched, 切換到g0和g0的棧空間, 而後調用newstack函數.
newstack函數判斷g.stackguard0等於stackPreempt, 就知道這是搶佔觸發的, 這時會再檢查一遍是否要搶佔:
若是M被鎖定(函數的本地變量中有P), 則跳過這一次的搶佔並調用gogo函數繼續運行G
若是M正在分配內存, 則跳過這一次的搶佔並調用gogo函數繼續運行G
若是M設置了當前不能搶佔, 則跳過這一次的搶佔並調用gogo函數繼續運行G
若是M的狀態不是運行中, 則跳過這一次的搶佔並調用gogo函數繼續運行G
即便這一次搶佔失敗, 由於g.preempt等於true, runtime中的一些代碼會從新設置stackPreempt以重試下一次的搶佔.
若是判斷能夠搶佔, 則繼續判斷是否GC引發的, 若是是則對G的棧空間執行標記處理(掃描根對象)而後繼續運行,
若是不是GC引發的則調用gopreempt_m函數完成搶佔.
gopreempt_m函數會調用goschedImpl函數, goschedImpl函數的流程是:
把G的狀態由運行中(_Grunnable)改成待運行(_Grunnable)
調用dropg函數解除M和G之間的關聯
調用globrunqput把G放到全局運行隊列
調用schedule函數繼續調度
由於全局運行隊列的優先度比較低, 各個M會通過一段時間再去從新獲取這個G執行,
搶佔機制保證了不會有一個G長時間的運行致使其餘G沒法運行的狀況發生.
channel的實現
在goroutine運行的過程當中, 有時候須要對資源進行等待, channel就是最典型的資源.
channel的數據定義在這裏, 其中關鍵的成員以下:
qcount: 當前隊列中的元素數量
dataqsiz: 隊列能夠容納的元素數量, 若是爲0表示這個channel無緩衝區
buf: 隊列的緩衝區, 結構是環形隊列
elemsize: 元素的大小
closed: 是否已關閉
elemtype: 元素的類型, 判斷是否調用寫屏障時使用
sendx: 發送元素的序號
recvx: 接收元素的序號
recvq: 當前等待從channel接收數據的G的鏈表(實際類型是sudog的鏈表)
sendq: 當前等待發送數據到channel的G的鏈表(實際類型是sudog的鏈表)
lock: 操做channel時使用的線程鎖
發送數據到channel實際調用的是runtime.chansend1函數, chansend1函數調用了chansend函數, 流程是:
檢查channel.recvq是否有等待中的接收者的G
若是sudog.elem不等於nil, 調用sendDirect函數從發送者直接複製元素
等待接收的sudog.elem是指向接收目標的內存的指針, 若是是接收目標是
_
則elem是nil, 能夠省略複製等待發送的sudog.elem是指向來源目標的內存的指針
複製後調用goready恢復發送者的G
把G的狀態由等待中(_Gwaiting)改成待運行(_Grunnable)
把G放到P的本地運行隊列
若是當前有空閒的P, 可是無自旋的M(nmspinning等於0), 則喚醒或新建一個M
切換到g0調用ready函數, 調用完切換回來
若是有, 表示channel無緩衝區或者緩衝區爲空
調用send函數
從發送者拿到數據並喚醒了G後, 就能夠從chansend返回了
判斷是否能夠把元素放到緩衝區中
若是緩衝區有空餘的空間, 則把元素放到緩衝區並從chansend返回
無緩衝區或緩衝區已經寫滿, 發送者的G須要等待
調用gopark函數
mcall函數和上面說明的同樣, 會把當前的狀態保存到g.sched, 而後切換到g0和g0的棧空間並執行指定的函數
park_m函數首先把G的狀態從運行中(_Grunning)改成等待中(_Gwaiting)
而後調用dropg函數解除M和G之間的關聯
再調用傳入的解鎖函數, 這裏的解鎖函數會對解除channel.lock的鎖定
最後調用schedule函數繼續調度
經過mcall函數調用park_m函數
獲取當前的g
新建一個sudog
設置sudog.elem = 指向發送內存的指針
設置sudog.g = g
設置sudog.c = channel
設置g.waiting = sudog
把sudog放入channel.sendq
調用goparkunlock函數
從這裏恢復表示已經成功發送或者channel已關閉
檢查sudog.param是否爲nil, 若是爲nil表示channel已關閉, 拋出panic
不然釋放sudog而後返回
從channel接收數據實際調用的是runtime.chanrecv1函數, chanrecv1函數調用了chanrecv函數, 流程是:
檢查channel.sendq中是否有等待中的發送者的G
若是無緩衝區, 調用recvDirect函數把元素直接複製給接收者
若是有緩衝區表明緩衝區已滿
複製後調用goready恢復接收者的G, 處理同上
把隊列中下一個要出隊的元素直接複製給接收者
把發送的元素複製到隊列中剛纔出隊的位置
這時候緩衝區仍然是滿的, 可是發送序號和接收序號都會增長1
若是有, 表示channel無緩衝區或者緩衝區已滿, 這兩種狀況須要分別處理(爲了保證入出隊順序一致)
調用recv函數
把數據交給接收者並喚醒了G後, 就能夠從chanrecv返回了
判斷是否能夠從緩衝區獲取元素
若是緩衝區有元素, 則直接取出該元素並從chanrecv返回
無緩衝區或緩衝區無元素, 接收者的G須要等待
獲取當前的g
新建一個sudog
設置sudog.elem = 指向接收內存的指針
設置sudog.g = g
設置sudog.c = channel
設置g.waiting = sudog
把sudog放入channel.recvq
調用goparkunlock函數, 處理同上
從這裏恢復表示已經成功接收或者channel已關閉
檢查sudog.param是否爲nil, 若是爲nil表示channel已關閉
和發送不同的是接收不會拋panic, 會經過返回值通知channel已關閉
釋放sudog而後返回
關閉channel實際調用的是closechan函數, 流程是:
設置channel.closed = 1
枚舉channel.recvq, 清零它們sudog.elem, 設置sudog.param = nil
枚舉channel.sendq, 設置sudog.elem = nil, 設置sudog.param = nil
調用goready函數恢復全部接收者和發送者的G
能夠看到若是G須要等待資源時,
會記錄G的運行狀態到g.sched, 而後把狀態改成等待中(_Gwaiting), 再讓當前的M繼續運行其餘G.
等待中的G保存在哪裏, 何時恢復是等待的資源決定的, 上面對channel的等待會讓G放到channel中的鏈表.
對網絡資源的等待能夠看netpoll相關的處理, netpoll在不一樣系統中的處理都不同, 有興趣的能夠本身看看.
參考連接
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很早就已經開始寫golang內部實現相關的文章了, 他的文章頗有參考價值, 建議同時閱讀他寫的內容.
morsmachine寫的針對協程的分析也建議參考.
golang中的協程實現很是的清晰, 在這裏要再次佩服google工程師的功力, 能夠寫出這樣簡單易懂的代碼不容易.
本文分享自微信公衆號 - golang算法架構leetcode技術php(golangLeetcode)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。