<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          go 的運(yùn)行時

          共 9967字,需瀏覽 20分鐘

           ·

          2022-03-31 18:42

          goroutine 定義

          Goroutine 是一個與其他 goroutines 并行運(yùn)行在同一地址空間的 Go 函數(shù)或方法。一個運(yùn)行的程序由一個或更多個 goroutine 組成。它與線程、協(xié)程、進(jìn)程等不同。它是一個 goroutine” —— Rob PikeGoroutines 在同一個用戶地址空間里并行獨立執(zhí)行 functionschannels 則用于 goroutines 間的通信和同步訪問控制。

          goroutine VS thread

          • 內(nèi)存占用. 創(chuàng)建一個 goroutine 的棧內(nèi)存消耗為 2 KB(Linux AMD64 Go v1.4后),運(yùn)行過程中,如果棧空間不夠用,會自動進(jìn)行擴(kuò)容。創(chuàng)建一個 thread 為了盡量避免極端情況下操作系統(tǒng)線程棧的溢出,默認(rèn)會為其分配一個較大的棧內(nèi)存( 1 - 8 MB 棧內(nèi)存,線程標(biāo)準(zhǔn)POSIX Thread),而且還需要一個被稱為 “guard page” 的區(qū)域用于和其他 thread 的棧空間進(jìn)行隔離。而棧內(nèi)存空間一旦創(chuàng)建和初始化完成之后其大小就不能再有變化,這決定了在某些特殊場景下系統(tǒng)線程棧還是有溢出的風(fēng)險
          • 創(chuàng)建/銷毀,線程創(chuàng)建和銷毀都會有巨大的消耗,是內(nèi)核級的交互(trap)。POSIX 線程(定義了創(chuàng)建和操縱線程的一套 API) 通常是在已有的進(jìn)程模型中增加的邏輯擴(kuò)展,所以線程控制和進(jìn)程控制很相似。而進(jìn)入內(nèi)核調(diào)度所消耗的性能代價比較高,開銷較大。goroutine 是用戶態(tài)線程,是由 go runtime 管理,創(chuàng)建和銷毀的消耗非常小。
          • 調(diào)度切換 拋開陷入內(nèi)核,線程切換會消耗 1000-1500 納秒(上下文保存成本高,較多寄存器,公平性,復(fù)雜時間計算統(tǒng)計),一個納秒平均可以執(zhí)行 12-18 條指令。所以由于線程切換,執(zhí)行指令的條數(shù)會減少 12000-18000goroutine 的切換約為 200ns (用戶態(tài)、3個寄存器),相當(dāng)于 2400-3600 條指令。因此, goroutines 切換成本比 ?threads 要小得多。
          • 復(fù)雜性 線程的創(chuàng)建和退出復(fù)雜,多個 thread 間通訊復(fù)雜(share memory)。不能大量創(chuàng)建線程(參考早期的 httpd),成本高,使用網(wǎng)絡(luò)多路復(fù)用,存在大量callback(參考twemproxynginx 的代碼) 。對于應(yīng)用服務(wù)線程門檻高,例如需要做第三方庫隔離,需要考慮引入線程池等。

          Goroutine 運(yùn)行原理

          Go 程序的執(zhí)行由兩層組成:Go ProgramRuntime,即用戶程序和運(yùn)行時。它們之間通過函數(shù)調(diào)用來實現(xiàn)內(nèi)存管理、channel 通信、goroutines 創(chuàng)建等功能。用戶程序進(jìn)行的系統(tǒng)調(diào)用都會被 Runtime 攔截,以此來幫助它進(jìn)行調(diào)度以及垃圾回收相關(guān)的工作。

          M:N 模型

          Go runtime 會負(fù)責(zé) goroutine 的生老病死,從創(chuàng)建到銷毀,都一手包辦。Runtime 會在程序啟動的時候。Go 創(chuàng)建 M 個線程(CPU 執(zhí)行調(diào)度的單元,內(nèi)核的 task_struct),之后創(chuàng)建的 Ngoroutine 都會依附在這 M 個線程上執(zhí)行,即 M:N 模型。它們能夠同時運(yùn)行,與線程類似,但相比之下非常輕量。因此,程序運(yùn)行時,Goroutines的個數(shù)應(yīng)該是遠(yuǎn)大于線程的個數(shù)的(phread 是內(nèi)核線程?)。

          同一個時刻,一個線程只能跑一個 goroutine。當(dāng) goroutine 發(fā)生阻塞 (chan阻塞、mutexsyscall 等等) 時,Go 會把當(dāng)前的 goroutine 調(diào)度走,讓其他 goroutine 來繼續(xù)執(zhí)行,而不是讓線程阻塞休眠,盡可能多的分發(fā)任務(wù)出去,讓 CPU 忙。

          GM 調(diào)度模型

          go1.2版本之前,調(diào)度模型使用的是 GM 調(diào)度模型。

          G

          goroutine 的縮寫,每次 go func() 都代表一個 G,無限制。使用 struct runtime.g,包含了當(dāng)前 goroutine 的狀態(tài)、堆棧、上下文。

          M

          工作線程(OS thread)也被稱為 Machine,使用 struct runtime.m,所有 M 是有線程棧的。如果不對該線程棧提供內(nèi)存的話,系統(tǒng)會給該線程棧提供內(nèi)存(不同操作系統(tǒng)提供的線程棧大小不同) 。當(dāng)指定了線程棧,則 M.stack→G.stackMPC 寄存器指向 G 提供的函數(shù),然后去執(zhí)行。

          GM 調(diào)度

          Go 1.2前的調(diào)度器實現(xiàn),限制了 Go 并發(fā)程序的伸縮性,尤其是對那些有高吞吐或并行計算需求的服務(wù)程序。每個 goroutine 對應(yīng)于 runtime 中的一個抽象結(jié)構(gòu):G,而 thread 作為“物理 CPU”的存在而被抽象為一個結(jié)構(gòu):M(machine)。當(dāng) goroutine 調(diào)用了一個阻塞的系統(tǒng)調(diào)用,運(yùn)行這個 goroutine 的線程就會被阻塞,這時至少應(yīng)該再創(chuàng)建/喚醒一個線程來運(yùn)行別的沒有阻塞的 goroutine 。線程這里可以創(chuàng)建不止一個,可以按需不斷地創(chuàng)建,而活躍的線程(處于非阻塞狀態(tài)的線程)的最大個數(shù)存儲在變量 GOMAXPROCS 中。

          調(diào)用過程如下所示:

          66203fb1af305bb735612207c35f88fa.webp

          M 想要執(zhí)行、放回 G 都必須訪問全局 G 隊列,并且 M 有多個,即多線程訪問同一資源需要加鎖進(jìn)行保證互斥 / 同步,所以全局 G 隊列是有互斥鎖進(jìn)行保護(hù)的

          GM 調(diào)度模型的問題

          • 單一全局互斥鎖(Sched.Lock)和集中狀態(tài)存儲 導(dǎo)致所有 goroutine 相關(guān)操作,比如:創(chuàng)建、結(jié)束、重新調(diào)度等都要上鎖。
          • Goroutine 傳遞問題M 經(jīng)常在 M 之間傳遞”可運(yùn)行”的 goroutine ,這導(dǎo)致調(diào)度延遲增大以及額外的性能損耗(剛創(chuàng)建的 G 放到了全局隊列,而不是本地 M 執(zhí)行,不必要的開銷和延遲)。
          • Per-M 持有內(nèi)存緩存 (M.mcache) 每個 M 持有 mcachestackalloc ,然而只有在 M 運(yùn)行 Go 代碼時才需要使用的內(nèi)存(每個 mcache 可以高達(dá) 2mb ),當(dāng) M 在處于 syscall 時并不需要。運(yùn)行 Go 代碼和阻塞在 syscallM 的比例高達(dá)1:100,造成了很大的浪費。同時內(nèi)存親緣性也較差,G 當(dāng)前在 M 運(yùn)行后對 M 的內(nèi)存進(jìn)行了預(yù)熱,因為現(xiàn)在 G 調(diào)度到同一個 M 的概率不高,數(shù)據(jù)局部性不好。
          • 嚴(yán)重的線程阻塞/解鎖 在系統(tǒng)調(diào)用的情況下,工作線程經(jīng)常被阻塞和取消阻塞,這增加了很多開銷。比如 M 找不到G,此時 M 就會進(jìn)入頻繁阻塞/喚醒來進(jìn)行檢查的邏輯,以便及時發(fā)現(xiàn)新的 G 來執(zhí)行。by Dmitry Vyukov “Scalable Go Scheduler Design Doc”

          GMP 調(diào)度模型

          go 1.2 版本及以后,go 引入 GMP 調(diào)度模型

          G

          goroutine 的縮寫,每次 go func() 都代表一個 G,無限制。使用 struct runtime.g,包含了當(dāng)前 goroutine 的狀態(tài)、堆棧、上下文。

          M

          工作線程(OS thread)也被稱為 Machine,使用 struct runtime.m,所有 M 是有線程棧的。如果不對該線程棧提供內(nèi)存的話,系統(tǒng)會給該線程棧提供內(nèi)存(不同操作系統(tǒng)提供的線程棧大小不同) 。當(dāng)指定了線程棧,則 M.stack→G.stackMPC 寄存器指向 G 提供的函數(shù),然后去執(zhí)行。

          P

          “Processor”是一個抽象的概念,并不是真正的物理 CPU

          Dmitry Vyukov 的方案是引入一個結(jié)構(gòu) P,它代表了 M 所需的上下文環(huán)境,也是處理用戶級代碼邏輯的處理器。它負(fù)責(zé)銜接 MG 的調(diào)度上下文,將等待執(zhí)行的 GM 對接。當(dāng) P 有任務(wù)時需要創(chuàng)建或者喚醒一個 M 來執(zhí)行它隊列里的任務(wù)。所以 P/M 需要進(jìn)行綁定,構(gòu)成一個執(zhí)行單元。P 決定了并行任務(wù)的數(shù)量,可通過 runtime.GOMAXPROCS 來設(shè)定。在 Go1.5 之后 GOMAXPROCS 被默認(rèn)設(shè)置可用的核數(shù),而之前則默認(rèn)為1

          Runtime 起始時會啟動一些 G:垃圾回收的 G,執(zhí)行調(diào)度的 G,運(yùn)行用戶代碼的 G;并且會創(chuàng)建一個 M 用來開始 G 的運(yùn)行。隨著時間的推移,更多的 G 會被創(chuàng)建出來,更多的 M 也會被創(chuàng)建出來。

          Tips: https://github.com/uber-go/automaxprocsAutomatically set GOMAXPROCS to match Linux container CPU quota.mcache/stackallocM 移到了 P,而 G 隊列也被分成兩類,保留全局 G 隊列,同時每個 P 中都會有一個本地的 G 隊列。

          GMP 調(diào)度

          GMP調(diào)度模型, 引入了 local queue,因為 P 的存在,runtime 并不需要做一個集中式的 goroutine 調(diào)度,每一個 M 都會在 P's local queueglobal queue 或者其他 P 隊列中找 G 執(zhí)行,減少全局鎖對性能的影響。這也是 GMP Work-stealing 調(diào)度算法的核心。注意 P 的本地 G 隊列還是可能面臨一個并發(fā)訪問的場景,為了避免加鎖,這里 P 的本地隊列是一個 LockFree的隊列,竊取 G 時使用 CAS 原子操作來完成。關(guān)于LockFreeCAS 的知識參見 Lock-Free

          4d8d29f6abf415f5f60fc4e295edb095.webp

          Work Stealing

          當(dāng)一個 P 執(zhí)行完本地所有的 G 之后,并且全局隊列為空的時候,會嘗試挑選一個受害者 P ,從它的 G 隊列中竊取一半的 G。否則會從全局隊列中獲取(當(dāng)前個數(shù)/GOMAXPROCS)個 G 。為了保證公平性,從隨機(jī)位置上的 P 開始,而且遍歷的順序也隨機(jī)化了(選擇一個小于 GOMAXPROCS ,且和它互為質(zhì)數(shù)的步長),保證遍歷的順序也隨機(jī)化了。

          fc7a4303fc1ce6a38e765fdb7430d0a3.webp


          光竊取失敗時獲取是不夠的,可能會導(dǎo)致全局隊列饑餓。P 的調(diào)度算法中還會每個 N 輪調(diào)度之后就去全局隊列拿一個 G。如下圖。

          50bbb07b307c1cb2db7cc250c70a34d2.webp

          誰放入的全局隊列呢

          有兩種情況會把G放到全局隊列中。

          • 新建 GP 的本地 G 隊列放不下已滿并達(dá)到256個的時候會放半數(shù) G 到全局隊列去。
          • 阻塞的系統(tǒng)調(diào)用返回時找不到空閑 P 也會放到全局隊列。
          143780a725e336e8ca5987a763c1a7c0.webp

          SysCall 系統(tǒng)調(diào)用

          當(dāng) G 調(diào)用 syscall 后會解綁 P,然后 MG 進(jìn)入阻塞,而 P 此時的狀態(tài)就是 syscall,表明這個 PG 正在 syscall 中,這時的 P 是不能被調(diào)度給別的 M 的。如果在短時間內(nèi)阻塞的 M 就喚醒了,那么 M 會優(yōu)先來重新獲取這個 P,能獲取到就繼續(xù)綁回去,這樣有利于數(shù)據(jù)的局部性。系統(tǒng)監(jiān)視器 (system monitor),稱為 sysmon,會定時掃描。在執(zhí)行 syscall 時, 如果某個 PG 執(zhí)行超過一個 sysmon tick(10ms),就會把他設(shè)為 idle,重新調(diào)度給需要的 M,強(qiáng)制解綁。

          a7b384f07797bf16d3e76d0f9ad74727.webp

          P3M 脫離后目前在 idle list 中等待被綁定(處于 syscall 狀態(tài))。而 syscall 結(jié)束后 M 按照如下規(guī)則執(zhí)行直到滿足其中一個條件:

          • 嘗試獲取同一個 P(P3),恢復(fù)執(zhí)行 G
          • 嘗試獲取 idle list 中的其他空閑 P,恢復(fù)執(zhí)行 G
          • 找不到空閑 P,把 G 放回 global queueM 放回到 idle list

          再舉一個例子:如下圖.6dd39e5718e439aa32b80ddcbaf9f941.webp

          • 第一步:G35發(fā)生了系統(tǒng)調(diào)用,長時間沒有返回。P1M 解綁。(p1 不會馬上被推送到idle list, 而是經(jīng)過一段時間才會推送到idle list.)
          • 第二步:G35系統(tǒng)調(diào)用完成,將G35推向了全局隊列.
          • 第三步:G35被其他的P撈到了(可能P0經(jīng)過1/61輪次正好check全局隊列), 這樣 G35 就可以繼續(xù)執(zhí)行了。

          需要注意的是:當(dāng)使用了 SyscallGo 無法限制 Blocked OS threads 的數(shù)量:The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. This package’s GOMAXPROCS function queries and changes the limit.

          Tips: 使用 syscall 寫程序要認(rèn)真考慮 pthread exhaust 問題。

          Spining Thread.

          線程自旋是相對于線程阻塞而言的,表象就是循環(huán)執(zhí)行一個指定邏輯(調(diào)度邏輯,目的是不停地尋找 G)。這樣做的問題顯而易見,如果 G 遲遲不來,CPU 會白白浪費在這無意義的計算上。但好處也很明顯,降低了 M 的上下文切換成本,提高了性能。在兩個地方引入自旋:

          • 類型1: M 不帶 P 的找 P 掛載(一有 P 釋放就結(jié)合)
          • 類型2: MP 的找 G 運(yùn)行(一有 runableG 就執(zhí)行)。這種情況下會首先 按照 1/61 輪次的查詢 global Queue , 然后再查看 local Queue 是否有 G. 如果沒有,則去查看 Global Queue, 如果沒有再去檢查 ?net poller, 看看是否有可用的 goroutine. 為了避免過多浪費 CPU 資源,自旋的 M 最多只允許 GOMAXPROCS (Busy P)。同時當(dāng)有類型1的自旋 M 存在時,類型2的自旋 M 就不阻塞,阻塞會釋放 P,一釋放 P 就馬上被類型1的自旋 M 搶走了,沒必要。

          在新 G 被創(chuàng)建、M 進(jìn)入系統(tǒng)調(diào)用、M 從空閑被激活這三種狀態(tài)變化前,調(diào)度器會確保至少有一個自旋 M 存在(喚醒或者創(chuàng)建一個 M),除非沒有空閑的 P

          為什么呢?

          • 當(dāng)新 G 創(chuàng)建,如果有可用 P,就意味著新 G 可以被立即執(zhí)行,即便不在同一個 P 也無妨,所以我們保留一個自旋的 M(這時應(yīng)該不存在類型1的自旋只有類型2的自旋)就可以保證新 G 很快被運(yùn)行。
          • 當(dāng) M 進(jìn)入系統(tǒng)調(diào)用,意味著 M 不知道何時可以醒來,那么 M 對應(yīng)的 P 中剩下的 G 就得有新的 M 來執(zhí)行,所以我們保留一個自旋的 M 來執(zhí)行剩下的 G(這時應(yīng)該不存在類型2的自旋只有類型1的自旋)。
          • 如果 M 從空閑變成活躍,意味著可能一個處于自旋狀態(tài)的 M 進(jìn)入工作狀態(tài)了,這時要檢查并確保還有一個自旋 M 存在,以防還有 G 或者還有 P 空著的。

          GMP 模型問題總結(jié)

          • 單一全局互斥鎖(Sched.Lock)和集中狀態(tài)存儲G 被分成全局隊列和 P 的本地隊列,全局隊列依舊是全局鎖,但是使用場景明顯很少,P 本地隊列使用無鎖隊列,使用原子操作來面對可能的并發(fā)場景。
          • Goroutine 傳遞問題G 創(chuàng)建時就在 P 的本地隊列,可以避免在 G 之間傳遞(竊取除外),GP 的數(shù)據(jù)局部性好; 當(dāng) G 開始執(zhí)行了,系統(tǒng)調(diào)用返回后 M 會嘗試獲取可用 P,獲取到了的話可以避免在 M 之間傳遞。而且優(yōu)先獲取調(diào)用阻塞前的 P,所以 GM 數(shù)據(jù)局部性好,GP 的數(shù)據(jù)局部性也好。
          • Per-M 持有內(nèi)存緩存 (M.mcache) 內(nèi)存 mcache 只存在 P 結(jié)構(gòu)中,P 最多只有 GOMAXPROCS 個,遠(yuǎn)小于 M 的個數(shù),所以內(nèi)存沒有過多的消耗。
          • 嚴(yán)重的線程阻塞/解鎖 通過引入自旋,保證任何時候都有處于等待狀態(tài)的自旋 M,避免在等待可用的 PG 時頻繁的阻塞和喚醒。

          syscon

          sysmon 也叫監(jiān)控線程,它無需 P 也可以運(yùn)行,他是一個死循環(huán),每20us~10ms循環(huán)一次,循環(huán)完一次就 sleep 一會,為什么會是一個變動的周期呢,主要是避免空轉(zhuǎn),如果每次循環(huán)都沒什么需要做的事,那么 sleep 的時間就會加大。

          • 釋放閑置超過5分鐘的 span 物理內(nèi)存;
          • 如果超過2分鐘沒有垃圾回收,強(qiáng)制執(zhí)行;
          • 將長時間未處理的 netpoll 添加到全局隊列;
          • 向長時間運(yùn)行的 G 任務(wù)發(fā)出搶占調(diào)度;
          • 收回因 syscall 長時間阻塞的 P
          f9ff789e4ae740be1e7617e67e1b3f39.webp

          當(dāng) PM 上執(zhí)行時間超過10mssysmon 調(diào)用 preemptoneG 標(biāo)記為 stackPreempt 。因此需要在某個地方觸發(fā)檢測邏輯,Go 當(dāng)前是在檢查棧是否溢出的地方判定(morestack()),M 會保存當(dāng)前 G 的上下文,重新進(jìn)入調(diào)度邏輯, 這樣就不會死循環(huán)了。死循環(huán):issues/11462信號搶占:go1.14基于信號的搶占式調(diào)度實現(xiàn)原理異步搶占,注冊 sigurg 信號,通過 sysmon 檢測,對 M 對應(yīng)的線程發(fā)送信號,觸發(fā)注冊的 handler,它往當(dāng)前 GPC 中插入一條指令(調(diào)用某個方法),在處理完 handlerG 恢復(fù)后,自己把自己推到了 global queue 中。

          319c17bfa87d8618be57945b47495438.webp

          Network poller

          Go 所有的 I/O 都是阻塞的。然后通過 goroutine + channel 來處理并發(fā)。因此所有的 IO 邏輯都是直來直去的,你不再需要回調(diào),不再需要 future,要的僅僅是 step by step。這對于代碼的可讀性是很有幫助的。G 發(fā)起網(wǎng)絡(luò) I/O 操作也不會導(dǎo)致 M 被阻塞(僅阻塞G),從而不會導(dǎo)致大量 M 被創(chuàng)建出來。將異步 I/O 轉(zhuǎn)換為阻塞 I/O 的部分稱為 netpoller。打開或接受連接都被設(shè)置為非阻塞模式。如果你試圖對其進(jìn)行 I/O 操作,并且文件描述符數(shù)據(jù)還沒有準(zhǔn)備好,G 會進(jìn)入 gopark 函數(shù),將當(dāng)前正在執(zhí)行的 G 狀態(tài)保存起來,然后切換到新的堆棧上執(zhí)行新的 G

          e3042a2004e7c2defacd6307978c763e.webp

          那什么時候 G 被調(diào)度回來呢?

          • sysmon
          • schedule()MG 的調(diào)度函數(shù)
          • GCstart the world調(diào)用 netpoll() 在某一次調(diào)度 G 的過程中,處于就緒狀態(tài)的 fd 對應(yīng)的 G 就會被調(diào)度回來。Ggopark 狀態(tài):G 置為 waiting 狀態(tài),等待顯示 goready 喚醒,在 poller 中用得較多,還有鎖、chan 等。
          ea1609b2b2c879f8e2af3eaf04ffee4a.webp

          Scheduler Affinity 調(diào)度親和性

          db4366d72b1f06ec24076eb46f5819a8.webp

          GM 調(diào)度器時代的,chan 操作導(dǎo)致的切換代價。

          • Goroutine#7 正在等待消息,阻塞在 chan。一旦收到消息,這個 goroutine 就被推到全局隊列。
          • 然后,chan 推送消息,goroutine#X 將在可用線程上運(yùn)行,而 goroutine#8 將阻塞在 chan
          • goroutine#7 現(xiàn)在在可用線程上運(yùn)行。在 chan 來回通信的 goroutine 會導(dǎo)致頻繁的 blocks,即頻繁地在本地隊列中重新排隊。然而,由于本地隊列是 FIFO 實現(xiàn),如果另一個 goroutine 占用線程,unblock goroutine 不能保證盡快運(yùn)行。同時 Go 親緣性調(diào)度的一些限制:Work-stealing、系統(tǒng)調(diào)用。goroutine #9chan 被阻塞后恢復(fù)。但是,它必須等待#2#5#4之后才能運(yùn)行。goroutine #5將阻塞其線程,從而延遲goroutine #9,并使其面臨被另一個 P 竊取的風(fēng)險。e5a03fc8637f8935a43d18a7858e53c5.webp

          針對 communicate-and-wait 模式,進(jìn)行了親緣性調(diào)度的優(yōu)化。Go 1.5P 中引入了 runnext 特殊的一個字段,可以高優(yōu)先級執(zhí)行 unblock Ggoroutine #9現(xiàn)在被標(biāo)記為下一個可運(yùn)行的。這種新的優(yōu)先級排序允許 goroutine 在再次被阻塞之前快速運(yùn)行。這一變化對運(yùn)行中的標(biāo)準(zhǔn)庫產(chǎn)生了總體上的積極影響,提高了一些包的性能。

          f110a7557fbc74697733bc335220ff4b.webp

          Goroutine Lifecycle

          go 程序的啟動

          c33d3a4183e2cb1d0ae1d7e111bf322d.webp

          整個程序始于一段匯編,而在隨后的 runtime·rt0_go(也是匯編程序)中,會執(zhí)行很多初始化工作。

          4a42000a9361016fd561d1f410ab7f1d.webp
          • 綁定 m0 和 g0,m0就是程序的主線程,程序啟動必然會擁有一個主線程,這個就是 m0。g0 負(fù)責(zé)調(diào)度,即 shedule() 函數(shù)。
          • 創(chuàng)建 P,綁定 m0 和 p0,首先會創(chuàng)建 GOMAXPROCS 個 P ,存儲在 sched 的 空閑鏈表(pidle)。
          • 新建任務(wù) g 到 p0 本地隊列,m0 的 g0 會創(chuàng)建一個 指向 runtime.main() 的 g ,并放到 p0 的本地隊列。runtime.main(): 啟動 sysmon 線程;啟動 GC 協(xié)程;執(zhí)行 init,即代碼中的各種 init 函數(shù);執(zhí)行 main.main 函數(shù)。

          Os Thread 創(chuàng)建

          準(zhǔn)備運(yùn)行的新 goroutine 將喚醒 P 以更好地分發(fā)工作。這個 P 將創(chuàng)建一個與之關(guān)聯(lián)的 M 綁定到一個 OS thread。69dbec0715ec9fa0275d95bacadd419e.webpdc0bb6613defb3cfe05fd42889a7c4fb.webp

          go func() 中 觸發(fā) Wakeup 喚醒機(jī)制:有空閑的 P 而沒有在 spinning 狀態(tài)的 M 時候, 需要去喚醒一個空閑(睡眠)的 M 或者新建一個。當(dāng)線程首次創(chuàng)建時,會執(zhí)行一個特殊的 G,即 g0,它負(fù)責(zé)管理和調(diào)度 G76a47b3464fd5c4949de978fe6f8636a.webp

          特殊的g0

          Go 基于兩種斷點將 G 調(diào)度到線程上:

          • 當(dāng) G 阻塞時:系統(tǒng)調(diào)用、互斥鎖或 chan。阻塞的 G 進(jìn)入睡眠模式/進(jìn)入隊列,并允許 Go 安排和運(yùn)行等待其他的 G
          • 在函數(shù)調(diào)用期間,如果 G 必須擴(kuò)展其堆棧。這個斷點允許 Go 調(diào)度另一個 G 并避免運(yùn)行 G 占用 CPU。在這兩種情況下,運(yùn)行調(diào)度程序的 g0 將當(dāng)前 G 替換為另一個 G,即 ready to run。然后,選擇的 G 替換 g0 并在線程上運(yùn)行。與常規(guī) G 相反,g0 有一個固定和更大的棧。
          • Defer 函數(shù)的分配
          • GC 收集,比如 STW、掃描 G 的堆棧和標(biāo)記、清除操作
          • 棧擴(kuò)容,當(dāng)需要的時候,由 g0 進(jìn)行擴(kuò)棧操作
          292903f756ffaf4708744a2800218ba2.webp

          Schedule

          Go 中,G 的切換相當(dāng)輕便,其中需要保存的狀態(tài)僅僅涉及以下兩個:

          • Goroutine 在停止運(yùn)行前執(zhí)行的指令,程序當(dāng)前要運(yùn)行的指令是記錄在程序計數(shù)器(PC)中的, G 稍后將在同一指令處恢復(fù)運(yùn)行;
          • G 的堆棧,以便在再次運(yùn)行時還原局部變量;在切換之前,堆棧將被保存,以便在 G 再次運(yùn)行時進(jìn)行恢復(fù):

          6851b93bbab45c12d2e137a16ecdfa34.webpa4e4d4bee2b3fa23db47c62f7503bb65.webp64ebb5202c0f37b696c477af9fc3b06c.webp4c0d7ab03bcba9a410fcc397e42f6853.webp

          gg0 或從 g0g 的切換是相當(dāng)迅速的,它們只包含少量固定的指令(9-10ns)。相反,對于調(diào)度階段,調(diào)度程序需要檢查許多資源以便確定下一個要運(yùn)行的 G。當(dāng)前 g 阻塞在 chan 上并切換到 g0

          • 1、PC 和堆棧指針一起保存在內(nèi)部結(jié)構(gòu)中;
          • 2、將 g0 設(shè)置為正在運(yùn)行的 goroutine;
          • 3、g0 的堆棧替換當(dāng)前堆棧;

          g0 尋找新的 Goroutine 來運(yùn)行g0 使用所選的 Goroutine 進(jìn)行切換:

          • 1、PC 和堆棧指針是從其內(nèi)部結(jié)構(gòu)中獲取的;
          • 2、程序跳轉(zhuǎn)到對應(yīng)的 PC 地址;
          eb3efe30f120a368a74660a221aae0da.webp

          Goroutine Recycle

          goroutine重用

          G 很容易創(chuàng)建,棧很小以及快速的上下文切換。基于這些原因,開發(fā)人員非常喜歡并使用它們。然而,一個產(chǎn)生許多 shortliveG 的程序?qū)⒒ㄙM相當(dāng)長的時間來創(chuàng)建和銷毀它們。每個 P 維護(hù)一個 freelist G,保持這個列表是本地的,這樣做的好處是不使用任何鎖來 push/get 一個空閑的 G。當(dāng) G 退出當(dāng)前工作時,它將被 push 到這個空閑列表中。

          94f0940b37a41ff7c3544b074bc820f5.webp

          為了更好地分發(fā)空閑的 G ,調(diào)度器也有自己的列表。它實際上有兩個列表:一個包含已分配棧的 G,另一個包含釋放過堆棧的 G(無棧)。鎖保護(hù) central list,因為任何 M 都可以訪問它。當(dāng)本地列表長度超過64時,調(diào)度程序持有的列表從 P 獲取 G。然后一半的 G 將移動到中心列表(central list)。需求回收 G 是一種節(jié)省分配成本的好方法。但是,由于堆棧是動態(tài)增長的,現(xiàn)有的G 最終可能會有一個大棧。因此,當(dāng)堆棧增長(即超過2K)時,Go 不會保留這些棧。

          5a6cbd7eaad94c9463756806645f3cf6.webp

          最后

          希望和你一起遇見更好的自己

          看到這里啦,就點個關(guān)注再走吧~

          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  簧片网站在线观看 | 欧美插逼视频 | 黄免费视频网站 | 在线一级黄色录像 | 久草免费在线视频 |