Golang最大的特點能夠說是協程(goroutine)了, 協程讓原本很複雜的異步編程變得簡單, 讓程序員再也不須要面對回調地獄,
雖然如今引入了協程的語言愈來愈多, 但go中的協程仍然是實現的是最完全的.
這篇文章將經過分析golang的源代碼來說解協程的實現原理.html

這個系列分析的golang源代碼是Google官方的實現的1.9.2版本, 不適用於其餘版本和gccgo等其餘實現,
運行環境是Ubuntu 16.04 LTS 64bit.linux

核心概念

要理解協程的實現, 首先須要瞭解go中的三個很是重要的概念, 它們分別是GMP,
沒有看過golang源代碼的可能會對它們感到陌生, 這三項是協程最主要的組成部分, 它們在golang的源代碼中無處不在.git

G (goroutine)

go
func main() { go other() }

goroutine的新建, 休眠, 恢復, 中止都受到go運行時的管理.
goroutine執行異步操做時會進入休眠狀態, 待操做完成後再恢復, 無需佔用系統線程,
goroutine新建或恢復時會添加到運行隊列, 等待M取出並運行.golang

M (machine)

M是machine的頭文字, 在當前版本的golang中等同於系統線程.
M能夠運行兩種代碼:web

  • go代碼, 即goroutine, M運行go代碼須要一個P
  • 原生代碼, 例如阻塞的syscall, M運行原生代碼不須要P

M會從運行隊列中取出G, 而後運行G, 若是G運行完畢或者進入休眠狀態, 則從運行隊列中取出下一個G運行, 周而復始.
有時候G須要調用一些沒法避免阻塞的原生代碼, 這時M會釋放持有的P並進入阻塞狀態, 其餘M會取得這個P並繼續運行隊列中的G.
go須要保證有足夠的M能夠運行G, 不讓CPU閒着, 也須要保證M的數量不能過多.編程

P (process)

GOMAXPROC

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

空閒M鏈表

sched

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鏈表

sched
工做流程(概覽)

下圖是協程可能出現的工做狀態, 圖中有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

G: main_ = <- cG: printNumber

printNumber會打印數字, 完成後向channel寫數據,
寫數據時發現channel中有正在等待的G, 會把數據交給這個G, 把G變爲待運行(_Grunnable)並從新放入運行隊列:

G: printNumber

G: main

G: main_ <- c

_ <- c_ <- c
_ <- c
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
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等於返回值
[匿名函數的地址, 變量a的地址, 變量b的值]
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

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, 流程是:

runtime.main

第一個被調度的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放到運行隊列
    • 首先隨機把g放到p.runnext, 若是放到runnext則入隊原來在runnext的g
    • 而後嘗試把g放到P的"本地運行隊列"
    • 若是本地運行隊列滿了則調用runqputslow把g放到"全局運行隊列"
      • runqputslow會把本地運行隊列中一半的g放到全局運行隊列, 這樣下次就能夠繼續用快速的本地運行隊列了
  • 若是當前有空閒的P, 可是無自旋的M(nmspinning等於0), 而且主函數已執行則喚醒或新建一個M
    • 這一步很是重要, 用於保證當前有足夠的M運行G, 具體請查看上面的"空閒M鏈表"
    • 喚醒或新建一個M會經過wakep函數
      • 首先交換nmspinning到1, 成功再繼續, 多個線程同時執行wakep只有一個會繼續
      • 調用startm函數
        • 調用pidleget從"空閒P鏈表"獲取一個空閒的P
        • 調用mget從"空閒M鏈表"獲取一個空閒的M
        • 若是沒有空閒的M, 則調用newm新建一個M
          • newm會新建一個m的實例, m的實例包含一個g0, 而後調用newosproc動一個系統線程
          • newosproc會調用syscall clone建立一個新的線程
          • 線程建立後會設置TLS, 設置TLS中當前的g爲g0, 而後執行mstart
        • 調用notewakeup(&mp.park)喚醒線程

建立goroutine的流程就這麼多了, 接下來看看M是如何調度的.

調度器的實現

M啓動時會調用mstart函數, m0在初始化後調用, 其餘的的m在線程啓動後調用.
mstart函數的處理以下:

  • 調用getg獲取當前的g, 這裏會獲取到g0
  • 若是g未分配棧則從當前的棧空間(系統棧空間)上分配, 也就是說g0會使用系統棧空間
  • 調用mstart1函數

調用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須要中止整個世界(STW), 則調用stopm休眠當前的M
    • 若是M擁有的P中指定了須要在安全點運行的函數(P.runSafePointFn), 則運行它
    • 若是有析構器待運行則使用"運行析構器的G"
    • 從P的本地運行隊列中獲取G, 調用runqget函數
    • 從全局運行隊列獲取G, 調用globrunqget函數, 須要上鎖
    • 從網絡事件反應器獲取G, 函數netpoll會獲取哪些fd可讀可寫或已關閉, 而後返回等待fd相關事件的G
    • 若是獲取不到G, 則執行Work Stealing
    • 若是仍是獲取不到G, 就須要休眠M了, 接下來是休眠的步驟
      • 再次檢查當前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的頂部重試
  • 成功獲取到一個待運行的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
    • 若是P在系統調用中(_Psyscall), 且通過了一次sysmon循環(20us~10ms), 則搶佔這個P
    • 若是P在運行中(_Prunning), 且通過了一次sysmon循環而且G運行時間超過forcePreemptNS(10ms), 則搶佔這個P
      • 調用preemptone函數
        • 設置g.preempt = true
        • 設置g.stackguard0 = stackPreempt

爲何設置了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接收數據實際調用的是runtime.chanrecv1函數, chanrecv1函數調用了chanrecv函數, 流程是:

  • 檢查channel.sendq中是否有等待中的發送者的G
    • 若是有, 表示channel無緩衝區或者緩衝區已滿, 這兩種狀況須要分別處理(爲了保證入出隊順序一致)
    • 調用recv函數
      • 若是無緩衝區, 調用recvDirect函數把元素直接複製給接收者
      • 若是有緩衝區表明緩衝區已滿
        • 把隊列中下一個要出隊的元素直接複製給接收者
        • 把發送的元素複製到隊列中剛纔出隊的位置
        • 這時候緩衝區仍然是滿的, 可是發送序號和接收序號都會增長1
      • 複製後調用goready恢復接收者的G, 處理同上
    • 把數據交給接收者並喚醒了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在不一樣系統中的處理都不同, 有興趣的能夠本身看看.

參考連接

legendtkl很早就已經開始寫golang內部實現相關的文章了, 他的文章頗有參考價值, 建議同時閱讀他寫的內容.
morsmachine寫的針對協程的分析也建議參考.
golang中的協程實現很是的清晰, 在這裏要再次佩服google工程師的功力, 能夠寫出這樣簡單易懂的代碼不容易.