国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

golang 協(xié)程的實現(xiàn)原理

這篇具有很好參考價值的文章主要介紹了golang 協(xié)程的實現(xiàn)原理。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

核心概念

要理解協(xié)程的實現(xiàn), 首先需要了解go中的三個非常重要的概念, 它們分別是G,?MP,
沒有看過golang源代碼的可能會對它們感到陌生, 這三項是協(xié)程最主要的組成部分, 它們在golang的源代碼中無處不在.

G (goroutine)

G是goroutine的頭文字, goroutine可以解釋為受管理的輕量線程, goroutine使用go關(guān)鍵詞創(chuàng)建.

舉例來說,?func main() { go other() }, 這段代碼創(chuàng)建了兩個goroutine,
一個是main, 另一個是other, 注意main本身也是一個goroutine.

goroutine的新建, 休眠, 恢復(fù), 停止都受到go運行時的管理.
goroutine執(zhí)行異步操作時會進入休眠狀態(tài), 待操作完成后再恢復(fù), 無需占用系統(tǒng)線程,
goroutine新建或恢復(fù)時會添加到運行隊列, 等待M取出并運行.

需要框架源碼的朋友可以看我個人簡介聯(lián)系我,推薦分布式架構(gòu)源碼。?

golang 協(xié)程的實現(xiàn)原理

M (machine)

M是machine的頭文字, 在當前版本的golang中等同于系統(tǒng)線程.
M可以運行兩種代碼:

  • go代碼, 即goroutine, M運行g(shù)o代碼需要一個P

  • 原生代碼, 例如阻塞的syscall, M運行原生代碼不需要P

M會從運行隊列中取出G, 然后運行G, 如果G運行完畢或者進入休眠狀態(tài), 則從運行隊列中取出下一個G運行, 周而復(fù)始.
有時候G需要調(diào)用一些無法避免阻塞的原生代碼, 這時M會釋放持有的P并進入阻塞狀態(tài), 其他M會取得這個P并繼續(xù)運行隊列中的G.
go需要保證有足夠的M可以運行G, 不讓CPU閑著, 也需要保證M的數(shù)量不能過多.

P (process)

P是process的頭文字, 代表M運行G所需要的資源.
一些講解協(xié)程的文章把P理解為cpu核心, 其實這是錯誤的.
雖然P的數(shù)量默認等于cpu核心數(shù), 但可以通過環(huán)境變量GOMAXPROC修改, 在實際運行時P跟cpu核心并無任何關(guān)聯(lián).

P也可以理解為控制go代碼的并行度的機制,
如果P的數(shù)量等于1, 代表當前最多只能有一個線程(M)執(zhí)行g(shù)o代碼,
如果P的數(shù)量等于2, 代表當前最多只能有兩個線程(M)執(zhí)行g(shù)o代碼.
執(zhí)行原生代碼的線程數(shù)量不受P控制.

因為同一時間只有一個線程(M)可以擁有P, P中的數(shù)據(jù)都是鎖自由(lock free)的, 讀寫這些數(shù)據(jù)的效率會非常的高.

數(shù)據(jù)結(jié)構(gòu)

在講解協(xié)程的工作流程之前, 還需要理解一些內(nèi)部的數(shù)據(jù)結(jié)構(gòu).

G的狀態(tài)

  • 空閑中(_Gidle): 表示G剛剛新建, 仍未初始化

  • 待運行(_Grunnable): 表示G在運行隊列中, 等待M取出并運行

  • 運行中(_Grunning): 表示M正在運行這個G, 這時候M會擁有一個P

  • 系統(tǒng)調(diào)用中(_Gsyscall): 表示M正在運行這個G發(fā)起的系統(tǒng)調(diào)用, 這時候M并不擁有P

  • 等待中(_Gwaiting): 表示G在等待某些條件完成, 這時候G不在運行也不在運行隊列中(可能在channel的等待隊列中)

  • 已中止(_Gdead): 表示G未被使用, 可能已執(zhí)行完畢(并在freelist中等待下次復(fù)用)

  • 棧復(fù)制中(_Gcopystack): 表示G正在獲取一個新的棧空間并把原來的內(nèi)容復(fù)制過去(用于防止GC掃描)

M的狀態(tài)

M并沒有像G和P一樣的狀態(tài)標記, 但可以認為一個M有以下的狀態(tài):

  • 自旋中(spinning): M正在從運行隊列獲取G, 這時候M會擁有一個P

  • 執(zhí)行g(shù)o代碼中: M正在執(zhí)行g(shù)o代碼, 這時候M會擁有一個P

  • 執(zhí)行原生代碼中: M正在執(zhí)行原生代碼或者阻塞的syscall, 這時M并不擁有P

  • 休眠中: M發(fā)現(xiàn)無待運行的G時會進入休眠, 并添加到空閑M鏈表中, 這時M并不擁有P

自旋中(spinning)這個狀態(tài)非常重要, 是否需要喚醒或者創(chuàng)建新的M取決于當前自旋中的M的數(shù)量.

P的狀態(tài)

  • 空閑中(_Pidle): 當M發(fā)現(xiàn)無待運行的G時會進入休眠, 這時M擁有的P會變?yōu)榭臻e并加到空閑P鏈表中

  • 運行中(_Prunning): 當M擁有了一個P后, 這個P的狀態(tài)就會變?yōu)檫\行中, M運行G會使用這個P中的資源

  • 系統(tǒng)調(diào)用中(_Psyscall): 當go調(diào)用原生代碼, 原生代碼又反過來調(diào)用go代碼時, 使用的P會變?yōu)榇藸顟B(tài)

  • GC停止中(_Pgcstop): 當gc停止了整個世界(STW)時, P會變?yōu)榇藸顟B(tài)

  • 已中止(_Pdead): 當P的數(shù)量在運行時改變, 且數(shù)量減少時多余的P會變?yōu)榇藸顟B(tài)

本地運行隊列

在go中有多個運行隊列可以保存待運行(_Grunnable)的G, 它們分別是各個P中的本地運行隊列和全局運行隊列.
入隊待運行的G時會優(yōu)先加到當前P的本地運行隊列, M獲取待運行的G時也會優(yōu)先從擁有的P的本地運行隊列獲取,
本地運行隊列入隊和出隊不需要使用線程鎖.

本地運行隊列有數(shù)量限制, 當數(shù)量達到256個時會入隊到全局運行隊列.
本地運行隊列的數(shù)據(jù)結(jié)構(gòu)是環(huán)形隊列, 由一個256長度的數(shù)組和兩個序號(head, tail)組成.

當M從P的本地運行隊列獲取G時, 如果發(fā)現(xiàn)本地隊列為空會嘗試從其他P盜取一半的G過來,
這個機制叫做Work Stealing, 詳見后面的代碼分析.

全局運行隊列

全局運行隊列保存在全局變量sched中, 全局運行隊列入隊和出隊需要使用線程鎖.
全局運行隊列的數(shù)據(jù)結(jié)構(gòu)是鏈表, 由兩個指針(head, tail)組成.

空閑M鏈表

當M發(fā)現(xiàn)無待運行的G時會進入休眠, 并添加到空閑M鏈表中, 空閑M鏈表保存在全局變量sched.
進入休眠的M會等待一個信號量(m.park), 喚醒休眠的M會使用這個信號量.

go需要保證有足夠的M可以運行G, 是通過這樣的機制實現(xiàn)的:

  • 入隊待運行的G后, 如果當前無自旋的M但是有空閑的P, 就喚醒或者新建一個M

  • 當M離開自旋狀態(tài)并準備運行出隊的G時, 如果當前無自旋的M但是有空閑的P, 就喚醒或者新建一個M

  • 當M離開自旋狀態(tài)并準備休眠時, 會在離開自旋狀態(tài)后再次檢查所有運行隊列, 如果有待運行的G則重新進入自旋狀態(tài)

因為"入隊待運行的G"和"M離開自旋狀態(tài)"會同時進行, go會使用這樣的檢查順序:

入隊待運行的G => 內(nèi)存屏障 => 檢查當前自旋的M數(shù)量 => 喚醒或者新建一個M
減少當前自旋的M數(shù)量 => 內(nèi)存屏障 => 檢查所有運行隊列是否有待運行的G => 休眠

這樣可以保證不會出現(xiàn)待運行的G入隊了, 也有空閑的資源P, 但無M去執(zhí)行的情況.

空閑P鏈表

當P的本地運行隊列中的所有G都運行完畢, 又不能從其他地方拿到G時,
擁有P的M會釋放P并進入休眠狀態(tài), 釋放的P會變?yōu)榭臻e狀態(tài)并加到空閑P鏈表中, 空閑P鏈表保存在全局變量sched
下次待運行的G入隊時如果發(fā)現(xiàn)有空閑的P, 但是又沒有自旋中的M時會喚醒或者新建一個M, M會擁有這個P, P會重新變?yōu)檫\行中的狀態(tài).

工作流程(概覽)

下圖是協(xié)程可能出現(xiàn)的工作狀態(tài), 圖中有4個P, 其中M1~M3正在運行G并且運行后會從擁有的P的運行隊列繼續(xù)獲取G:

golang 協(xié)程的實現(xiàn)原理

只看這張圖可能有點難以想象實際的工作流程, 這里我根據(jù)實際的代碼再講解一遍:

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
}

程序啟動時會先創(chuàng)建一個G, 指向的是main(實際是runtime.main而不是main.main, 后面解釋):
圖中的虛線指的是G待運行或者開始運行的地址, 不是當前運行的地址.

golang 協(xié)程的實現(xiàn)原理

M會取得這個G并運行:golang 協(xié)程的實現(xiàn)原理

這時main會創(chuàng)建一個新的channel, 并啟動兩個新的G:golang 協(xié)程的實現(xiàn)原理

接下來G: main會從channel獲取數(shù)據(jù), 因為獲取不到, G會保存狀態(tài)并變?yōu)榈却?_Gwaiting)并添加到channel的隊列:golang 協(xié)程的實現(xiàn)原理

因為G: main保存了運行狀態(tài), 下次運行時將會從_ = <- c繼續(xù)運行.
接下來M會從運行隊列獲取到G: printNumber并運行:golang 協(xié)程的實現(xiàn)原理

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

接下來M會運行下一個G: printNumber, 因為創(chuàng)建channel時指定了大小為3的緩沖區(qū), 可以直接把數(shù)據(jù)寫入緩沖區(qū)而無需等待:golang 協(xié)程的實現(xiàn)原理

然后printNumber運行完畢, 運行隊列中就只剩下G: main了:golang 協(xié)程的實現(xiàn)原理

最后M把G: main取出來運行, 會從上次中斷的位置_ <- c繼續(xù)運行:golang 協(xié)程的實現(xiàn)原理

第一個_ <- c的結(jié)果已經(jīng)在前面設(shè)置過了, 這條語句會執(zhí)行成功.
第二個_ <- c在獲取時會發(fā)現(xiàn)channel中有已緩沖的0, 于是結(jié)果就是這個0, 不需要等待.
最后main執(zhí)行完畢, 程序結(jié)束.

有人可能會好奇如果最后再加一個_ <- c會變成什么結(jié)果, 這時因為所有G都進入等待狀態(tài), go會檢測出來并報告死鎖:

fatal error: all goroutines are asleep - deadlock!

開始代碼分析

關(guān)于概念的講解到此結(jié)束, 從這里開始會分析go中的實現(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
}

可以生成以下的匯編代碼(平臺是linux x64, 使用的是默認選項, 即啟用優(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)系, 下面會從這里取出一部分來解釋.

調(diào)用規(guī)范

不同平臺對于函數(shù)有不同的調(diào)用規(guī)范.
例如32位通過棧傳遞參數(shù), 通過eax寄存器傳遞返回值.
64位windows通過rcx, rdx, r8, r9傳遞前4個參數(shù), 通過棧傳遞第5個開始的參數(shù), 通過eax寄存器傳遞返回值.
64位linux, unix通過rdi, rsi, rdx, rcx, r8, r9傳遞前6個參數(shù), 通過棧傳遞第7個開始的參數(shù), 通過eax寄存器傳遞返回值.
go并不使用這些調(diào)用規(guī)范(除非涉及到與原生代碼交互), go有一套獨自的調(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ù)時的棧的內(nèi)容如下:golang 協(xié)程的實現(xiàn)原理

可以看得出參數(shù)和返回值都從低位到高位排列, go函數(shù)可以有多個返回值的原因也在于此. 因為返回值都通過棧傳遞了.
需要注意的這里的"返回地址"是x86和x64上的, arm的返回地址會通過LR寄存器保存, 內(nèi)容會和這里的稍微不一樣.
另外注意的是和c不一樣, 傳遞構(gòu)造體時整個構(gòu)造體的內(nèi)容都會復(fù)制到棧上, 如果構(gòu)造體很大將會影響性能.

TLS

TLS的全稱是Thread-local storage, 代表每個線程的中的本地數(shù)據(jù).
例如標準c中的errno就是一個典型的TLS變量, 每個線程都有一個獨自的errno, 寫入它不會干擾到其他線程中的值.
go在實現(xiàn)協(xié)程時非常依賴TLS機制, 會用于獲取系統(tǒng)線程中當前的G和G所屬的M的實例.

因為go并不使用glibc, 操作TLS會使用系統(tǒng)原生的接口, 以linux x64為例,
go在新建M時會調(diào)用arch_prctl這個syscall設(shè)置FS寄存器的值為M.tls的地址,
運行中每個M的FS寄存器都會指向它們對應(yīng)的M實例的tls, linux內(nèi)核調(diào)度線程時FS寄存器會跟著線程一起切換,
這樣go代碼只需要訪問FS寄存器就可以存取線程本地的數(shù)據(jù).

上面的匯編代碼中的

hello[0x401000] <+0>:   movq   %fs:-0x8, %rcx

會把指向當前的G的指針從TLS移動到rcx寄存器中.

棧擴張

因為go中的協(xié)程是stackful coroutine, 每一個goroutine都需要有自己的??臻g,
??臻g的內(nèi)容在goroutine休眠時需要保留, 待休眠完成后恢復(fù)(這時整個調(diào)用樹都是完整的).
這樣就引出了一個問題, goroutine可能會同時存在很多個, 如果每一個goroutine都預(yù)先分配一個足夠的??臻g那么go就會使用過多的內(nèi)存.

為了避免這個問題, go在一開始只為goroutine分配一個很小的??臻g, 它的大小在當前版本是2K.
當函數(shù)發(fā)現(xiàn)??臻g不足時, 會申請一塊新的??臻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

會檢查比較rsp減去一定值以后是否比g.stackguard0小, 如果小于等于則需要調(diào)到下面調(diào)用morestack_noctxt函數(shù).
細心的可能會發(fā)現(xiàn)比較的值跟實際減去的值不一致, 這是因為stackguard0下面會預(yù)留一小部分空間, 編譯時確定不超過預(yù)留的空間可以省略比對.

寫屏障(Write Barrier)

因為go支持并行GC, GC的掃描和go代碼可以同時運行, 這樣帶來的問題是GC掃描的過程中g(shù)o代碼有可能改變了對象的依賴樹,
例如開始掃描時發(fā)現(xiàn)根對象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的標記階段啟用, 平時會直接把值寫入到目標地址:

關(guān)于寫屏障的詳細將在下一篇(GC篇)分析.
值得一提的是CoreCLR的GC也有寫屏障的機制, 作用跟這里說明的一樣.

閉包(Closure)

閉包這個概念本身應(yīng)該不需要解釋, 我們實際看一看go是如何實現(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)該不會感到意外.
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)              ; 第一個參數(shù)等于地址rsp+0x60
hello[0x4a09a0] <+96>:  callq  0x4a08f0                  ; 執(zhí)行main.executeFn
hello[0x4a09a5] <+101>: movq   0x8(%rsp), %rax           ; 寄存器rax等于返回值

我們可以看到傳給executeFn的是一個指針, 指針指向的內(nèi)容是[匿名函數(shù)的地址, 變量a的地址, 變量b的值].
變量a傳地址的原因是匿名函數(shù)中對a進行了修改, 需要反映到原來的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           ; 把第一個參數(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ù)并不通過棧傳遞, 而是通過寄存器rdx傳遞, 閉包的匯編代碼如下:

hello[0x455660] <+0>:  movq   0x8(%rdx), %rax            ; 第一個參數(shù)移到寄存器rax(變量a的指針)
hello[0x455664] <+4>:  movq   (%rax), %rcx               ; 把寄存器rax指向的值移到寄存器rcx(變量a的值)
hello[0x455667] <+7>:  addq   0x10(%rdx), %rcx           ; 添加第二個參數(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ù)時會傳遞指向"閉包的內(nèi)容"的指針

  • 調(diào)用閉包時會把指向"閉包的內(nèi)容"的指針放到寄存器rdx(在go內(nèi)部這個指針稱為"上下文")

  • 閉包會從寄存器rdx取出參數(shù)

  • 如果閉包修改了變量, 閉包中的參數(shù)會是指針而不是值, 修改時會修改到原來的位置上

閉包+goroutine

細心的可能會發(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)              ; 第一個參數(shù)等于類型信息
hello[0x45561c] <+44>:  callq  0x40d910                  ; 調(diào)用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)              ; 第一個參數(shù)等于類型信息
hello[0x45563d] <+77>:  callq  0x40d910                  ; 調(diào)用runtime.newobject
hello[0x455642] <+82>:  movq   0x8(%rsp), %rax           ; 寄存器rax等于返回值(這里稱為新對象fn)
hello[0x455647] <+87>:  leaq   0x82(%rip), %rcx          ; 寄存器rcx等于匿名函數(shù)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                  ; 啟用寫屏障時調(diào)用后面的邏輯
hello[0x455661] <+113>: movq   0x28(%rsp), %rcx          ; 寄存器rcx等于新對象a
hello[0x455666] <+118>: movq   %rcx, 0x8(%rax)           ; 設(shè)置新對象fn+0x8的值等于新對象a
hello[0x45566a] <+122>: movq   $0x2, 0x10(%rax)          ; 設(shè)置新對象fn+0x10的值等于2(變量b的值)
hello[0x455672] <+130>: movq   %rax, 0x10(%rsp)          ; 第三個參數(shù)等于新對象fn(額外參數(shù))
hello[0x455677] <+135>: movl   $0x10, (%rsp)             ; 第一個參數(shù)等于0x10(函數(shù)+參數(shù)的大小)
hello[0x45567e] <+142>: leaq   0x22fb3(%rip), %rax       ; 第二個參數(shù)等于一個常量構(gòu)造體的地址
hello[0x455685] <+149>: movq   %rax, 0x8(%rsp)           ; 這個構(gòu)造體的類型是funcval, 值是executeFn的地址
hello[0x45568a] <+154>: callq  0x42e690                  ; 調(diào)用runtime.newproc創(chuàng)建新的goroutine

我們可以看到goroutine+閉包的情況更復(fù)雜, 首先go會通過逃逸分析算出變量a和閉包會逃逸到外面,
這時go會在heap上分配變量a和閉包, 上面調(diào)用的兩次newobject就是分別對變量a和閉包的分配.
在創(chuàng)建goroutine時, 首先會傳入函數(shù)+參數(shù)的大小(上面是8+8=16), 然后傳入函數(shù)+參數(shù), 上面的參數(shù)即閉包的地址.

m0和g0

go中還有特殊的M和G, 它們是m0和g0.

m0是啟動程序后的主線程, 這個m對應(yīng)的實例會在全局變量m0中, 不需要在heap上分配,
m0負責執(zhí)行初始化操作和啟動第一個g, 在之后m0就和其他的m一樣了.

g0是僅用于負責調(diào)度的G, g0不指向任何可執(zhí)行的函數(shù), 每個m都會有一個自己的g0,
在調(diào)度或系統(tǒng)調(diào)用時會使用g0的??臻g, 全局變量的g0是m0的g0.

如果上面的內(nèi)容都了解, 就可以開始看golang的源代碼了.

程序初始化

go程序的入口點是runtime.rt0_go, 流程是:

  • 分配棧空間, 需要2個本地變量+2個函數(shù)參數(shù), 然后向8對齊

  • 把傳入的argc和argv保存到棧上

  • 更新g0中的stackguard的值, stackguard用于檢測??臻g是否不足, 需要分配新的棧空間

  • 獲取當前cpu的信息并保存到各個全局變量

  • 調(diào)用_cgo_init如果函數(shù)存在

  • 初始化當前線程的TLS, 設(shè)置FS寄存器為m0.tls+8(獲取時會-8)

  • 測試TLS是否工作

  • 設(shè)置g0到TLS中, 表示當前的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í)行共同的初始化

    • 這里的處理比較多, 會初始化??臻g分配器, GC, 按cpu核心數(shù)量或GOMAXPROCS的值生成P等

    • 生成P的處理在procresize中

  • 調(diào)用runtime.newproc創(chuàng)建一個新的goroutine, 指向的是runtime.main

    • runtime.newproc這個函數(shù)在創(chuàng)建普通的goroutine時也會使用, 在下面的"go的實現(xiàn)"中會詳細講解

  • 調(diào)用runtime·mstart啟動m0

    • 啟動后m0會不斷從運行隊列獲取G并運行, runtime.mstart調(diào)用后不會返回

    • runtime.mstart這個函數(shù)是m的入口點(不僅僅是m0), 在下面的"調(diào)度器的實現(xiàn)"中會詳細講解

第一個被調(diào)度的G會運行runtime.main, 流程是:

  • 標記主函數(shù)已調(diào)用, 設(shè)置mainStarted = true

  • 啟動一個新的M執(zhí)行sysmon函數(shù), 這個函數(shù)會監(jiān)控全局的狀態(tài)并對運行時間過長的G進行搶占

  • 要求G必須在當前M(系統(tǒng)主線程)上執(zhí)行

  • 調(diào)用runtime_init函數(shù)

  • 調(diào)用gcenable函數(shù)

  • 調(diào)用main.init函數(shù), 如果函數(shù)存在

  • 不再要求G必須在當前M上運行

  • 如果程序是作為c的類庫編譯的, 在這里返回

  • 調(diào)用main.main函數(shù)

  • 如果當前發(fā)生了panic, 則等待panic處理

  • 調(diào)用exit(0)退出程序

G M P的定義

G的定義在這里.
M的定義在這里.
P的定義在這里.

G里面比較重要的成員如下

  • stack: 當前g使用的棧空間, 有l(wèi)o和hi兩個成員

  • stackguard0: 檢查??臻g是否足夠的值, 低于這個值會擴張棧, 0是go代碼使用的

  • stackguard1: 檢查棧空間是否足夠的值, 低于這個值會擴張棧, 1是原生代碼使用的

  • m: 當前g對應(yīng)的m

  • sched: g的調(diào)度數(shù)據(jù), 當g中斷時會保存當前的pc和rsp等值到這里, 恢復(fù)運行時會使用這里的值

  • atomicstatus: g的當前狀態(tài)

  • schedlink: 下一個g, 當g在鏈表結(jié)構(gòu)中會使用

  • preempt: g是否被搶占中

  • lockedm: g是否要求要回到這個M執(zhí)行, 有的時候g中斷了恢復(fù)會要求使用原來的M執(zhí)行

M里面比較重要的成員如下

  • g0: 用于調(diào)度的特殊g, 調(diào)度和執(zhí)行系統(tǒng)調(diào)用時會切換到這個g

  • curg: 當前運行的g

  • p: 當前擁有的P

  • nextp: 喚醒M時, M會擁有這個P

  • park: M休眠時使用的信號量, 喚醒M時會通過它喚醒

  • schedlink: 下一個m, 當m在鏈表結(jié)構(gòu)中會使用

  • mcache: 分配內(nèi)存時使用的本地分配器, 和p.mcache一樣(擁有P時會復(fù)制過來)

  • lockedg: lockedm的對應(yīng)值

P里面比較重要的成員如下

  • status: p的當前狀態(tài)

  • link: 下一個p, 當p在鏈表結(jié)構(gòu)中會使用

  • m: 擁有這個P的M

  • mcache: 分配內(nèi)存時使用的本地分配器

  • runqhead: 本地運行隊列的出隊序號

  • runqtail: 本地運行隊列的入隊序號

  • runq: 本地運行隊列的數(shù)組, 可以保存256個G

  • gfree: G的自由列表, 保存變?yōu)開Gdead后可以復(fù)用的G實例

  • gcBgMarkWorker: 后臺GC的worker函數(shù), 如果它存在M會優(yōu)先執(zhí)行它

  • gcw: GC的本地工作隊列, 詳細將在下一篇(GC篇)分析

go的實現(xiàn)

使用go命令創(chuàng)建goroutine時, go會把go命令編譯為對runtime.newproc的調(diào)用, 堆棧的結(jié)構(gòu)如下:golang 協(xié)程的實現(xiàn)原理

第一個參數(shù)是funcval + 額外參數(shù)的長度, 第二個參數(shù)是funcval, 后面的都是傳遞給goroutine中執(zhí)行的函數(shù)的額外參數(shù).
funcval的定義在這里, fn是指向函數(shù)機器代碼的指針.
runtime.newproc的處理如下:

  • 計算額外參數(shù)的地址argp

  • 獲取調(diào)用端的地址(返回地址)pc

  • 使用systemstack調(diào)用newproc1

systemstack會切換當前的g到g0, 并且使用g0的??臻g, 然后調(diào)用傳入的函數(shù), 再切換回原來的g和原來的棧空間.
切換到g0后會假裝返回地址是mstart, 這樣traceback的時候可以在mstart停止.
這里傳給systemstack的是一個閉包, 調(diào)用時會把閉包的地址放到寄存器rdx, 具體可以參考上面對閉包的分析.

runtime.newproc1的處理如下:

  • 調(diào)用getg獲取當前的g, 會編譯為讀取FS寄存器(TLS), 這里會獲取到g0

  • 設(shè)置g對應(yīng)的m的locks++, 禁止搶占

  • 獲取m擁有的p

  • 新建一個g

    • 首先調(diào)用gfget從p.gfree獲取g, 如果之前有g(shù)被回收在這里就可以復(fù)用

    • 獲取不到時調(diào)用malg分配一個g, 初始的??臻g大小是2K

    • 需要先設(shè)置g的狀態(tài)為已中止(_Gdead), 這樣gc不會去掃描這個g的未初始化的棧

  • 把參數(shù)復(fù)制到g的棧上

  • 把返回地址復(fù)制到g的棧上, 這里的返回地址是goexit, 表示調(diào)用完目標函數(shù)后會調(diào)用goexit

  • 設(shè)置g的調(diào)度數(shù)據(jù)(sched)

    • 設(shè)置sched.sp等于參數(shù)+返回地址后的rsp地址

    • 設(shè)置sched.pc等于目標函數(shù)的地址, 查看gostartcallfn和gostartcall

    • 設(shè)置sched.g等于g

  • 設(shè)置g的狀態(tài)為待運行(_Grunnable)

  • 調(diào)用runqput把g放到運行隊列

    • runqputslow會把本地運行隊列中一半的g放到全局運行隊列, 這樣下次就可以繼續(xù)用快速的本地運行隊列了

    • 首先隨機把g放到p.runnext, 如果放到runnext則入隊原來在runnext的g

    • 然后嘗試把g放到P的"本地運行隊列"

    • 如果本地運行隊列滿了則調(diào)用runqputslow把g放到"全局運行隊列"

  • 如果當前有空閑的P, 但是無自旋的M(nmspinning等于0), 并且主函數(shù)已執(zhí)行則喚醒或新建一個M

    • 首先交換nmspinning到1, 成功再繼續(xù), 多個線程同時執(zhí)行wakep只有一個會繼續(xù)

    • 調(diào)用startm函數(shù)

    • newm會新建一個m的實例, m的實例包含一個g0, 然后調(diào)用newosproc動一個系統(tǒng)線程

    • newosproc會調(diào)用syscall clone創(chuàng)建一個新的線程

    • 線程創(chuàng)建后會設(shè)置TLS, 設(shè)置TLS中當前的g為g0, 然后執(zhí)行mstart

    • 調(diào)用pidleget從"空閑P鏈表"獲取一個空閑的P

    • 調(diào)用mget從"空閑M鏈表"獲取一個空閑的M

    • 如果沒有空閑的M, 則調(diào)用newm新建一個M

    • 調(diào)用notewakeup(&mp.park)喚醒線程

    • 這一步非常重要, 用于保證當前有足夠的M運行G, 具體請查看上面的"空閑M鏈表"

    • 喚醒或新建一個M會通過wakep函數(shù)

創(chuàng)建goroutine的流程就這么多了, 接下來看看M是如何調(diào)度的.

調(diào)度器的實現(xiàn)

M啟動時會調(diào)用mstart函數(shù), m0在初始化后調(diào)用, 其他的的m在線程啟動后調(diào)用.
mstart函數(shù)的處理如下:

  • 調(diào)用getg獲取當前的g, 這里會獲取到g0

  • 如果g未分配棧則從當前的棧空間(系統(tǒng)??臻g)上分配, 也就是說g0會使用系統(tǒng)??臻g

  • 調(diào)用mstart1函數(shù)

    • 調(diào)用gosave函數(shù)保存當前的狀態(tài)到g0的調(diào)度數(shù)據(jù)中, 以后每次調(diào)度都會從這個棧地址開始

    • 調(diào)用asminit函數(shù), 不做任何事情

    • 調(diào)用minit函數(shù), 設(shè)置當前線程可以接收的信號(signal)

    • 調(diào)用schedule函數(shù)

調(diào)用schedule函數(shù)后就進入了調(diào)度循環(huán), 整個流程可以簡單總結(jié)為:

schedule函數(shù)獲取g => [必要時休眠] => [喚醒后繼續(xù)獲取] => execute函數(shù)執(zhí)行g(shù) => 執(zhí)行后返回到goexit => 重新執(zhí)行schedule函數(shù)

schedule函數(shù)的處理如下:

  • 如果當前GC需要停止整個世界(STW), 則調(diào)用stopm休眠當前的M

  • 如果M擁有的P中指定了需要在安全點運行的函數(shù)(P.runSafePointFn), 則運行它

  • 快速獲取待運行的G, 以下處理如果有一個獲取成功后面就不會繼續(xù)獲取

    • 如果當前GC正在標記階段, 則查找有沒有待運行的GC Worker, GC Worker也是一個G

    • 為了公平起見, 每61次調(diào)度從全局運行隊列獲取一次G, (一直從本地獲取可能導(dǎo)致全局運行隊列中的G不被運行)

    • 從P的本地運行隊列中獲取G, 調(diào)用runqget函數(shù)

  • 快速獲取失敗時, 調(diào)用findrunnable函數(shù)獲取待運行的G, 會阻塞到獲取成功為止

    • 再次檢查當前GC是否在標記階段, 在則查找有沒有待運行的GC Worker, GC Worker也是一個G

    • 再次檢查如果當前GC需要停止整個世界, 或者P指定了需要再安全點運行的函數(shù), 則跳到findrunnable的頂部重試

    • 再次檢查全局運行隊列中是否有G, 有則獲取并返回

    • 釋放M擁有的P, P會變?yōu)榭臻e(_Pidle)狀態(tài)

    • 把P添加到"空閑P鏈表"中

    • 讓M離開自旋狀態(tài), 這里的處理非常重要, 參考上面的"空閑M鏈表"

    • 首先減少表示當前自旋中的M的數(shù)量的全局變量nmspinning

    • 再次檢查所有P的本地運行隊列, 如果不為空則讓M重新進入自旋狀態(tài), 并跳到findrunnable的頂部重試

    • 再次檢查有沒有待運行的GC Worker, 有則讓M重新進入自旋狀態(tài), 并跳到findrunnable的頂部重試

    • 再次檢查網(wǎng)絡(luò)事件反應(yīng)器是否有待運行的G, 這里對netpoll的調(diào)用會阻塞, 直到某個fd收到了事件

    • 如果最終還是獲取不到G, 調(diào)用stopm休眠當前的M

    • 喚醒后跳到findrunnable的頂部重試

    • 調(diào)用runqsteal嘗試從其他P的本地運行隊列盜取一半的G

    • 如果當前GC需要停止整個世界(STW), 則調(diào)用stopm休眠當前的M

    • 如果M擁有的P中指定了需要在安全點運行的函數(shù)(P.runSafePointFn), 則運行它

    • 如果有析構(gòu)器待運行則使用"運行析構(gòu)器的G"

    • 從P的本地運行隊列中獲取G, 調(diào)用runqget函數(shù)

    • 從全局運行隊列獲取G, 調(diào)用globrunqget函數(shù), 需要上鎖

    • 從網(wǎng)絡(luò)事件反應(yīng)器獲取G, 函數(shù)netpoll會獲取哪些fd可讀可寫或已關(guān)閉, 然后返回等待fd相關(guān)事件的G

    • 如果獲取不到G, 則執(zhí)行Work Stealing

    • 如果還是獲取不到G, 就需要休眠M了, 接下來是休眠的步驟

  • 成功獲取到一個待運行的G

  • 讓M離開自旋狀態(tài), 調(diào)用resetspinning, 這里的處理和上面的不一樣

    • 如果當前有空閑的P, 但是無自旋的M(nmspinning等于0), 則喚醒或新建一個M

    • 上面離開自旋狀態(tài)是為了休眠M, 所以會再次檢查所有隊列然后休眠

    • 這里離開自選狀態(tài)是為了執(zhí)行G, 所以會檢查是否有空閑的P, 有則表示可以再開新的M執(zhí)行G

  • 如果G要求回到指定的M(例如上面的runtime.main)

    • 調(diào)用startlockedm函數(shù)把G和P交給該M, 自己進入休眠

    • 從休眠喚醒后跳到schedule的頂部重試

  • 調(diào)用execute函數(shù)執(zhí)行G

execute函數(shù)的處理如下:

  • 調(diào)用getg獲取當前的g

  • 把G的狀態(tài)由待運行(_Grunnable)改為運行中(_Grunning)

  • 設(shè)置G的stackguard, 棧空間不足時可以擴張

  • 增加P中記錄的調(diào)度次數(shù)(對應(yīng)上面的每61次優(yōu)先獲取一次全局運行隊列)

  • 設(shè)置g.m.curg = g

  • 設(shè)置g.m = m

  • 調(diào)用gogo函數(shù)

    • 這個函數(shù)會根據(jù)g.sched中保存的狀態(tài)恢復(fù)各個寄存器的值并繼續(xù)運行g(shù)

    • 首先針對g.sched.ctxt調(diào)用寫屏障(GC標記指針存活), ctxt中一般會保存指向[函數(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

    • 因為前面創(chuàng)建goroutine的newproc1函數(shù)把返回地址設(shè)為了goexit, 函數(shù)運行完畢返回時將會調(diào)用goexit函數(shù)

g.sched.pc在G首次運行時會指向目標函數(shù)的第一條機器指令,
如果G被搶占或者等待資源而進入休眠, 在休眠前會保存狀態(tài)到g.sched,
g.sched.pc會變?yōu)閱拘押笮枰^續(xù)執(zhí)行的地址, "保存狀態(tài)"的實現(xiàn)將在下面講解.

目標函數(shù)執(zhí)行完畢后會調(diào)用goexit函數(shù), goexit函數(shù)會調(diào)用goexit1函數(shù), goexit1函數(shù)會通過mcall調(diào)用goexit0函數(shù).
mcall這個函數(shù)就是用于實現(xiàn)"保存狀態(tài)"的, 處理如下:

  • 設(shè)置g.sched.pc等于當前的返回地址

  • 設(shè)置g.sched.sp等于寄存器rsp的值

  • 設(shè)置g.sched.g等于當前的g

  • 設(shè)置g.sched.bp等于寄存器rbp的值

  • 切換TLS中當前的g等于m.g0

  • 設(shè)置寄存器rsp等于g0.sched.sp, 使用g0的??臻g

  • 設(shè)置第一個參數(shù)為原來的g

  • 設(shè)置rdx寄存器為指向函數(shù)地址的指針(上下文)

  • 調(diào)用指定的函數(shù), 不會返回

mcall這個函數(shù)保存當前的運行狀態(tài)到g.sched, 然后切換到g0和g0的棧空間, 再調(diào)用指定的函數(shù).
回到g0的??臻g這個步驟非常重要, 因為這個時候g已經(jīng)中斷, 繼續(xù)使用g的棧空間且其他M喚醒了這個g將會產(chǎn)生災(zāi)難性的后果.
G在中斷或者結(jié)束后都會通過mcall回到g0的??臻g繼續(xù)調(diào)度, 從goexit調(diào)用的mcall的保存狀態(tài)其實是多余的, 因為G已經(jīng)結(jié)束了.

goexit1函數(shù)會通過mcall調(diào)用goexit0函數(shù),?goexit0函數(shù)調(diào)用時已經(jīng)回到了g0的??臻g, 處理如下:

  • 把G的狀態(tài)由運行中(_Grunning)改為已中止(_Gdead)

  • 清空G的成員

  • 調(diào)用dropg函數(shù)解除M和G之間的關(guān)聯(lián)

  • 調(diào)用gfput函數(shù)把G放到P的自由列表中, 下次創(chuàng)建G時可以復(fù)用

  • 調(diào)用schedule函數(shù)繼續(xù)調(diào)度

G結(jié)束后回到schedule函數(shù), 這樣就結(jié)束了一個調(diào)度循環(huán).
不僅只有G結(jié)束會重新開始調(diào)度, G被搶占或者等待資源也會重新進行調(diào)度, 下面繼續(xù)來看這兩種情況.

搶占的實現(xiàn)

上面我提到了runtime.main會創(chuàng)建一個額外的M運行sysmon函數(shù), 搶占就是在sysmon中實現(xiàn)的.
sysmon會進入一個無限循環(huán), 第一輪回休眠20us, 之后每次休眠時間倍增, 最終每一輪都會休眠10ms.
sysmon中有netpool(獲取fd事件), retake(搶占), forcegc(按時間強制執(zhí)行g(shù)c), scavenge heap(釋放自由列表中多余的項減少內(nèi)存占用)等處理.

retake函數(shù)負責處理搶占, 流程是:

  • 枚舉所有的P

    • 調(diào)用preemptone函數(shù)

    • 設(shè)置g.preempt = true

    • 設(shè)置g.stackguard0 = stackPreempt

    • 調(diào)用handoffp解除M和P之間的關(guān)聯(lián)

    • 如果P在系統(tǒng)調(diào)用中(_Psyscall), 且經(jīng)過了一次sysmon循環(huán)(20us~10ms), 則搶占這個P

    • 如果P在運行中(_Prunning), 且經(jīng)過了一次sysmon循環(huán)并且G運行時間超過forcePreemptNS(10ms), 則搶占這個P

為什么設(shè)置了stackguard就可以實現(xiàn)搶占?
因為這個值用于檢查當前??臻g是否足夠, go函數(shù)的開頭會比對這個值判斷是否需要擴張棧.
stackPreempt是一個特殊的常量, 它的值會比任何的棧地址都要大, 檢查時一定會觸發(fā)棧擴張.

棧擴張調(diào)用的是morestack_noctxt函數(shù), morestack_noctxt函數(shù)清空rdx寄存器并調(diào)用morestack函數(shù).
morestack函數(shù)會保存G的狀態(tài)到g.sched, 切換到g0和g0的??臻g, 然后調(diào)用newstack函數(shù).
newstack函數(shù)判斷g.stackguard0等于stackPreempt, 就知道這是搶占觸發(fā)的, 這時會再檢查一遍是否要搶占:

  • 如果M被鎖定(函數(shù)的本地變量中有P), 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運行G

  • 如果M正在分配內(nèi)存, 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運行G

  • 如果M設(shè)置了當前不能搶占, 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運行G

  • 如果M的狀態(tài)不是運行中, 則跳過這一次的搶占并調(diào)用gogo函數(shù)繼續(xù)運行G

即使這一次搶占失敗, 因為g.preempt等于true, runtime中的一些代碼會重新設(shè)置stackPreempt以重試下一次的搶占.
如果判斷可以搶占, 則繼續(xù)判斷是否GC引起的, 如果是則對G的??臻g執(zhí)行標記處理(掃描根對象)然后繼續(xù)運行,
如果不是GC引起的則調(diào)用gopreempt_m函數(shù)完成搶占.

gopreempt_m函數(shù)會調(diào)用goschedImpl函數(shù), goschedImpl函數(shù)的流程是:

  • 把G的狀態(tài)由運行中(_Grunnable)改為待運行(_Grunnable)

  • 調(diào)用dropg函數(shù)解除M和G之間的關(guān)聯(lián)

  • 調(diào)用globrunqput把G放到全局運行隊列

  • 調(diào)用schedule函數(shù)繼續(xù)調(diào)度

因為全局運行隊列的優(yōu)先度比較低, 各個M會經(jīng)過一段時間再去重新獲取這個G執(zhí)行,
搶占機制保證了不會有一個G長時間的運行導(dǎo)致其他G無法運行的情況發(fā)生.

channel的實現(xiàn)

在goroutine運行的過程中, 有時候需要對資源進行等待, channel就是最典型的資源.
channel的數(shù)據(jù)定義在這里, 其中關(guān)鍵的成員如下:

  • qcount: 當前隊列中的元素數(shù)量

  • dataqsiz: 隊列可以容納的元素數(shù)量, 如果為0表示這個channel無緩沖區(qū)

  • buf: 隊列的緩沖區(qū), 結(jié)構(gòu)是環(huán)形隊列

  • elemsize: 元素的大小

  • closed: 是否已關(guān)閉

  • elemtype: 元素的類型, 判斷是否調(diào)用寫屏障時使用

  • sendx: 發(fā)送元素的序號

  • recvx: 接收元素的序號

  • recvq: 當前等待從channel接收數(shù)據(jù)的G的鏈表(實際類型是sudog的鏈表)

  • sendq: 當前等待發(fā)送數(shù)據(jù)到channel的G的鏈表(實際類型是sudog的鏈表)

  • lock: 操作channel時使用的線程鎖

發(fā)送數(shù)據(jù)到channel實際調(diào)用的是runtime.chansend1函數(shù), chansend1函數(shù)調(diào)用了chansend函數(shù), 流程是:

  • 檢查channel.recvq是否有等待中的接收者的G

    • 如果sudog.elem不等于nil, 調(diào)用sendDirect函數(shù)從發(fā)送者直接復(fù)制元素

    • 等待接收的sudog.elem是指向接收目標的內(nèi)存的指針, 如果是接收目標是_則elem是nil, 可以省略復(fù)制

    • 等待發(fā)送的sudog.elem是指向來源目標的內(nèi)存的指針

    • 復(fù)制后調(diào)用goready恢復(fù)發(fā)送者的G

    • 把G的狀態(tài)由等待中(_Gwaiting)改為待運行(_Grunnable)

    • 把G放到P的本地運行隊列

    • 如果當前有空閑的P, 但是無自旋的M(nmspinning等于0), 則喚醒或新建一個M

    • 切換到g0調(diào)用ready函數(shù), 調(diào)用完切換回來

    • 如果有, 表示channel無緩沖區(qū)或者緩沖區(qū)為空

    • 調(diào)用send函數(shù)

    • 從發(fā)送者拿到數(shù)據(jù)并喚醒了G后, 就可以從chansend返回了

  • 判斷是否可以把元素放到緩沖區(qū)中

    • 如果緩沖區(qū)有空余的空間, 則把元素放到緩沖區(qū)并從chansend返回

  • 無緩沖區(qū)或緩沖區(qū)已經(jīng)寫滿, 發(fā)送者的G需要等待

    • 調(diào)用gopark函數(shù)

    • mcall函數(shù)和上面說明的一樣, 會把當前的狀態(tài)保存到g.sched, 然后切換到g0和g0的??臻g并執(zhí)行指定的函數(shù)

    • park_m函數(shù)首先把G的狀態(tài)從運行中(_Grunning)改為等待中(_Gwaiting)

    • 然后調(diào)用dropg函數(shù)解除M和G之間的關(guān)聯(lián)

    • 再調(diào)用傳入的解鎖函數(shù), 這里的解鎖函數(shù)會對解除channel.lock的鎖定

    • 最后調(diào)用schedule函數(shù)繼續(xù)調(diào)度

    • 通過mcall函數(shù)調(diào)用park_m函數(shù)

    • 獲取當前的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ù)

  • 從這里恢復(fù)表示已經(jīng)成功發(fā)送或者channel已關(guān)閉

    • 檢查sudog.param是否為nil, 如果為nil表示channel已關(guān)閉, 拋出panic

    • 否則釋放sudog然后返回

從channel接收數(shù)據(jù)實際調(diào)用的是runtime.chanrecv1函數(shù), chanrecv1函數(shù)調(diào)用了chanrecv函數(shù), 流程是:

  • 檢查channel.sendq中是否有等待中的發(fā)送者的G

    • 如果無緩沖區(qū), 調(diào)用recvDirect函數(shù)把元素直接復(fù)制給接收者

    • 如果有緩沖區(qū)代表緩沖區(qū)已滿

    • 復(fù)制后調(diào)用goready恢復(fù)接收者的G, 處理同上

    • 把隊列中下一個要出隊的元素直接復(fù)制給接收者

    • 把發(fā)送的元素復(fù)制到隊列中剛才出隊的位置

    • 這時候緩沖區(qū)仍然是滿的, 但是發(fā)送序號和接收序號都會增加1

    • 如果有, 表示channel無緩沖區(qū)或者緩沖區(qū)已滿, 這兩種情況需要分別處理(為了保證入出隊順序一致)

    • 調(diào)用recv函數(shù)

    • 把數(shù)據(jù)交給接收者并喚醒了G后, 就可以從chanrecv返回了

  • 判斷是否可以從緩沖區(qū)獲取元素

    • 如果緩沖區(qū)有元素, 則直接取出該元素并從chanrecv返回

  • 無緩沖區(qū)或緩沖區(qū)無元素, 接收者的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ā)送不一樣的是接收不會拋panic, 會通過返回值通知channel已關(guān)閉

    • 釋放sudog然后返回

關(guān)閉channel實際調(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文章來源地址http://www.zghlxwxcb.cn/news/detail-494930.html

到了這里,關(guān)于golang 協(xié)程的實現(xiàn)原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • Kotlin協(xié)程的JVM實現(xiàn)源碼分析(上)

    本文從協(xié)程的啟動 launch 源碼入手分析,協(xié)程JVM實現(xiàn)分為兩篇: 協(xié)程啟動和執(zhí)行源碼分析 無棧協(xié)程 和 Continuation 基本環(huán)境: IntelliJ IDEA 2023.3.2 Kotlin 1.8.20 kotlinx-coroutines-core 1.7.3 gradle 8.2 以 GlobalScope.launch 啟動協(xié)程分析: 調(diào)用關(guān)系: CoroutineScope.launch - StandaloneCoroutine.start - Corou

    2024年01月19日
    瀏覽(24)
  • Kotlin協(xié)程的JVM實現(xiàn)源碼分析(下)

    Kotlin協(xié)程的JVM實現(xiàn)源碼分析(下)

    協(xié)程 根據(jù) 是否保存切換 調(diào)用棧 ,分為: 有棧協(xié)程(stackful coroutine) 無棧協(xié)程(stackless coroutine) 在代碼上的區(qū)別是:是否可在普通函數(shù)里調(diào)用,并暫停其執(zhí)行。 Kotlin協(xié)程,必須在掛起函數(shù)中調(diào)用和恢復(fù),屬于 無棧協(xié)程 。 常見的語言,協(xié)程實現(xiàn): 有棧協(xié)程 :Go、Lua 無棧

    2024年01月23日
    瀏覽(17)
  • 深入理解 Golang: Goroutine 協(xié)程

    深入理解 Golang: Goroutine 協(xié)程

    進程用來分配內(nèi)存空間,是操作系統(tǒng)分配資源的最小單位;線程用來分配 CPU 時間,多個線程共享內(nèi)存空間,是操作系統(tǒng)或 CPU 調(diào)度的最小單位;協(xié)程用來精細利用線程。協(xié)程就是將一段程序的運行狀態(tài)打包,可以在線程之間調(diào)度?;蛘哒f將一段生產(chǎn)流程打包,使流程不固定在

    2024年02月11日
    瀏覽(20)
  • 重寫Sylar基于協(xié)程的服務(wù)器(7、TcpServer & HttpServer的設(shè)計與實現(xiàn))

    重寫Sylar基于協(xié)程的服務(wù)器(7、TcpServer & HttpServer的設(shè)計與實現(xiàn))

    重寫Sylar基于協(xié)程的服務(wù)器系列: 重寫Sylar基于協(xié)程的服務(wù)器(0、搭建開發(fā)環(huán)境以及項目框架 || 下載編譯簡化版Sylar) 重寫Sylar基于協(xié)程的服務(wù)器(1、日志模塊的架構(gòu)) 重寫Sylar基于協(xié)程的服務(wù)器(2、配置模塊的設(shè)計) 重寫Sylar基于協(xié)程的服務(wù)器(3、協(xié)程模塊的設(shè)計) 重寫

    2024年02月21日
    瀏覽(25)
  • elasticsearch高級篇:核心概念和實現(xiàn)原理

    elasticsearch高級篇:核心概念和實現(xiàn)原理

    1.1 索引(index) 一個索引就是一個擁有幾分相似特征的文檔的集合。比如說,你可以有一個客戶數(shù)據(jù)的索引,另一個產(chǎn)品目錄的索引,還有一個訂單數(shù)據(jù)的索引。一個索引由一個名字來標識(必須全部是小寫字母),并且當我們要對這個索引中的文檔進行索引、搜索、更新和刪

    2023年04月08日
    瀏覽(25)
  • Golang的協(xié)程調(diào)度器原理及GMP設(shè)計思想

    (1) 單進程時代不需要調(diào)度器 我們知道,一切的軟件都是跑在操作系統(tǒng)上,真正用來干活(計算)的是CPU。早期的操作系統(tǒng)每個程序就是一個進程,知道一個程序運行完,才能進行下一個進程,就是“單進程時代” 一切的程序只能串行發(fā)生。 早期的單進程操作系統(tǒng),面臨2個問題

    2024年02月16日
    瀏覽(27)
  • [Kotlin Tutorials 21] 協(xié)程的取消

    [Kotlin Tutorials 21] 協(xié)程的取消

    本文討論協(xié)程的取消, 以及實現(xiàn)時可能會碰到的幾個問題. 本文屬于合輯: https://github.com/mengdd/KotlinTutorials 取消的意義: 避免資源浪費, 以及多余操作帶來的問題. 基本特性: cancel scope的時候會cancel其中的所有child coroutines. 一旦取消一個scope, 你將不能再在其中l(wèi)aunch新的coroutine. 一

    2024年02月08日
    瀏覽(14)
  • Kotlin: 協(xié)程的四種啟動模式(CoroutineStart)

    Kotlin: 協(xié)程的四種啟動模式(CoroutineStart)

    點擊查看CoroutineStart英文文檔 創(chuàng)建協(xié)程的三種方式 runBlocking 運行一個協(xié)程并且會阻塞當前線程,直到它完成。 launch 啟動一個新的協(xié)程,不會阻塞當前線程,并且返回一個Job,可以取消。 async async和await是兩個函數(shù),這兩個函數(shù)在我們使用過程中一般都是成對出現(xiàn)的。 async用

    2024年04月23日
    瀏覽(22)
  • Unity中停止協(xié)程的多種方式解析

    在Unity3D游戲開發(fā)中,協(xié)程(Coroutine)是一種非常有用的功能,可以在游戲中實現(xiàn)延遲執(zhí)行、定期執(zhí)行和異步操作等任務(wù)。然而,有時候我們需要在運行時停止協(xié)程的執(zhí)行。本文將介紹Unity中停止協(xié)程的幾種常用方式,并提供相應(yīng)的源代碼示例。 使用StopCoroutine函數(shù)停止協(xié)程

    2024年02月03日
    瀏覽(22)
  • Android上的基于協(xié)程的存儲框架

    在Android上,經(jīng)常會需要持久化本地數(shù)據(jù),比如我們需要緩存用戶的配置信息、用戶的數(shù)據(jù)、緩存數(shù)據(jù)、離線緩存數(shù)據(jù)等等。我們通常使用的工具為SharePreference、MMKV、DataStore、Room、文件等等。通過使用現(xiàn)有的存儲框架,結(jié)合協(xié)程,我們可以方便地實現(xiàn)一個輕量級的響應(yīng)式存儲

    2024年02月13日
    瀏覽(13)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包