詳解Go語言中調(diào)度器的原理與使用
概述
Go 語言在并發(fā)編程方面有強(qiáng)大的能力,這離不開語言層面對(duì)并發(fā)編程的支持。本節(jié)會(huì)介紹 Go 語言運(yùn)行時(shí)調(diào)度器的實(shí)現(xiàn)原理,其中包含調(diào)度器的設(shè)計(jì)與實(shí)現(xiàn)原理、演變過程以及與運(yùn)行時(shí)調(diào)度相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
談到 Go 語言調(diào)度器,我們繞不開的是操作系統(tǒng)、進(jìn)程與線程這些概念,線程是操作系統(tǒng)調(diào)度時(shí)的最基本單元,而 Linux 在調(diào)度器并不區(qū)分進(jìn)程和線程的調(diào)度,它們?cè)诓煌僮飨到y(tǒng)上也有不同的實(shí)現(xiàn),但是在大多數(shù)的實(shí)現(xiàn)中線程都屬于進(jìn)程:
多個(gè)線程可以屬于同一個(gè)進(jìn)程并共享內(nèi)存空間。因?yàn)槎嗑€程不需要?jiǎng)?chuàng)建新的虛擬內(nèi)存空間,所以它們也不需要內(nèi)存管理單元處理上下文的切換,線程之間的通信也正是基于共享的內(nèi)存進(jìn)行的,與重量級(jí)的進(jìn)程相比,線程顯得比較輕量。
雖然線程比較輕量,但是在調(diào)度時(shí)也有比較大的額外開銷。每個(gè)線程會(huì)都占用 1M 以上的內(nèi)存空間,在切換線程時(shí)不止會(huì)消耗較多的內(nèi)存,恢復(fù)寄存器中的內(nèi)容還需要向操作系統(tǒng)申請(qǐng)或者銷毀資源,每一次線程上下文的切換都需要消耗 ~1us 左右的時(shí)間,但是 Go 調(diào)度器對(duì) Goroutine 的上下文切換約為 ~0.2us,減少了 80% 的額外開銷。
Go 語言的調(diào)度器通過使用與 CPU 數(shù)量相等的線程減少線程頻繁切換的內(nèi)存開銷,同時(shí)在每一個(gè)線程上執(zhí)行額外開銷更低的 Goroutine 來降低操作系統(tǒng)和硬件的負(fù)載。
設(shè)計(jì)原理
今天的 Go 語言調(diào)度器有著優(yōu)異的性能,但是如果我們回頭看 Go 語言的 0.x 版本的調(diào)度器會(huì)發(fā)現(xiàn)最初的調(diào)度器不僅實(shí)現(xiàn)非常簡(jiǎn)陋,也無法支撐高并發(fā)的服務(wù)。調(diào)度器經(jīng)過幾個(gè)大版本的迭代才有今天的優(yōu)異性能,歷史上幾個(gè)不同版本的調(diào)度器引入了不同的改進(jìn),也存在著不同的缺陷:
1.單線程調(diào)度器 — Go 0.x
- 改進(jìn):只包含 40 多行代碼;
- 缺陷:程序中只能存在一個(gè)活躍線程,由 G-M 模型組成;
2.多線程調(diào)度器 — Go 1.0
- 改進(jìn):允許運(yùn)行多線程的程序;
- 缺陷:全局鎖導(dǎo)致競(jìng)爭(zhēng)嚴(yán)重;
3.任務(wù)竊取調(diào)度器 — Go 1.1
- 改進(jìn)1:引入了處理器 P,構(gòu)成了目前的 G-M-P 模型;
- 改進(jìn)2:在處理器 P 的基礎(chǔ)上實(shí)現(xiàn)了基于工作竊取的調(diào)度器;
- 缺陷1:在某些情況下,Goroutine 不會(huì)讓出線程,進(jìn)而造成饑餓問題;
- 缺陷2:時(shí)間過長(zhǎng)的垃圾回收(Stop-the-world,STW)會(huì)導(dǎo)致程序長(zhǎng)時(shí)間無法工作;
4..搶占式調(diào)度器 — Go 1.2 ~ 至今
基于協(xié)作的搶占式調(diào)度器 - 1.2 ~ 1.13
- 改進(jìn):通過編譯器在函數(shù)調(diào)用時(shí)插入搶占檢查指令,在函數(shù)調(diào)用時(shí)檢查當(dāng)前 Goroutine 是否發(fā)起了搶占請(qǐng)求,實(shí)現(xiàn)基于協(xié)作的搶占式調(diào)度;
- 缺陷:Goroutine 可能會(huì)因?yàn)槔厥蘸脱h(huán)長(zhǎng)時(shí)間占用資源導(dǎo)致程序暫停;
基于信號(hào)的搶占式調(diào)度器 - 1.14 ~ 至今
- 改進(jìn):實(shí)現(xiàn)基于信號(hào)的真搶占式調(diào)度;
- 缺陷1:垃圾回收在掃描棧時(shí)會(huì)觸發(fā)搶占調(diào)度;
- 缺陷2:搶占的時(shí)間點(diǎn)不夠多,還不能覆蓋全部的邊緣情況;
5.非均勻存儲(chǔ)訪問調(diào)度器 — 提案
- 改進(jìn):對(duì)運(yùn)行時(shí)的各種資源進(jìn)行分區(qū);
- 缺陷:實(shí)現(xiàn)非常復(fù)雜,到今天還沒有提上日程;
除了多線程、任務(wù)竊取和搶占式調(diào)度器之外,Go 語言社區(qū)目前還有一個(gè)非均勻存儲(chǔ)訪問(Non-uniform memory access,NUMA)調(diào)度器的提案。在這一節(jié)中,我們將依次介紹不同版本調(diào)度器的實(shí)現(xiàn)原理以及未來可能會(huì)實(shí)現(xiàn)的調(diào)度器提案。
單線程調(diào)度器
0.x 版本調(diào)度器只包含兩種結(jié)構(gòu) — 表示 Goroutine 的 G 和表示線程的 M 兩種結(jié)構(gòu),全局也只有一個(gè)線程。我們可以在 clean up scheduler 提交中找到單線程調(diào)度器的源代碼,在這時(shí) Go 語言的調(diào)度器還是由 C 語言實(shí)現(xiàn)的,調(diào)度函數(shù) runtime.scheduler:9682400
也只包含 40 多行代碼 :
static void scheduler(void) { G* gp; lock(&sched); if(gosave(&m->sched)){ lock(&sched); gp = m->curg; switch(gp->status){ case Grunnable: case Grunning: gp->status = Grunnable; gput(gp); break; ... } notewakeup(&gp->stopped); } gp = nextgandunlock(); noteclear(&gp->stopped); gp->status = Grunning; m->curg = gp; g = gp; gogo(&gp->sched); }
該函數(shù)會(huì)遵循如下的過程調(diào)度 Goroutine:
- 獲取調(diào)度器的全局鎖;
- 調(diào)用
runtime.gosave:9682400
保存棧寄存器和程序計(jì)數(shù)器; - 調(diào)用
runtime.nextgandunlock:9682400
獲取下一個(gè)需要運(yùn)行的 Goroutine 并解鎖調(diào)度器; - 修改全局線程
m
上要執(zhí)行的 Goroutine; - 調(diào)用
runtime.gogo:9682400
函數(shù)運(yùn)行最新的 Goroutine;
雖然這個(gè)單線程調(diào)度器的唯一優(yōu)點(diǎn)就是能運(yùn)行,但是這次提交已經(jīng)包含了 G 和 M 兩個(gè)重要的數(shù)據(jù)結(jié)構(gòu),也建立了 Go 語言調(diào)度器的框架。
多線程調(diào)度器
Go 語言在 1.0 版本正式發(fā)布時(shí)就支持了多線程的調(diào)度器,與上一個(gè)版本幾乎不可用的調(diào)度器相比,Go 語言團(tuán)隊(duì)在這一階段實(shí)現(xiàn)了從不可用到可用的跨越。我們可以在 pkg/runtime/proc.c
文件中找到 1.0.1 版本的調(diào)度器,多線程版本的調(diào)度函數(shù) runtime.schedule:go1.0.1
包含 70 多行代碼,我們?cè)谶@里保留了該函數(shù)的核心邏輯:
static void schedule(G *gp) { schedlock(); if(gp != nil) { gp->m = nil; uint32 v = runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift); if(atomic_mcpu(v) > maxgomaxprocs) runtime·throw("negative mcpu in scheduler"); switch(gp->status){ case Grunning: gp->status = Grunnable; gput(gp); break; case ...: } } else { ... } gp = nextgandunlock(); gp->status = Grunning; m->curg = gp; gp->m = m; runtime·gogo(&gp->sched, 0); }
整體的邏輯與單線程調(diào)度器沒有太多區(qū)別,因?yàn)槲覀兊某绦蛑锌赡芡瑫r(shí)存在多個(gè)活躍線程,所以多線程調(diào)度器引入了 GOMAXPROCS
變量幫助我們靈活控制程序中的最大處理器數(shù),即活躍線程數(shù)。
多線程調(diào)度器的主要問題是調(diào)度時(shí)的鎖競(jìng)爭(zhēng)會(huì)嚴(yán)重浪費(fèi)資源,Scalable Go Scheduler Design Doc 中對(duì)調(diào)度器做的性能測(cè)試發(fā)現(xiàn) 14% 的時(shí)間都花費(fèi)在 runtime.futex:go1.0.1
上,該調(diào)度器有以下問題需要解決:
- 調(diào)度器和鎖是全局資源,所有的調(diào)度狀態(tài)都是中心化存儲(chǔ)的,鎖競(jìng)爭(zhēng)問題嚴(yán)重;
- 線程需要經(jīng)常互相傳遞可運(yùn)行的 Goroutine,引入了大量的延遲;
- 每個(gè)線程都需要處理內(nèi)存緩存,導(dǎo)致大量的內(nèi)存占用并影響數(shù)據(jù)局部性;
- 系統(tǒng)調(diào)用頻繁阻塞和解除阻塞正在運(yùn)行的線程,增加了額外開銷;
這里的全局鎖問題和 Linux 操作系統(tǒng)調(diào)度器在早期遇到的問題比較相似,解決的方案也都大同小異。
任務(wù)竊取調(diào)度器
2012 年 Google 的工程師 Dmitry Vyukov 在 Scalable Go Scheduler Design Doc 中指出了現(xiàn)有多線程調(diào)度器的問題并在多線程調(diào)度器上提出了兩個(gè)改進(jìn)的手段:
- 在當(dāng)前的 G-M 模型中引入了處理器 P,增加中間層;
- 在處理器 P 的基礎(chǔ)上實(shí)現(xiàn)基于工作竊取的調(diào)度器;
基于任務(wù)竊取的 Go 語言調(diào)度器使用了沿用至今的 G-M-P 模型,我們能在 runtime: improved scheduler 提交中找到任務(wù)竊取調(diào)度器剛被實(shí)現(xiàn)時(shí)的源代碼,調(diào)度器的 runtime.schedule:779c45a
在這個(gè)版本的調(diào)度器中反而更簡(jiǎn)單了:
static void schedule(void) { G *gp; top: if(runtime·gcwaiting) { gcstopm(); goto top; } gp = runqget(m->p); if(gp == nil) gp = findrunnable(); ... execute(gp); }
- 如果當(dāng)前運(yùn)行時(shí)在等待垃圾回收,調(diào)用
runtime.gcstopm:779c45a
函數(shù); - 調(diào)用
runtime.runqget:779c45a
和runtime.findrunnable:779c45a
從本地或者全局的運(yùn)行隊(duì)列中獲取待執(zhí)行的 Goroutine; - 調(diào)用
runtime.execute:779c45a
在當(dāng)前線程 M 上運(yùn)行 Goroutine;
當(dāng)前處理器本地的運(yùn)行隊(duì)列中不包含 Goroutine 時(shí),調(diào)用 runtime.findrunnable:779c45a
會(huì)觸發(fā)工作竊取,從其它的處理器的隊(duì)列中隨機(jī)獲取一些 Goroutine。
運(yùn)行時(shí) G-M-P 模型中引入的處理器 P 是線程和 Goroutine 的中間層,我們從它的結(jié)構(gòu)體中就能看到處理器與 M 和 G 的關(guān)系:
struct P { Lock; uint32 status; P* link; uint32 tick; M* m; MCache* mcache; G** runq; int32 runqhead; int32 runqtail; int32 runqsize; G* gfree; int32 gfreecnt; };
處理器持有一個(gè)由可運(yùn)行的 Goroutine 組成的環(huán)形的運(yùn)行隊(duì)列 runq
,還反向持有一個(gè)線程。調(diào)度器在調(diào)度時(shí)會(huì)從處理器的隊(duì)列中選擇隊(duì)列頭的 Goroutine 放到線程 M 上執(zhí)行。
基于工作竊取的多線程調(diào)度器將每一個(gè)線程綁定到了獨(dú)立的 CPU 上,這些線程會(huì)被不同處理器管理,不同的處理器通過工作竊取對(duì)任務(wù)進(jìn)行再分配實(shí)現(xiàn)任務(wù)的平衡,也能提升調(diào)度器和 Go 語言程序的整體性能,今天所有的 Go 語言服務(wù)都受益于這一改動(dòng)。
搶占式調(diào)度器
對(duì) Go 語言并發(fā)模型的修改提升了調(diào)度器的性能,但是 1.1 版本中的調(diào)度器仍然不支持搶占式調(diào)度,程序只能依靠 Goroutine 主動(dòng)讓出 CPU 資源才能觸發(fā)調(diào)度。Go 語言的調(diào)度器在 1.2 版本中引入基于協(xié)作的搶占式調(diào)度解決下面的問題:
- 某些 Goroutine 可以長(zhǎng)時(shí)間占用線程,造成其它 Goroutine 的饑餓;
- 垃圾回收需要暫停整個(gè)程序(Stop-the-world,STW),最長(zhǎng)可能需要幾分鐘的時(shí)間,導(dǎo)致整個(gè)程序無法工作;
1.2 版本的搶占式調(diào)度雖然能夠緩解這個(gè)問題,但是它實(shí)現(xiàn)的搶占式調(diào)度是基于協(xié)作的,在之后很長(zhǎng)的一段時(shí)間里 Go 語言的調(diào)度器都有一些無法被搶占的邊緣情況,例如:for 循環(huán)或者垃圾回收長(zhǎng)時(shí)間占用線程,這些問題中的一部分直到 1.14 才被基于信號(hào)的搶占式調(diào)度解決。
基于協(xié)作的搶占式調(diào)度
我們可以在 pkg/runtime/proc.c
文件中找到引入基于協(xié)作的搶占式調(diào)度后的調(diào)度器。Go 語言會(huì)在分段棧的機(jī)制上實(shí)現(xiàn)搶占調(diào)度,利用編譯器在分段棧上插入的函數(shù),所有 Goroutine 在函數(shù)調(diào)用時(shí)都有機(jī)會(huì)進(jìn)入運(yùn)行時(shí)檢查是否需要執(zhí)行搶占。Go 團(tuán)隊(duì)通過以下的多個(gè)提交實(shí)現(xiàn)該特性:
runtime: add stackguard0 to G
為 Goroutine 引入 stackguard0
字段,該字段被設(shè)置成 StackPreempt
意味著當(dāng)前 Goroutine 發(fā)出了搶占請(qǐng)求;
runtime: introduce preemption function (not used for now)
- 引入搶占函數(shù)
runtime.preemptone:1e112cd
和runtime.preemptall:1e112cd
,這兩個(gè)函數(shù)會(huì)改變 Goroutine 的stackguard0
字段發(fā)出搶占請(qǐng)求; - 定義搶占請(qǐng)求
StackPreempt
;
runtime: preempt goroutines for GC
- 在
runtime.stoptheworld:1e112cd
中調(diào)用runtime.preemptall:1e112cd
設(shè)置所有處理器上正在運(yùn)行的 Goroutine 的stackguard0
為StackPreempt
; - 在
runtime.newstack:1e112cd
中增加搶占的代碼,當(dāng)stackguard0
等于StackPreempt
時(shí)觸發(fā)調(diào)度器搶占讓出線程;
runtime: preempt long-running goroutines
在系統(tǒng)監(jiān)控中,如果一個(gè) Goroutine 的運(yùn)行時(shí)間超過 10ms,就會(huì)調(diào)用 runtime.retake:1e112cd
和 runtime.preemptone:1e112cd
;
runtime: more reliable preemption
修復(fù) Goroutine 因?yàn)橹芷谛詧?zhí)行非阻塞的 CGO 或者系統(tǒng)調(diào)用不會(huì)被搶占的問題;
上面的多個(gè)提交實(shí)現(xiàn)了搶占式調(diào)度,但是還缺少最關(guān)鍵的一個(gè)環(huán)節(jié) — 編譯器如何在函數(shù)調(diào)用前插入函數(shù),我們能在非常古老的提交 runtime: stack growth adjustments, cleanup 中找到編譯器插入函數(shù)的雛形,最新版本的 Go 語言會(huì)通過 cmd/internal/obj/x86.stacksplit
插入 runtime.morestack
,該函數(shù)可能會(huì)調(diào)用 runtime.newstack
觸發(fā)搶占。從上面的多個(gè)提交中,我們能歸納出基于協(xié)作的搶占式調(diào)度的工作原理:
- 編譯器會(huì)在調(diào)用函數(shù)前插入
runtime.morestack
; - Go 語言運(yùn)行時(shí)會(huì)在垃圾回收暫停程序、系統(tǒng)監(jiān)控發(fā)現(xiàn) Goroutine 運(yùn)行超過 10ms 時(shí)發(fā)出搶占請(qǐng)求
StackPreempt
; - 當(dāng)發(fā)生函數(shù)調(diào)用時(shí),可能會(huì)執(zhí)行編譯器插入的
runtime.morestack
,它調(diào)用的runtime.newstack
會(huì)檢查 Goroutine 的stackguard0
字段是否為StackPreempt
; - 如果
stackguard0
是StackPreempt
,就會(huì)觸發(fā)搶占讓出當(dāng)前線程;
這種實(shí)現(xiàn)方式雖然增加了運(yùn)行時(shí)的復(fù)雜度,但是實(shí)現(xiàn)相對(duì)簡(jiǎn)單,也沒有帶來過多的額外開銷,總體來看還是比較成功的實(shí)現(xiàn),也在 Go 語言中使用了 10 幾個(gè)版本。因?yàn)檫@里的搶占是通過編譯器插入函數(shù)實(shí)現(xiàn)的,還是需要函數(shù)調(diào)用作為入口才能觸發(fā)搶占,所以這是一種協(xié)作式的搶占式調(diào)度。
基于信號(hào)的搶占式調(diào)度
基于協(xié)作的搶占式調(diào)度雖然實(shí)現(xiàn)巧妙,但是并不完備,我們能在 runtime: non-cooperative goroutine preemption 中找到一些遺留問題:
- runtime: tight loops should be preemptible
- An empty for{} will block large slice allocation in another goroutine, even with GOMAXPROCS > 1 ?
- runtime: tight loop hangs process completely after some time
- …
Go 語言在 1.14 版本中實(shí)現(xiàn)了非協(xié)作的搶占式調(diào)度,在實(shí)現(xiàn)的過程中我們重構(gòu)已有的邏輯并為 Goroutine 增加新的狀態(tài)和字段來支持搶占。Go 團(tuán)隊(duì)通過下面的一系列提交實(shí)現(xiàn)了這一功能,我們可以按時(shí)間順序分析相關(guān)提交理解它的工作原理:
runtime: add general suspendG/resumeG
- 掛起 Goroutine 的過程是在垃圾回收的棧掃描時(shí)完成的,我們通過
runtime.suspendG
和runtime.resumeG
兩個(gè)函數(shù)重構(gòu)棧掃描這一過程; - 調(diào)用
runtime.suspendG
時(shí)會(huì)將處于運(yùn)行狀態(tài)的 Goroutine 的preemptStop
標(biāo)記成true
; - 調(diào)用
runtime.preemptPark
可以掛起當(dāng)前 Goroutine、將其狀態(tài)更新成_Gpreempted
并觸發(fā)調(diào)度器的重新調(diào)度,該函數(shù)能夠交出線程控制權(quán);
runtime: asynchronous preemption function for x86
在 x86 架構(gòu)上增加異步搶占的函數(shù) runtime.asyncPreempt
和 runtime.asyncPreempt2
;
runtime: use signals to preempt Gs for suspendG
- 支持通過向線程發(fā)送信號(hào)的方式暫停運(yùn)行的 Goroutine;
- 在
runtime.sighandler
函數(shù)中注冊(cè)SIGURG
信號(hào)的處理函數(shù)runtime.doSigPreempt
; - 實(shí)現(xiàn)
runtime.preemptM
,它可以通過SIGURG
信號(hào)向線程發(fā)送搶占請(qǐng)求;
runtime: implement async scheduler preemption
修改 runtime.preemptone
函數(shù)的實(shí)現(xiàn),加入異步搶占的邏輯;
目前的搶占式調(diào)度也只會(huì)在垃圾回收掃描任務(wù)時(shí)觸發(fā),我們可以梳理一下上述代碼實(shí)現(xiàn)的搶占式調(diào)度過程:
1.程序啟動(dòng)時(shí),在 runtime.sighandler
中注冊(cè) SIGURG
信號(hào)的處理函數(shù) runtime.doSigPreempt
;
2.在觸發(fā)垃圾回收的棧掃描時(shí)會(huì)調(diào)用 runtime.suspendG
掛起 Goroutine,該函數(shù)會(huì)執(zhí)行下面的邏輯:
將 _Grunning
狀態(tài)的 Goroutine 標(biāo)記成可以被搶占,即將 preemptStop
設(shè)置成 true
;
調(diào)用 runtime.preemptM
觸發(fā)搶占;
3.runtime.preemptM
會(huì)調(diào)用 runtime.signalM
向線程發(fā)送信號(hào) SIGURG
;
4.操作系統(tǒng)會(huì)中斷正在運(yùn)行的線程并執(zhí)行預(yù)先注冊(cè)的信號(hào)處理函數(shù) runtime.doSigPreempt
;
5.runtime.doSigPreempt
函數(shù)會(huì)處理搶占信號(hào),獲取當(dāng)前的 SP 和 PC 寄存器并調(diào)用 runtime.sigctxt.pushCall
;
6.runtime.sigctxt.pushCall
會(huì)修改寄存器并在程序回到用戶態(tài)時(shí)執(zhí)行 runtime.asyncPreempt
;
7.匯編指令 runtime.asyncPreempt
會(huì)調(diào)用運(yùn)行時(shí)函數(shù) runtime.asyncPreempt2
;
8.runtime.asyncPreempt2
會(huì)調(diào)用 runtime.preemptPark
;
9.runtime.preemptPark
會(huì)修改當(dāng)前 Goroutine 的狀態(tài)到 _Gpreempted
并調(diào)用 runtime.schedule
讓當(dāng)前函數(shù)陷入休眠并讓出線程,調(diào)度器會(huì)選擇其它的 Goroutine 繼續(xù)執(zhí)行;
上述 9 個(gè)步驟展示了基于信號(hào)的搶占式調(diào)度的執(zhí)行過程。除了分析搶占的過程之外,我們還需要討論一下?lián)屨夹盘?hào)的選擇,提案根據(jù)以下的四個(gè)原因選擇 SIGURG
作為觸發(fā)異步搶占的信號(hào);
- 該信號(hào)需要被調(diào)試器透?jìng)鳎?/li>
- 該信號(hào)不會(huì)被內(nèi)部的 libc 庫使用并攔截;
- 該信號(hào)可以隨意出現(xiàn)并且不觸發(fā)任何后果;
- 我們需要處理多個(gè)平臺(tái)上的不同信號(hào);
STW 和棧掃描是一個(gè)可以搶占的安全點(diǎn)(Safe-points),所以 Go 語言會(huì)在這里先加入搶占功能?;谛盘?hào)的搶占式調(diào)度只解決了垃圾回收和棧掃描時(shí)存在的問題,它到目前為止沒有解決所有問題,但是這種真搶占式調(diào)度是調(diào)度器走向完備的開始,相信在未來我們會(huì)在更多的地方觸發(fā)搶占。
非均勻內(nèi)存訪問調(diào)度器
非均勻內(nèi)存訪問(Non-uniform memory access,NUMA)調(diào)度器現(xiàn)在只是 Go 語言的提案。該提案的原理就是通過拆分全局資源,讓各個(gè)處理器能夠就近獲取,減少鎖競(jìng)爭(zhēng)并增加數(shù)據(jù)的局部性。
在目前的運(yùn)行時(shí)中,線程、處理器、網(wǎng)絡(luò)輪詢器、運(yùn)行隊(duì)列、全局內(nèi)存分配器狀態(tài)、內(nèi)存分配緩存和垃圾收集器都是全局資源。運(yùn)行時(shí)沒有保證本地化,也不清楚系統(tǒng)的拓?fù)浣Y(jié)構(gòu),部分結(jié)構(gòu)可以提供一定的局部性,但是從全局來看沒有這種保證。
如上圖所示,堆棧、全局運(yùn)行隊(duì)列和線程池會(huì)按照 NUMA 節(jié)點(diǎn)進(jìn)行分區(qū),網(wǎng)絡(luò)輪詢器和計(jì)時(shí)器會(huì)由單獨(dú)的處理器持有。這種方式雖然能夠利用局部性提高調(diào)度器的性能,但是本身的實(shí)現(xiàn)過于復(fù)雜,所以 Go 語言團(tuán)隊(duì)還沒有著手實(shí)現(xiàn)這一提案。
小結(jié)
Go 語言的調(diào)度器在最初的幾個(gè)版本中迅速迭代,但是從 1.2 版本之后調(diào)度器就沒有太多的變化,直到 1.14 版本引入了真正的搶占式調(diào)度才解決了自 1.2 以來一直存在的問題。在可預(yù)見的未來,Go 語言的調(diào)度器還會(huì)進(jìn)一步演進(jìn),增加觸發(fā)搶占式調(diào)度的時(shí)間點(diǎn)以減少存在的邊緣情況。
以上就是詳解Go語言中調(diào)度器的原理與使用的詳細(xì)內(nèi)容,更多關(guān)于Go語言調(diào)度器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go?Web開發(fā)之Gin多服務(wù)配置及優(yōu)雅關(guān)閉平滑重啟實(shí)現(xiàn)方法
這篇文章主要為大家介紹了Go?Web開發(fā)之Gin多服務(wù)配置及優(yōu)雅關(guān)閉平滑重啟實(shí)現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Go語言中如何確保Cookie數(shù)據(jù)的安全傳輸
這篇文章主要介紹了Go語言中如何確保Cookie數(shù)據(jù)的安全傳輸,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03Golang中struct{}和struct{}{}的區(qū)別解析
這篇文章主要介紹了Golang中struct{}和struct{}{}的區(qū)別,通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03一文搞懂Golang 時(shí)間和日期相關(guān)函數(shù)
這篇文章主要介紹了Golang 時(shí)間和日期相關(guān)函數(shù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12goland 實(shí)現(xiàn)websocket server的示例代碼
本文主要介紹了goland 實(shí)現(xiàn)websocket server的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06Golang使用lua腳本實(shí)現(xiàn)redis原子操作
這篇文章主要介紹了Golang使用lua腳本實(shí)現(xiàn)redis原子操作,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的工作或?qū)W習(xí)具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03Go語言使用goroutine及通道實(shí)現(xiàn)并發(fā)詳解
這篇文章主要為大家介紹了Go語言使用goroutine及通道實(shí)現(xiàn)并發(fā)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08