0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

goroutine調(diào)度器的概念、演進(jìn)及場(chǎng)景分析

馬哥Linux運(yùn)維 ? 來源:馬哥Linux運(yùn)維 ? 作者:馬哥Linux運(yùn)維 ? 2022-10-12 09:42 ? 次閱讀

goroutine 調(diào)度器的概念

說到“調(diào)度”,首先會(huì)想到操作系統(tǒng)對(duì)進(jìn)程、線程的調(diào)度。操作系統(tǒng)調(diào)度器會(huì)將系統(tǒng)中的多個(gè)線程按照一定算法調(diào)度到物理 CPU 上去運(yùn)行。

傳統(tǒng)的編程語(yǔ)言比如 C、C++ 等的并發(fā)實(shí)現(xiàn)實(shí)際上就是基于操作系統(tǒng)調(diào)度的,即程序負(fù)責(zé)創(chuàng)建線程,操作系統(tǒng)負(fù)責(zé)調(diào)度。

盡管線程的調(diào)度方式相對(duì)于進(jìn)程來說,線程運(yùn)行所需要資源比較少,在同一進(jìn)程中進(jìn)行線程切換效率會(huì)高很多,但實(shí)際上多線程開發(fā)設(shè)計(jì)會(huì)變得更加復(fù)雜,要考慮很多同步競(jìng)爭(zhēng)等問題,如鎖、競(jìng)爭(zhēng)沖突等。

線程是操作系統(tǒng)調(diào)度時(shí)的最基本單元,而 Linux 在調(diào)度器并不區(qū)分進(jìn)程和線程的調(diào)度,只是說線程調(diào)度因?yàn)橘Y源少,所以切換的效率比較高。

使用多線程編程會(huì)遇到以下問題:

并發(fā)單元間通信困難,易錯(cuò):多個(gè) thread 之間的通信雖然有多種機(jī)制可選,但用起來是相當(dāng)復(fù)雜;并且一旦涉及到共享內(nèi)存,就會(huì)用到各種 lock,一不小心就會(huì)出現(xiàn)死鎖的情況。

對(duì)于線程池的大小不好確認(rèn),在請(qǐng)求量大的時(shí)候容易導(dǎo)致 OOM 的情況

雖然線程比較輕量,但是在調(diào)度時(shí)也有比較大的額外開銷。每個(gè)線程會(huì)都占用 1 兆以上的內(nèi)存空間,在對(duì)線程進(jìn)行切換時(shí)不僅會(huì)消耗較多的內(nèi)存,恢復(fù)寄存器中的內(nèi)容還需要向操作系統(tǒng)申請(qǐng)或者銷毀對(duì)應(yīng)的資源,每一次線程上下文的切換仍然需要一定的時(shí)間(us 級(jí)別)

對(duì)于很多網(wǎng)絡(luò)服務(wù)程序,由于不能大量創(chuàng)建 thread,就要在少量 thread 里做網(wǎng)絡(luò)多路復(fù)用,例如 JAVA 的Netty 框架,寫起這樣的程序也不容易。

這便有了“協(xié)程”,線程分為內(nèi)核態(tài)線程和用戶態(tài)線程,用戶態(tài)線程需要綁定內(nèi)核態(tài)線程,CPU 并不能感知用戶態(tài)線程的存在,它只知道它在運(yùn)行1個(gè)線程,這個(gè)線程實(shí)際是內(nèi)核態(tài)線程。

用戶態(tài)線程實(shí)際有個(gè)名字叫協(xié)程(co-routine),為了容易區(qū)分,使用協(xié)程指用戶態(tài)線程,使用線程指內(nèi)核態(tài)線程。

協(xié)程跟線程是有區(qū)別的,線程由CPU調(diào)度是搶占式的,協(xié)程由用戶態(tài)調(diào)度是協(xié)作式的,一個(gè)協(xié)程讓出 CPU 后,才執(zhí)行下一個(gè)協(xié)程。

Go中,協(xié)程被稱為 goroutine(但其實(shí)并不完全是協(xié)程,還做了其他方面的優(yōu)化),它非常輕量,一個(gè) goroutine 只占幾 KB,并且這幾 KB 就足夠 goroutine 運(yùn)行完,這就能在有限的內(nèi)存空間內(nèi)支持大量 goroutine,支持了更多的并發(fā)。雖然一個(gè) goroutine 的棧只占幾 KB,但實(shí)際是可伸縮的,如果需要更多內(nèi)容,runtime會(huì)自動(dòng)為 goroutine 分配。

而將所有的 goroutines 按照一定算法放到 CPU 上執(zhí)行的程序就稱為 goroutine 調(diào)度器或 goroutine scheduler。

不過,一個(gè) Go 程序?qū)τ诓僮飨到y(tǒng)來說只是一個(gè)用戶層程序,對(duì)于操作系統(tǒng)而言,它的眼中只有 thread,它并不知道有什么叫 Goroutine 的東西的存在。goroutine 的調(diào)度全要靠 Go 自己完成,所以就需要 goroutine 調(diào)度器來實(shí)現(xiàn) Go 程序內(nèi) goroutine 之間的 CPU 資源調(diào)度。

在操作系統(tǒng)層面,Thread 競(jìng)爭(zhēng)的 CPU 資源是真實(shí)的物理 CPU,但對(duì)于 Go 程序來說,它是一個(gè)用戶層程序,它本身整體是運(yùn)行在一個(gè)或多個(gè)操作系統(tǒng)線程上的,因此 goroutine 們要競(jìng)爭(zhēng)的所謂 “CPU” 資源就是操作系統(tǒng)線程。

這樣 Go scheduler 的要做的就是:將 goroutines 按照一定算法放到不同的操作系統(tǒng)線程中去執(zhí)行。這種在語(yǔ)言層面自帶調(diào)度器的,稱之為原生支持并發(fā)。

goroutine 調(diào)度器的演進(jìn)

調(diào)度器的任務(wù)是在用戶態(tài)完成 goroutine 的調(diào)度,而調(diào)度器的實(shí)現(xiàn)好壞,對(duì)并發(fā)實(shí)際有很大的影響。

G-M模型

現(xiàn)在的 Go語(yǔ)言調(diào)度器是 2012 年重新設(shè)計(jì)的,在這之前的調(diào)度器稱為老調(diào)度器,老調(diào)度器采用的是 G-M 模型,在這個(gè)調(diào)度器中,每個(gè) goroutine 對(duì)應(yīng)于 runtime 中的一個(gè)抽象結(jié)構(gòu):G,而 os thread 作為物理 CPU 的存在而被抽象為一個(gè)結(jié)構(gòu):

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

a7de3986-4972-11ed-a3b6-dac502259ad0.png

這個(gè)結(jié)構(gòu)雖然簡(jiǎn)單,但是卻存在著許多問題。它限制了 Go 并發(fā)程序的伸縮性,尤其是對(duì)那些有高吞吐或并行計(jì)算需求的服務(wù)程序。主要體現(xiàn)在如下幾個(gè)方面:

單一全局互斥鎖(Sched.Lock)和集中狀態(tài)存儲(chǔ)的存在導(dǎo)致所有 goroutine 相關(guān)操作,比如:創(chuàng)建、重新調(diào)度等都要上鎖,這會(huì)造成激烈的鎖競(jìng)爭(zhēng)

goroutine 傳遞問題:M 經(jīng)常在 M 之間傳遞可運(yùn)行的 goroutine,這導(dǎo)致調(diào)度延遲增大以及額外的性能損耗

每個(gè) M 做內(nèi)存緩存,導(dǎo)致內(nèi)存占用過高,數(shù)據(jù)局部性較差

系統(tǒng)調(diào)用導(dǎo)致頻繁的線程阻塞和取消阻塞操作增加了系統(tǒng)開銷

所以用了 4 年左右就被替換掉了。

G-P-M 模型

面對(duì)之前調(diào)度器的問題,Go 設(shè)計(jì)了新的調(diào)度器,并在其中引入了 P(Processor),另外還引入了任務(wù)竊取調(diào)度的方式(work stealing)

P:Processor,它包含了運(yùn)行 goroutine 的資源,如果線程想運(yùn)行 goroutine,必須先獲取 P,P 中還包含了可運(yùn)行的 G 隊(duì)列。work stealing:當(dāng) M 綁定的 P 沒有可運(yùn)行的 G 時(shí),它可以從其他運(yùn)行的 M 那里偷取G。G-P-M 模型的結(jié)構(gòu)如下圖:

a80a413e-4972-11ed-a3b6-dac502259ad0.jpg

從上往下是調(diào)度器的4個(gè)部分:

全局隊(duì)列(Global Queue):存放等待運(yùn)行的 G。P 的本地隊(duì)列:同全局隊(duì)列類似,存放的也是等待運(yùn)行的G,存的數(shù)量有限,不超過256個(gè)。新建 G 時(shí),G 優(yōu)先加入到 P 的本地隊(duì)列,如果隊(duì)列滿了,則會(huì)把本地隊(duì)列中一半的 G 移動(dòng)到全局隊(duì)列。P列表:所有的 P 都在程序啟動(dòng)時(shí)創(chuàng)建,并保存在數(shù)組中,最多有 GOMAXPROCS 個(gè)。M:線程想運(yùn)行任務(wù)就得獲取 P,從 P 的本地隊(duì)列獲取 G,P 隊(duì)列為空時(shí),M 也會(huì)嘗試從全局隊(duì)列拿一批 G放到 P 的本地隊(duì)列,或從其他 P 的本地隊(duì)列偷一半放到自己 P 的本地隊(duì)列。M 運(yùn)行 G,G 執(zhí)行之后,M 會(huì)從 P 獲取下一個(gè) G,不斷重復(fù)下去。Goroutine 調(diào)度器和 OS 調(diào)度器是通過 M 結(jié)合起來的,每個(gè) M 都代表了1個(gè)內(nèi)核線程,OS 調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到 CPU 的核上執(zhí)行。

有關(guān) P 和 M 的個(gè)數(shù)問題

P 的數(shù)量

由啟動(dòng)時(shí)環(huán)境變量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS() 決定。這意味著在程序執(zhí)行的任意時(shí)刻都只有 $GOMAXPROCS 個(gè) goroutine 在同時(shí)運(yùn)行。

M 的數(shù)量

go 語(yǔ)言本身的限制:go 程序啟動(dòng)時(shí),會(huì)設(shè)置 M 的最大數(shù)量,默認(rèn) 10000。但是內(nèi)核很難支持這么多的線程數(shù),所以這個(gè)限制可以忽略。

runtime/debug 中的 SetMaxThreads 函數(shù),可以設(shè)置 M 的最大數(shù)量

一個(gè) M 阻塞了,會(huì)創(chuàng)建新的 M。

M 與 P 的數(shù)量沒有絕對(duì)關(guān)系,一個(gè) M 阻塞,P 就會(huì)去創(chuàng)建或者切換另一個(gè) M,所以,即使 P 的默認(rèn)數(shù)量是 1,也有可能會(huì)創(chuàng)建很多個(gè) M 出來。

搶占式調(diào)度

G-P-M 模型中還實(shí)現(xiàn)了搶占式調(diào)度,所謂搶占式調(diào)度指的是在 coroutine 中要等待一個(gè)協(xié)程主動(dòng)讓出 CPU 才執(zhí)行下一個(gè)協(xié)程,在 Go 中,一個(gè) goroutine 最多占用CPU 10ms,防止其他 goroutine 被餓死,這也是 goroutine 不同于 coroutine 的一個(gè)地方。在 goroutine 中先后實(shí)現(xiàn)了兩種搶占式調(diào)度算法,分別是基于協(xié)作的方式和基于信號(hào)的方式。

基于協(xié)作的搶占式調(diào)度

G-P-M 模型的實(shí)現(xiàn)是 Go scheduler 的一大進(jìn)步,但此時(shí)的調(diào)度器仍然有一個(gè)問題,那就是不支持搶占式調(diào)度,導(dǎo)致一旦某個(gè) G 中出現(xiàn)死循環(huán)或永久循環(huán)的代碼邏輯,那么 G 將永久占用分配給它的 P 和 M,位于同一個(gè) P 中的其他 G 將得不到調(diào)度,出現(xiàn)“餓死”的情況。當(dāng)只有一個(gè) P 時(shí)(GOMAXPROCS=1)時(shí),整個(gè) Go 程序中的其他 G 都會(huì)被餓死。所以后面 Go 設(shè)計(jì)團(tuán)隊(duì)在 Go 1.2 中實(shí)現(xiàn)了基于協(xié)作的搶占式調(diào)度。

這種搶占式調(diào)度的原理則是在每個(gè)函數(shù)或方法的入口,加上一段額外的代碼,讓 runtime 有機(jī)會(huì)檢查是否需要執(zhí)行搶占調(diào)度。

基于協(xié)作的搶占式調(diào)度的工作原理大致如下:

編譯器會(huì)在調(diào)用函數(shù)前插入一個(gè) runtime.morestack 函數(shù)

Go 語(yǔ)言運(yùn)行時(shí)會(huì)在垃圾回收暫停程序、系統(tǒng)監(jiān)控發(fā)現(xiàn) Goroutine 運(yùn)行超過 10ms 時(shí)發(fā)出搶占請(qǐng)求,此時(shí)會(huì)設(shè)置一個(gè) StackPreempt 字段值為 StackPreempt ,標(biāo)示當(dāng)前 Goroutine 發(fā)出了搶占請(qǐng)求。

當(dāng)發(fā)生函數(shù)調(diào)用時(shí),可能會(huì)執(zhí)行編譯器插入的 runtime.morestack 函數(shù),它調(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)單,也沒有帶來過多的額外開銷,所以在 Go 語(yǔ)言中使用了 10 幾個(gè)版本。因?yàn)檫@里的搶占是通過編譯器插入函數(shù)實(shí)現(xiàn)的,還是需要函數(shù)調(diào)用作為入口才能觸發(fā)搶占,所以這是一種協(xié)作式的搶占式調(diào)度。這種解決方案只能說局部解決了“餓死”問題,對(duì)于沒有函數(shù)調(diào)用,純算法循環(huán)計(jì)算的 G,scheduler 依然無法搶占。

基于信號(hào)的搶占式調(diào)度

Go 語(yǔ)言在 1.14 版本中實(shí)現(xiàn)了非協(xié)作的搶占式調(diào)度,在實(shí)現(xiàn)的過程中重構(gòu)已有的邏輯并為 Goroutine 增加新的狀態(tài)和字段來支持搶占。

基于信號(hào)的搶占式調(diào)度的工作原理大致如下:

程序啟動(dòng)時(shí),在runtime.sighandler函數(shù)中注冊(cè)一個(gè) SIGURG 信號(hào)的處理函數(shù)runtime.doSigPreempt

在觸發(fā)垃圾回收的棧掃描時(shí)會(huì)調(diào)用函數(shù) runtime.suspendG 掛起 Goroutine,此時(shí)會(huì)執(zhí)行下面的邏輯:

將處于運(yùn)行狀態(tài)(_Grunning)的 Goroutine 標(biāo)記成可以被搶占,即將 Goroutine 的字段 preemptStop 設(shè)置成 true;

調(diào)用 runtime.preemptM函數(shù), 它可以通過 SIGURG 信號(hào)向線程發(fā)送搶占請(qǐng)求觸發(fā)搶占;

runtime.preemptM 會(huì)調(diào)用 runtime.signalM 向線程發(fā)送信號(hào) SIGURG;

操作系統(tǒng)收到信號(hào)后會(huì)中斷正在運(yùn)行的線程并執(zhí)行預(yù)先在第 1 步注冊(cè)的信號(hào)處理函數(shù) runtime.doSigPreempt;

runtime.doSigPreempt 函數(shù)會(huì)處理?yè)屨夹盘?hào),獲取當(dāng)前的 SP 和 PC 寄存器并調(diào)用 runtime.sigctxt.pushCall;

runtime.sigctxt.pushCall 會(huì)修改寄存器并在程序回到用戶態(tài)時(shí)執(zhí)行 runtime.asyncPreempt;

匯編指令 runtime.asyncPreempt 會(huì)調(diào)用運(yùn)行時(shí)函數(shù) runtime.asyncPreempt2;

runtime.asyncPreempt2 會(huì)調(diào)用 runtime.preemptPark;

runtime.preemptPark會(huì)修改當(dāng)前 Goroutine 的狀態(tài)到_Gpreempted并調(diào)用runtime.schedule讓當(dāng)前函數(shù)陷入休眠并讓出線程,調(diào)度器會(huì)選擇其它的 Goroutine 繼續(xù)執(zhí)行

_Gpreempted狀態(tài)表示當(dāng)前 groutine 由于搶占而被阻塞,沒有執(zhí)行用戶代碼并且不在運(yùn)行隊(duì)列上,等待喚醒

在上面的選擇 SIGURG 作為觸發(fā)異步搶占的信號(hào):

該信號(hào)需要被調(diào)試器透?jìng)鳎?/p>

該信號(hào)不會(huì)被內(nèi)部的 libc 庫(kù)使用并攔截;

該信號(hào)可以隨意出現(xiàn)并且不觸發(fā)任何后果;

需要處理多個(gè)平臺(tái)上的不同信號(hào);

垃圾回收過程中需要暫停整個(gè)程序(Stop the world,STW),有時(shí)候可能需要幾分鐘的時(shí)間,這會(huì)導(dǎo)致整個(gè)程序無法工作。所以 STW 和棧掃描是一個(gè)可以搶占的安全點(diǎn)(Safe-points), Go 語(yǔ)言在這里先加入搶占功能?;谛盘?hào)的搶占式調(diào)度只解決了垃圾回收和棧掃描時(shí)存在的問題,它到目前為止沒有解決全部問題。

go func() 調(diào)度流程

基于上面的模型,當(dāng)我們使用 go func()創(chuàng)建一個(gè)新的 goroutine 的時(shí)候,其調(diào)度流程如下:

a8658076-4972-11ed-a3b6-dac502259ad0.jpg

通過 go func ()來創(chuàng)建一個(gè) goroutine;

有兩個(gè)存儲(chǔ) G 的隊(duì)列,一個(gè)是局部調(diào)度器 P 的本地隊(duì)列、一個(gè)是全局 G 隊(duì)列。新創(chuàng)建的 G 會(huì)先保存在 P 的本地隊(duì)列中,如果 P 的本地隊(duì)列已經(jīng)滿了就會(huì)保存在全局的隊(duì)列中;

G 只能運(yùn)行在 M 中,一個(gè) M 必須持有一個(gè) P,M 與 P 是 1:1 的關(guān)系。M 會(huì)從 P 的本地隊(duì)列彈出一個(gè)可執(zhí)行狀態(tài)的 G 來執(zhí)行,如果 P 的本地隊(duì)列為空,就會(huì)想其他的 MP 組合偷取一個(gè)可執(zhí)行的 G 來執(zhí)行;

一個(gè) M 調(diào)度 G 執(zhí)行的過程是一個(gè)循環(huán)機(jī)制;

當(dāng) M 執(zhí)行某一個(gè) G 時(shí)候如果發(fā)生了 syscall 或則其余阻塞操作,M 會(huì)阻塞,如果當(dāng)前有一些 G 在執(zhí)行,runtime 會(huì)把這個(gè)線程 M 從 P 中摘除 (detach),然后再創(chuàng)建一個(gè)新的操作系統(tǒng)的線程 (如果有空閑的線程可用就復(fù)用空閑線程) 來服務(wù)于這個(gè) P;

當(dāng) M 系統(tǒng)調(diào)用結(jié)束時(shí)候,這個(gè) G 會(huì)嘗試獲取一個(gè)空閑的 P 執(zhí)行,并放入到這個(gè) P 的本地隊(duì)列。如果獲取不到 P,那么這個(gè)線程 M 變成休眠狀態(tài), 加入到空閑線程中,然后這個(gè) G 會(huì)被放入全局隊(duì)列中。

Goroutine 生命周期

a884505a-4972-11ed-a3b6-dac502259ad0.png

在這里有一個(gè)線程和一個(gè) groutine 比較特殊,那就是 M0 和 G0:

M0:M0 是啟動(dòng)程序后的編號(hào)為 0 的主線程,這個(gè) M 對(duì)應(yīng)的實(shí)例會(huì)在全局變量 runtime.m0 中,不需要在 heap 上分配,M0 負(fù)責(zé)執(zhí)行初始化操作和啟動(dòng)第一個(gè) G, 在之后 M0 就和其他的 M 一樣了。

G0 :G0 是每次啟動(dòng)一個(gè) M 都會(huì)第一個(gè)創(chuàng)建的 gourtine,G0 僅用于負(fù)責(zé)調(diào)度的 G,G0 不指向任何可執(zhí)行的函數(shù),每個(gè) M 都會(huì)有一個(gè)自己的 G0。在調(diào)度或系統(tǒng)調(diào)用時(shí)會(huì)使用 G0 的??臻g,全局變量的 G0 是 M0 的 G0。

對(duì)于下面的簡(jiǎn)單代碼:

package main


import "fmt"


// main.main
func main() {
   fmt.Println("Hello scheduler")
}

其運(yùn)行時(shí)所經(jīng)歷的過程跟上面的生命周期相對(duì)應(yīng):

runtime 創(chuàng)建最初的線程 m0 和 goroutine g0,并把 2 者關(guān)聯(lián)。

調(diào)度器初始化:初始化 m0、棧、垃圾回收,以及創(chuàng)建和初始化由 GOMAXPROCS 個(gè) P 構(gòu)成的 P 列表。

示例代碼中的 main 函數(shù)是 main.main,runtime 中也有 1 個(gè) main 函數(shù)——runtime.main,代碼經(jīng)過編譯后,runtime.main會(huì)調(diào)用 main.main,程序啟動(dòng)時(shí)會(huì)為 runtime.main 創(chuàng)建 goroutine,稱它為main goroutine,然后把 main goroutine 加入到P的本地隊(duì)列。

啟動(dòng) m0,m0 已經(jīng)綁定了 P,會(huì)從 P 的本地隊(duì)列獲取 G,獲取到 main goroutine。

G 擁有棧,M 根據(jù) G 中的棧信息和調(diào)度信息設(shè)置運(yùn)行環(huán)境

M 運(yùn)行 G

G 退出,再次回到 M 獲取可運(yùn)行的 G,這樣重復(fù)下去,直到 main.main 退出,runtime.main執(zhí)行 Defer 和 Panic 處理,或調(diào)用 runtime.exit 退出程序。

調(diào)度器的生命周期幾乎占滿了一個(gè)Go程序的一生,runtime.main 的 goroutine 執(zhí)行之前都是為調(diào)度器做準(zhǔn)備工作,runtime.main 的 goroutine 運(yùn)行,才是調(diào)度器的真正開始,直到 runtime.main 結(jié)束而結(jié)束。

Goroutine 調(diào)度器場(chǎng)景分析

場(chǎng)景一

p1 擁有 g1,m1 獲取 p1 后開始運(yùn)行g(shù)1,g1 使用 go func() 創(chuàng)建了 g2,為了局部性 g2 優(yōu)先加入到 p1 的本地隊(duì)列:

a8a9fe68-4972-11ed-a3b6-dac502259ad0.png

場(chǎng)景二

g1運(yùn)行完成后(函數(shù):goexit),m 上運(yùn)行的 goroutine 切換為 g0,g0 負(fù)責(zé)調(diào)度時(shí)協(xié)程的切換(函數(shù):schedule)。

從 p1 的本地隊(duì)列取 g2,從 g0 切換到 g2,并開始運(yùn)行 g2 (函數(shù):execute)。實(shí)現(xiàn)了線程 m1 的復(fù)用。

a8d05ebe-4972-11ed-a3b6-dac502259ad0.png

場(chǎng)景三

假設(shè)每個(gè) p 的本地隊(duì)列只能存 4 個(gè) g。g2 要?jiǎng)?chuàng)建 6 個(gè) g,前 4 個(gè)g(g3, g4, g5, g6)已經(jīng)加入 p1 的本地隊(duì)列,p1 本地隊(duì)列滿了。

g2 在創(chuàng)建 g7 的時(shí)候,發(fā)現(xiàn) p1 的本地隊(duì)列已滿,需要執(zhí)行負(fù)載均衡,把 p1 中本地隊(duì)列中前一半的 g,還有新創(chuàng)建的 g 轉(zhuǎn)移到全局隊(duì)列

實(shí)現(xiàn)中并不一定是新的 g,如果 g 是 g2 之后就執(zhí)行的,會(huì)被保存在本地隊(duì)列,利用某個(gè)老的 g 替換新 g 加入全局隊(duì)列),這些 g 被轉(zhuǎn)移到全局隊(duì)列時(shí),會(huì)被打亂順序。

所以 g3,g4,g7 被轉(zhuǎn)移到全局隊(duì)列。

a9120292-4972-11ed-a3b6-dac502259ad0.png

藍(lán)色長(zhǎng)方形代表全局隊(duì)列。

如果此時(shí) G2 創(chuàng)建 G8 時(shí),P1 的本地隊(duì)列未滿,所以 G8 會(huì)被加入到 P1 的本地隊(duì)列:

a947ba22-4972-11ed-a3b6-dac502259ad0.png

場(chǎng)景四

在創(chuàng)建 g 時(shí),運(yùn)行的 g 會(huì)嘗試喚醒其他空閑的 p 和 m 組合去執(zhí)行。假定 g2 喚醒了 m2,m2 綁定了 p2,并運(yùn)行 g0,但 p2 本地隊(duì)列沒有 g,m2 此時(shí)為自旋線程(沒有 G 但為運(yùn)行狀態(tài)的線程,不斷尋找 g)。

a9696fc8-4972-11ed-a3b6-dac502259ad0.png

m2 接下來會(huì)嘗試從全局隊(duì)列 (GQ) 取一批 g 放到 p2 的本地隊(duì)列(函數(shù):findrunnable)。m2 從全局隊(duì)列取的 g 數(shù)量符合下面的公式:

n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))

公式的含義是,至少?gòu)娜株?duì)列取 1 個(gè) g,但每次不要從全局隊(duì)列移動(dòng)太多的 g 到 p 本地隊(duì)列,給其他 p 留點(diǎn)。這是從全局隊(duì)列到 P 本地隊(duì)列的負(fù)載均衡。

假定場(chǎng)景中一共有 4 個(gè) P(GOMAXPROCS=4),所以 m2 只從能從全局隊(duì)列取 1 個(gè) g(即 g3)移動(dòng) p2 本地隊(duì)列,然后完成從 g0 到 g3 的切換,運(yùn)行 g3:

a97dc752-4972-11ed-a3b6-dac502259ad0.png

場(chǎng)景五

假設(shè) g2 一直在 m1上運(yùn)行,經(jīng)過 2 輪后,m2 已經(jīng)把 g7、g4 也挪到了p2的本地隊(duì)列并完成運(yùn)行,全局隊(duì)列和 p2 的本地隊(duì)列都空了,如下圖左邊所示。

全局隊(duì)列已經(jīng)沒有 g,那 m 就要執(zhí)行 work stealing:從其他有 g 的 p 哪里偷取一半 g 過來,放到自己的 P 本地隊(duì)列。p2 從 p1 的本地隊(duì)列尾部取一半的 g,本例中一半則只有 1 個(gè) g8,放到 p2 的本地隊(duì)列,情況如下圖右邊:

場(chǎng)景六

p1 本地隊(duì)列 g5、g6 已經(jīng)被其他 m 偷走并運(yùn)行完成,當(dāng)前 m1 和 m2 分別在運(yùn)行 g2 和 g8,m3 和 m4 沒有g(shù)oroutine 可以運(yùn)行,m3 和 m4 處于自旋狀態(tài),它們不斷尋找 goroutine。

這里有一個(gè)問題,為什么要讓 m3 和 m4 自旋?自旋本質(zhì)是在運(yùn)行,線程在運(yùn)行卻沒有執(zhí)行 g,就變成了浪費(fèi)CPU,銷毀線程可以節(jié)約CPU資源不是更好嗎?實(shí)際上,創(chuàng)建和銷毀CPU都是浪費(fèi)時(shí)間的,我們希望當(dāng)有新 goroutine 創(chuàng)建時(shí),立刻能有 m 運(yùn)行它,如果銷毀再新建就增加了時(shí)延,降低了效率。當(dāng)然也考慮了過多的自旋線程是浪費(fèi) CPU,所以系統(tǒng)中最多有 GOMAXPROCS 個(gè)自旋的線程,多余的沒事做的線程會(huì)讓他們休眠(函數(shù):notesleep() 實(shí)現(xiàn)了這個(gè)思路)。

場(chǎng)景七

假定當(dāng)前除了 m3 和 m4 為自旋線程,還有 m5 和 m6 為自旋線程,g8 創(chuàng)建了 g9,g9 會(huì)放入本地隊(duì)列。加入此時(shí)g8 進(jìn)行了阻塞的系統(tǒng)調(diào)用,m2 和 p2 立即解綁,p2 會(huì)執(zhí)行以下判斷:如果 p2 本地隊(duì)列有 g、全局隊(duì)列有 g 或有空閑的 m,p2 都會(huì)立馬喚醒1個(gè) m 和它綁定,否則 p2 則會(huì)加入到空閑 P 列表,等待 m 來獲取可用的 p。本場(chǎng)景中,p2 本地隊(duì)列有 g,可以和其他自旋線程 m5 綁定。

a9901c72-4972-11ed-a3b6-dac502259ad0.png

場(chǎng)景八

假設(shè) g8 創(chuàng)建了 g9,假如 g8 進(jìn)行了非阻塞系統(tǒng)調(diào)用(CGO會(huì)是這種方式,見cgocall()),m2 和 p2 會(huì)解綁,但 m2 會(huì)記住 p,然后 g8 和 m2 進(jìn)入系統(tǒng)調(diào)用狀態(tài)。當(dāng) g8 和 m2 退出系統(tǒng)調(diào)用時(shí),會(huì)嘗試獲取 p2,如果無法獲取,則獲取空閑的 p,如果依然沒有,g8 會(huì)被記為可運(yùn)行狀態(tài),并加入到全局隊(duì)列。

a9a873e4-4972-11ed-a3b6-dac502259ad0.png

場(chǎng)景九

前面說過,Go 調(diào)度在 go1.12 實(shí)現(xiàn)了搶占,應(yīng)該更精確的稱為基于協(xié)作的請(qǐng)求式搶占,那是因?yàn)?go 調(diào)度器的搶占和 OS 的線程搶占比起來很柔和,不暴力,不會(huì)說線程時(shí)間片到了,或者更高優(yōu)先級(jí)的任務(wù)到了,執(zhí)行搶占調(diào)度。go 的搶占調(diào)度柔和到只給 goroutine 發(fā)送 1 個(gè)搶占請(qǐng)求,至于 goroutine 何時(shí)停下來,那就管不到了。搶占請(qǐng)求需要滿足2個(gè)條件中的1個(gè):

G 進(jìn)行系統(tǒng)調(diào)用超過 20us

G 運(yùn)行超過 10ms。調(diào)度器在啟動(dòng)的時(shí)候會(huì)啟動(dòng)一個(gè)單獨(dú)的線程 sysmon,它負(fù)責(zé)所有的監(jiān)控工作,其中 1 項(xiàng)就是搶占,發(fā)現(xiàn)滿足搶占條件的 G 時(shí),就發(fā)出搶占請(qǐng)求。

狀態(tài)匯總

從上面的場(chǎng)景中可以總結(jié)各個(gè)模型的狀態(tài):

G狀態(tài)

G的主要幾種狀態(tài):

狀態(tài) 描述
_Gidle 剛剛被分配并且還沒有被初始化,值為0,為創(chuàng)建goroutine后的默認(rèn)值
_Grunnable 沒有執(zhí)行代碼,沒有棧的所有權(quán),存儲(chǔ)在運(yùn)行隊(duì)列中,可能在某個(gè)P的本地隊(duì)列或全局隊(duì)列中
_Grunning 正在執(zhí)行代碼的goroutine,擁有棧的所有權(quán)
_Gsyscall 正在執(zhí)行系統(tǒng)調(diào)用,擁有棧的所有權(quán),沒有執(zhí)行用戶代碼,被賦予了內(nèi)核線程 M 但是不在運(yùn)行隊(duì)列上
_Gwaiting 由于運(yùn)行時(shí)而被阻塞,沒有執(zhí)行用戶代碼并且不在運(yùn)行隊(duì)列上,但是可能存在于 Channel 的等待隊(duì)列上
_Gdead 當(dāng)前goroutine未被使用,沒有執(zhí)行代碼,可能有分配的棧,分布在空閑列表 gFree,可能是一個(gè)剛剛初始化的 goroutine,也可能是執(zhí)行了 goexit 退出的 goroutine
_Gcopystack 棧正在被拷貝,沒有執(zhí)行代碼,不在運(yùn)行隊(duì)列上
_Gpreempted 由于搶占而被阻塞,沒有執(zhí)行用戶代碼并且不在運(yùn)行隊(duì)列上,等待喚醒
_Gscan GC 正在掃描??臻g,沒有執(zhí)行代碼,可以與其他狀態(tài)同時(shí)存在

P 狀態(tài)

狀態(tài) 描述
_Pidle 處理器沒有運(yùn)行用戶代碼或者調(diào)度器,被空閑隊(duì)列或者改變其狀態(tài)的結(jié)構(gòu)持有,運(yùn)行隊(duì)列為空
_Prunning 被線程 M 持有,并且正在執(zhí)行用戶代碼或者調(diào)度器
_Psyscall 沒有執(zhí)行用戶代碼,當(dāng)前線程陷入系統(tǒng)調(diào)用
_Pgcstop 被線程 M 持有,當(dāng)前處理器由于垃圾回收被停止
_Pdead 當(dāng)前處理器已經(jīng)不被使用

M 狀態(tài)

自旋線程:處于運(yùn)行狀態(tài)但是沒有可執(zhí)行 goroutine 的線程,數(shù)量最多為 GOMAXPROC,若是數(shù)量大于 GOMAXPROC 就會(huì)進(jìn)入休眠。

非自旋線程:處于運(yùn)行狀態(tài)有可執(zhí)行 goroutine 的線程。

調(diào)度器設(shè)計(jì)

從上面的流程可以總結(jié)出 goroutine 調(diào)度器的一些設(shè)計(jì)思路:

調(diào)度器設(shè)計(jì)的兩大思想

復(fù)用線程:協(xié)程本身就是運(yùn)行在一組線程之上,所以不需要頻繁的創(chuàng)建、銷毀線程,而是對(duì)線程進(jìn)行復(fù)用。在調(diào)度器中復(fù)用線程還有2個(gè)體現(xiàn):

work stealing,當(dāng)本線程無可運(yùn)行的 G 時(shí),嘗試從其他線程綁定的 P 偷取 G,而不是銷毀線程。

hand off,當(dāng)本線程因?yàn)?G 進(jìn)行系統(tǒng)調(diào)用阻塞時(shí),線程釋放綁定的 P,把 P 轉(zhuǎn)移給其他空閑的線程執(zhí)行。

利用并行:GOMAXPROCS 設(shè)置 P 的數(shù)量,當(dāng) GOMAXPROCS 大于 1 時(shí),就最多有 GOMAXPROCS 個(gè)線程處于運(yùn)行狀態(tài),這些線程可能分布在多個(gè) CPU 核上同時(shí)運(yùn)行,使得并發(fā)利用并行。另外,GOMAXPROCS 也限制了并發(fā)的程度,比如 GOMAXPROCS = 核數(shù)/2,則最多利用了一半的 CPU 核進(jìn)行并行。

調(diào)度器設(shè)計(jì)的兩小策略

搶占:

在 coroutine 中要等待一個(gè)協(xié)程主動(dòng)讓出 CPU 才執(zhí)行下一個(gè)協(xié)程,在 Go 中,一個(gè) goroutine 最多占用CPU 10ms,防止其他 goroutine 被餓死,這就是 goroutine 不同于 coroutine 的一個(gè)地方。

全局G隊(duì)列:

在新的調(diào)度器中依然有全局 G 隊(duì)列,但功能已經(jīng)被弱化了,當(dāng) M 執(zhí)行 work stealing 從其他 P 偷不到 G 時(shí),它可以從全局 G 隊(duì)列獲取 G。

GPM 可視化調(diào)試

有 2 種方式可以查看一個(gè)程序 GPM 的數(shù)據(jù):

go tool trace

trace 記錄了運(yùn)行時(shí)的信息,能提供可視化的Web頁(yè)面。

簡(jiǎn)單測(cè)試代碼:main 函數(shù)創(chuàng)建 trace,trace 會(huì)運(yùn)行在單獨(dú)的 goroutine 中,然后 main 打印 “Hello trace” 退出。

func main() {
    // 創(chuàng)建trace文件
    f, err := os.Create("trace.out")
    if err != nil {
        panic(err)
    }
    defer f.Close()


    // 啟動(dòng)trace goroutine
    err = trace.Start(f)
    if err != nil {
        panic(err)
    }
    defer trace.Stop()


    // main
    fmt.Println("Hello trace")
}

運(yùn)行程序和運(yùn)行trace:

$ go run trace.go 
Hello World

會(huì)得到一個(gè) trace.out 文件,然后可以用一個(gè)工具打開,來分析這個(gè)文件:


$ go tool trace trace.out 
2020/12/07 23:09:33 Parsing trace...
2020/12/07 23:09:33 Splitting trace...
2020/12/0723:09:33Openingbrowser.Traceviewerislisteningonhttp://127.0.0.1:56469

接下來通過瀏覽器打開 http://127.0.0.1:33479 網(wǎng)址,點(diǎn)擊 view trace 能夠
看見可視化的調(diào)度流程:

aa9a8b70-4972-11ed-a3b6-dac502259ad0.jpg

aab29184-4972-11ed-a3b6-dac502259ad0.png

g 信息

點(diǎn)擊 Goroutines 那一行的數(shù)據(jù)條,會(huì)看到一些詳細(xì)的信息:

aae11d6a-4972-11ed-a3b6-dac502259ad0.jpg

上面表示一共有兩個(gè) G 在程序中,一個(gè)是特殊的 G0,是每個(gè) M 必須有的一個(gè)初始化的 G。其中 G1 就是 main goroutine (執(zhí)行 main 函數(shù)的協(xié)程),在一段時(shí)間內(nèi)處于可運(yùn)行和運(yùn)行的狀態(tài)。

m 信息

點(diǎn)擊 Threads 那一行可視化的數(shù)據(jù)條,會(huì)看到一些詳細(xì)的信息:

ab15ac6a-4972-11ed-a3b6-dac502259ad0.jpg

這里一共有兩個(gè) M 在程序中,一個(gè)是特殊的 M0,用于初始化使用。

p 信息

ab3b7832-4972-11ed-a3b6-dac502259ad0.jpg

G1 中調(diào)用了 main.main,創(chuàng)建了 trace goroutine g6。G1 運(yùn)行在 P0 上,G6運(yùn)行在 P1 上。

這里有三個(gè) P。

在看看上面的 M 信息:

ab54a064-4972-11ed-a3b6-dac502259ad0.jpg

可以看到確實(shí) G6 在 P1 上被運(yùn)行之前,確實(shí)在 Threads 行多了一個(gè) M 的數(shù)據(jù),點(diǎn)擊查看如下:

ab735b9e-4972-11ed-a3b6-dac502259ad0.jpg

多了一個(gè) M2 應(yīng)該就是 P1 為了執(zhí)行 G6 而動(dòng)態(tài)創(chuàng)建的 M2。

Debug trace

示例代碼:

// main.main
func main() {
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second)
        fmt.Println("Hello scheduler")
    }
}

編譯后通過 Debug 方式運(yùn)行,運(yùn)行過程會(huì)打印trace:

? go build .
? GODEBUG=schedtrace=1000 ./one_routine2

結(jié)果:

SCHED 0ms: gomaxprocs=4 idleprocs=2 threads=3 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0]
Hello scheduler
SCHED 1003ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello scheduler
SCHED 2007ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello scheduler
SCHED 3010ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello scheduler
SCHED 4013ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello scheduler

各個(gè)字段的含義如下:

SCHED:調(diào)試信息輸出標(biāo)志字符串,代表本行是 goroutine 調(diào)度器的輸出;

0ms:即從程序啟動(dòng)到輸出這行日志的時(shí)間;

gomaxprocs: P的數(shù)量,本例有 4 個(gè)P;

idleprocs: 處于 idle (空閑)狀態(tài)的 P 的數(shù)量;通過 gomaxprocs 和 idleprocs 的差值,就可以知道執(zhí)行 go 代碼的 P 的數(shù)量;

threads: os threads/M 的數(shù)量,包含 scheduler 使用的 m 數(shù)量,加上 runtime 自用的類似 sysmon 這樣的 thread 的數(shù)量;

spinningthreads: 處于自旋狀態(tài)的 os thread 數(shù)量;

idlethread: 處于 idle 狀態(tài)的 os thread 的數(shù)量;

runqueue=0:Scheduler 全局隊(duì)列中 G 的數(shù)量;[0 0 0 0]: 分別為 4 個(gè) P 的 local queue 中的 G 的數(shù)量。

看第一行,含義是:剛啟動(dòng)時(shí)創(chuàng)建了 4 個(gè)P,其中 2 個(gè)空閑的 P,共創(chuàng)建 3 個(gè)M,其中 1 個(gè) M 處于自旋,沒有 M 處于空閑,第一個(gè) P 的本地隊(duì)列有一個(gè) G。

另外,可以加上 scheddetail=1 可以打印更詳細(xì)的 trace 信息。

命令:

? GODEBUG=schedtrace=1000,scheddetail=1 ./one_routine2

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11207

    瀏覽量

    208713
  • 調(diào)度器
    +關(guān)注

    關(guān)注

    0

    文章

    98

    瀏覽量

    5232

原文標(biāo)題:goroutine 調(diào)度器原理

文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    中斷、切換、調(diào)度概念關(guān)系不太明白

    切換”、“禁止中斷級(jí)切換”、“禁止任務(wù)調(diào)度”這幾個(gè)概念的對(duì)應(yīng)關(guān)系。2.任務(wù)級(jí)切換是不是需要用到軟中斷?希望知道的能解答一下。或者您有更系統(tǒng)的文章,可以分享下。
    發(fā)表于 04-28 09:56

    多頻超寬頻天線場(chǎng)景應(yīng)用

    時(shí)代天線部署難題,多頻超寬頻天線成為運(yùn)營(yíng)商的最佳選擇。多頻超寬頻天線,滿足TDD/FDD各種場(chǎng)景的混合組網(wǎng)方式,一根天線支持多個(gè)頻段,解決天面空間問題,同時(shí)預(yù)留可能增加的頻段,滿足未來網(wǎng)絡(luò)演進(jìn),有效保護(hù)
    發(fā)表于 06-12 07:22

    【HarmonyOS】鴻蒙內(nèi)核源碼分析(調(diào)度機(jī)制篇)

    源于生活,歸于生活,大家對(duì)程序的理解就是要用生活中的場(chǎng)景去打比方,更好的理解概念。那在內(nèi)核的調(diào)度層面,咱們就說task, task是內(nèi)核調(diào)度的單元,
    發(fā)表于 10-14 14:00

    鴻蒙內(nèi)核源碼分析(調(diào)度機(jī)制篇):Task是如何被調(diào)度執(zhí)行的

    本文分析任務(wù)調(diào)度機(jī)制源碼 詳見:代碼庫(kù)建議先閱讀閱讀之前建議先讀本系列其他文章,進(jìn)入鴻蒙系統(tǒng)源碼分析(總目錄),以便對(duì)本文任務(wù)調(diào)度機(jī)制的理解。為什么學(xué)一個(gè)東西要學(xué)那么多的
    發(fā)表于 11-23 10:53

    Linux2.4和Linux2.6的調(diào)度對(duì)比分析,Linux2.6對(duì)調(diào)度的改進(jìn)有哪些方面?

    Linux2.4和Linux2.6的調(diào)度對(duì)比分析,Linux2.6對(duì)調(diào)度的改進(jìn)有哪些方面?Linux2.4
    發(fā)表于 04-27 06:42

    編譯優(yōu)化的靜態(tài)調(diào)度介紹

    ,使用物理寄存替換虛擬寄存,由于物理寄存數(shù)量有限,寄存壓力增大,可能產(chǎn)生寄存spill場(chǎng)景
    發(fā)表于 03-17 17:07

    VxWorks實(shí)時(shí)內(nèi)核調(diào)度的研究分析

    VxWorks實(shí)時(shí)內(nèi)核調(diào)度的研究分析論述了0S中調(diào)度概念、類型、調(diào)度隊(duì)列模型,并著重對(duì)VxWorks實(shí)時(shí)內(nèi)核進(jìn)行了
    發(fā)表于 12-16 14:07 ?13次下載

    Vx Works實(shí)時(shí)內(nèi)核調(diào)度的研究分析

    論述了OS 中調(diào)度概念、類型、調(diào)度隊(duì)列模型,并著重對(duì)VxWorks 實(shí)時(shí)內(nèi)核進(jìn)行了分析。關(guān)鍵詞:嵌入式實(shí)時(shí)操作系統(tǒng)(RTOS) ;VxWorks ;
    發(fā)表于 03-25 10:36 ?33次下載

    Linux與VxWorks任務(wù)調(diào)度機(jī)制分析

    Linux與VxWorks任務(wù)調(diào)度機(jī)制分析
    發(fā)表于 03-28 09:52 ?19次下載

    VxWorks實(shí)時(shí)內(nèi)核調(diào)度的研究分析

    論述了0S中調(diào)度概念、類型、調(diào)度隊(duì)列模型,并著重對(duì)VxWorks實(shí)時(shí)內(nèi)核進(jìn)行了分析。
    發(fā)表于 11-27 16:22 ?16次下載

    CAN調(diào)度理論與實(shí)踐分析

    CAN調(diào)度理論與實(shí)踐分析 CAN總線中消息能否按時(shí)送達(dá)是事關(guān)系統(tǒng)安全等問題的重要指標(biāo),它要通過調(diào)度分析加以驗(yàn)證。本文介紹CAN
    發(fā)表于 03-29 15:11 ?692次閱讀
    CAN<b class='flag-5'>調(diào)度</b>理論與實(shí)踐<b class='flag-5'>分析</b>

    uClinux進(jìn)程調(diào)度的實(shí)現(xiàn)分析

    分享到:標(biāo)簽:uClinux 調(diào)度策略 進(jìn)程調(diào)度 摘要:針對(duì)操作系統(tǒng)中進(jìn)程的調(diào)度機(jī)制,依次對(duì)其調(diào)度方式、
    發(fā)表于 11-06 14:30 ?0次下載

    基于PLSA模型的群體情緒演進(jìn)分析

    針對(duì)群體情緒演進(jìn)分析中話題內(nèi)容挖掘及其對(duì)應(yīng)群體情緒分析兩個(gè)層面的難題,提出了一種基于概率潛在語(yǔ)義分析(PLSA)模型的群體情緒演進(jìn)
    發(fā)表于 12-30 17:16 ?0次下載
    基于PLSA模型的群體情緒<b class='flag-5'>演進(jìn)</b><b class='flag-5'>分析</b>

    基于形式概念分析的圖像場(chǎng)景語(yǔ)義標(biāo)注模型

    為生成有效表示圖像場(chǎng)景語(yǔ)義的視覺詞典,提高場(chǎng)景語(yǔ)義標(biāo)注性能,提出一種基于形式概念分析( FCA)的圖像場(chǎng)景語(yǔ)義標(biāo)注模型。該方法首先將訓(xùn)練圖像
    發(fā)表于 01-12 15:49 ?1次下載
    基于形式<b class='flag-5'>概念</b><b class='flag-5'>分析</b>的圖像<b class='flag-5'>場(chǎng)景</b>語(yǔ)義標(biāo)注模型

    Linux進(jìn)程調(diào)度時(shí)機(jī)概念分析

    Linux在眾多進(jìn)程中是怎么進(jìn)行調(diào)度的,這個(gè)牽涉到Linux進(jìn)程調(diào)度時(shí)機(jī)的概念,由Linux內(nèi)核中Schedule()的函數(shù)來決定是否要進(jìn)行進(jìn)程的切換,如果要切換的話,切換到哪個(gè)進(jìn)程等等。
    的頭像 發(fā)表于 01-23 17:14 ?2747次閱讀
    Linux進(jìn)程<b class='flag-5'>調(diào)度</b>時(shí)機(jī)<b class='flag-5'>概念</b><b class='flag-5'>分析</b>