Go 的并發(fā)性與調(diào)度器

本篇文章是我對(duì) Go 語(yǔ)言并發(fā)性的理解總結(jié),適合初步了解并發(fā)疯兼,對(duì) Go 語(yǔ)言的并發(fā)編程與調(diào)度器原理有興趣的讀者。

你真的了解并發(fā)嗎贫途?

相信讀者都對(duì)并發(fā)有著一定的理解吧彪,也都對(duì) Go 語(yǔ)言感興趣,Go 最吸引人的地方可能就是它的內(nèi)建并發(fā)支持丢早,使用 go 關(guān)鍵字姨裸,就可以輕松的實(shí)現(xiàn)并發(fā)。但是怨酝,你真正的了解并發(fā)嗎傀缩?

并發(fā)這個(gè)詞,你去問編程領(lǐng)域中不同的人农猬,會(huì)給出不同的答案赡艰。對(duì)于 WEB 領(lǐng)域的開發(fā)人員來說,并發(fā)通常是指同一時(shí)刻的請(qǐng)求量盛险,WEB 領(lǐng)域的面試官經(jīng)常會(huì)問到的問題:“做過多少并發(fā)的項(xiàng)目瞄摊?”或“接觸過高并發(fā)的項(xiàng)目嗎?”就是應(yīng)用的這個(gè)概念苦掘。高并發(fā)在這里還有個(gè)可能的概念是:同時(shí)應(yīng)對(duì)許多請(qǐng)求所使用的技術(shù)换帜,這通常與分布式、并行等概念掛鉤鹤啡,需要結(jié)合上下文語(yǔ)境來判斷惯驼。

并發(fā)是一個(gè)有趣的詞,因?yàn)樗鼘?duì)編程領(lǐng)域中的不同人員意味著不同的事情递瑰,在廣義概念下祟牲,有著許多狹義概念。除了“并發(fā)”之外抖部,你可能還聽過“并行”说贝、“多線程”、“異步”等詞匯慎颗,有些人認(rèn)為這些詞意思相同乡恕,而其他人則在每個(gè)詞之間劃清界限言询。

下面,讓我們看看 Go 語(yǔ)言編程中傲宜,“并發(fā)”這個(gè)詞的概念运杭。

Go 語(yǔ)言中的并發(fā)性

Go 語(yǔ)言的并發(fā)性并不是 WEB 領(lǐng)域的并發(fā)概念,很多人對(duì)此有所混淆函卒。在 Go 語(yǔ)言發(fā)布之初辆憔,大家對(duì) Go 的并發(fā)特性都有所疑問:

  • 為什么要有并發(fā)?
  • 什么是并發(fā)报嵌?
  • 這個(gè)想法源自哪里虱咧?
  • 并發(fā)有什么好處?
  • 我該如何使用它沪蓬?

面對(duì)這些問題彤钟,Rob Pike(Go 語(yǔ)言作者之一)在2012年的 Google I/O 上做了一次精彩的演講:《Go Concurrency Patterns》来候,在這場(chǎng)演講中跷叉,他回答了上述問題,并通過詳細(xì)的示例講解了 goroutine营搅、channel 與 select 的使用云挟,建議大家都去看一看這場(chǎng)演講。

簡(jiǎn)單的總結(jié)一下并發(fā)在 Go 語(yǔ)言編程中的概念:

“并發(fā)是一種將程序分解成小片段獨(dú)立執(zhí)行的程度設(shè)計(jì)方法”转质,它是一種結(jié)構(gòu)化程序的方式园欣,獨(dú)立執(zhí)行計(jì)算的組合。

在上述的演講中可以看出休蟹,Go 語(yǔ)言推薦使用并發(fā)沸枯,我們也應(yīng)該遵循這種編程方式。對(duì)于程序員來說赂弓,代碼更有說服力绑榴,我們可以通過這個(gè)素?cái)?shù)篩選程序來理解 Go 的并發(fā)編程:

// A concurrent prime sieve

package main

// Send the sequence 2, 3, 4, ... to channel 'ch'.
func Generate(ch chan<- int) {
    for i := 2; ; i++ {
        ch <- i // Send 'i' to channel 'ch'.
    }
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func Filter(in <-chan int, out chan<- int, prime int) {
    for {
        i := <-in // Receive value from 'in'.
        if i%prime != 0 {
            out <- i // Send 'i' to 'out'.
        }
    }
}

// The prime sieve: Daisy-chain Filter processes.
func main() {
    ch := make(chan int) // Create a new channel.
    go Generate(ch)      // Launch Generate goroutine.
    for i := 0; i < 10; i++ {
        prime := <-ch
        print(prime, "\n")
        ch1 := make(chan int)
        go Filter(ch, ch1, prime)
        ch = ch1
    }
}

它并不是復(fù)雜度最低的算法,特別是尋找大素?cái)?shù)方面盈魁,但卻是最能體現(xiàn) Go 并發(fā)編程翔怎、通過通信共享內(nèi)存的理念,而且非常優(yōu)雅杨耙。

在這段代碼中赤套,通過 goroutine 的組合,實(shí)現(xiàn)一層層的篩選器珊膜,篩選器之間通過 channel 通信容握,每一個(gè)篩選器就是一個(gè)素?cái)?shù),每個(gè)給 main goroutine 通信的內(nèi)容也是素?cái)?shù)车柠,簡(jiǎn)直精妙剔氏。

通過下面的 gif 動(dòng)畫能清晰的看到程序運(yùn)行過程:

primesieve

并發(fā)不是并行

Go 的并行

只需要 GOMAXPROCS 的值大于1脖旱,就可以讓 Go 程序在多核機(jī)器上實(shí)現(xiàn)以并行的形式運(yùn)行。但并發(fā)的程序一定可以并行嗎介蛉?

我們需要明確一個(gè)觀點(diǎn):并發(fā)不是為了效率萌庆,并發(fā)的程序不一定可以并行。還是上面素?cái)?shù)的例子币旧,這段代碼是并發(fā)的践险,但不可以并行,因?yàn)樗拿恳粋€(gè)執(zhí)行片段都需要上一個(gè)片段的篩選與通信吹菱。

正交概念

正交概念:從數(shù)學(xué)上引進(jìn)正交這個(gè)詞巍虫,用于表示指相互獨(dú)立,相互間不可替代鳍刷,并且可以組合起來占遥。

在廣義概念上來講,并發(fā)與并行是正交概念输瓜,對(duì)于 Go 語(yǔ)言的并發(fā)性來講也是如此瓦胎。

《Concurrency is not Parallelism》

同樣,在 Go 語(yǔ)言發(fā)布之初尤揣,有很多人混淆了并發(fā)與并行的概念搔啊,對(duì)此,Rob Pike 發(fā)表了另一篇演講《Concurrency is not Parallelism》北戏,通過地鼠燒書的比喻與簡(jiǎn)單負(fù)載均衡器的示例负芋,詳細(xì)的闡述了并發(fā)與并行的區(qū)別。

這里不再?gòu)?fù)述地鼠例子嗜愈,只是簡(jiǎn)單的總結(jié)旧蛾,感興趣的建議去看演講:

并行是指同時(shí)能執(zhí)行多個(gè)事情。

并發(fā)關(guān)乎結(jié)構(gòu)蠕嫁,是一種結(jié)構(gòu)化程序的方式锨天。

并行關(guān)乎執(zhí)行,表述的是程序的運(yùn)行狀態(tài)拌阴。

Go 語(yǔ)言是如何支持并發(fā)的绍绘?

上面一直在講 Go 語(yǔ)言的并發(fā)性,接下來看下 Go 語(yǔ)言是如何做到的并發(fā)支持迟赃。

我們?cè)谑褂?Go 編寫并發(fā)程序的過程中陪拘,無需關(guān)心線程的維護(hù)、調(diào)度等一系列問題纤壁,只需要關(guān)心程序結(jié)構(gòu)的分解與組合左刽、goroutine 之間的通信就可以寫出良好的并發(fā)程序,這全部都要依賴于 Go 語(yǔ)言內(nèi)建的 G-P-M 模型酌媒。

模型演化過程

在 Go 語(yǔ)言1.0版本時(shí)欠痴,只有 G-M 模型迄靠,Google 工程師 Dmitry Vyukov 在《Scalable Go Scheduler Design Doc》中指出了該模型在并發(fā)伸縮性方面的問題:

  1. 所有對(duì) G 的操作:創(chuàng)建、重新調(diào)用等由單個(gè)全局鎖(Sched.Lock)保護(hù)喇辽,浪費(fèi)時(shí)間掌挚。
  2. 當(dāng) M 阻塞時(shí),G 需要傳遞給別的 M 執(zhí)行菩咨,這導(dǎo)致調(diào)度延遲增大以及額外的性能損耗吠式;
  3. M 用到的 mCache 屬于內(nèi)核線程,當(dāng) M 阻塞后相應(yīng)的內(nèi)存資源仍被占用抽米,導(dǎo)致內(nèi)存占用過高特占;
  4. 由于 syscall 導(dǎo)致 M 的阻塞和恢復(fù),導(dǎo)致了額外性能損耗云茸。

并且親自下場(chǎng)是目,重新設(shè)計(jì)、改進(jìn)了 Go scheduler标捺,在 Go1.1 版本中實(shí)現(xiàn)了 G-P-M 模型:

gpm-nino

G-P-M 模型

那么這套模型與調(diào)度是怎樣的呢懊纳?先來簡(jiǎn)單的說一下 G、P宜岛、M 的定義:

  • G:表示 Goroutine长踊,G 存儲(chǔ)了 goroutine 執(zhí)行的棧信息功舀,狀態(tài)萍倡,任務(wù)函數(shù),可重用辟汰。
  • P:Processor列敲,表示邏輯處理器,擁有一個(gè)本地隊(duì)列帖汞。對(duì)于 G 來說戴而,P 相當(dāng)于 CPU 內(nèi)核,只有進(jìn)入到 P 的隊(duì)列中翩蘸,才可以被調(diào)度所意。對(duì)于 M 來說,P 提供了相關(guān)的執(zhí)行環(huán)境(Context)催首,如內(nèi)存分配狀態(tài)扶踊,任務(wù)隊(duì)列等。P 的數(shù)量就是程序可最大可并行的 G 的數(shù)量(前提:物理CPU核數(shù) >= P的數(shù)量)郎任,由用戶設(shè)置的 GOMAXPROCS 決定秧耗。
  • M:Machine,是對(duì)系統(tǒng)線程的抽象舶治,是真正執(zhí)行計(jì)算的部分分井。M 在綁定 P 后會(huì)在隊(duì)列中獲取 G车猬,切換到 G 的執(zhí)行棧并執(zhí)行 G 的函數(shù)。M 數(shù)量不定尺锚,但同時(shí)只有 P 個(gè) M 在執(zhí)行珠闰,為了防止創(chuàng)建過多系統(tǒng)線程導(dǎo)致系統(tǒng)調(diào)度出現(xiàn)問題,目前默認(rèn)最大限制10000個(gè)瘫辩。

接下來了解這套模型的基本調(diào)度铸磅,在調(diào)度過程中還有一個(gè) work-stealing 的算法:

  • 每個(gè) P 維護(hù)一個(gè)本地隊(duì)列;
  • 當(dāng)一個(gè) G 被創(chuàng)建后杭朱,放入當(dāng)前 P 的本地隊(duì)列中阅仔,如果隊(duì)列已滿,放入全局隊(duì)列弧械;
  • 當(dāng) M 執(zhí)行完一個(gè) G 后八酒,會(huì)在 當(dāng)前 P 的隊(duì)列中取出新的 G,隊(duì)列為空則在全局隊(duì)列中加鎖獲热刑啤羞迷;
  • 如果全局隊(duì)列也為空,則去其他的 P 的隊(duì)列中偷出一半的 G画饥,放入自己的本地隊(duì)列衔瓮。

Go 語(yǔ)言就是憑借著這套優(yōu)秀的并發(fā)模型與調(diào)度,實(shí)現(xiàn)了內(nèi)建的并發(fā)支持抖甘。

Goroutine 調(diào)度器的深入

讓我們深入的了解一下 goroutine 調(diào)度器热鞍。

調(diào)度器解決了什么問題?

阻塞問題

如果任務(wù)G陷入到阻塞的系統(tǒng)調(diào)用中衔彻,內(nèi)核線程M將一起阻塞薇宠,于是實(shí)際的運(yùn)行線程少了一個(gè)。更嚴(yán)重的艰额,如果所有M都阻塞了澄港,那些本可以運(yùn)行的任務(wù)G將沒有系統(tǒng)資源運(yùn)行。

Go 在執(zhí)行阻塞的系統(tǒng)調(diào)用時(shí)會(huì)調(diào)用 entersyscallblock 柄沮,然后通過 handoffp 解綁 M 對(duì)應(yīng)的 P回梧。如果此時(shí) P 的本地隊(duì)列中還有 G,P 會(huì)去尋找別的 M 或創(chuàng)建新的 M 繼續(xù)執(zhí)行祖搓,若本地隊(duì)列為空狱意,則進(jìn)入 pidle 鏈表,等待有需要時(shí)被取出棕硫。

如果是調(diào)用的 entersyscall髓涯,會(huì)將 P 的狀態(tài)置為 _Psyscall。監(jiān)控線程 sysmon 會(huì)通過 retake 循環(huán)所有的 P哈扮,發(fā)現(xiàn)是 _Psyscall 狀態(tài)纬纪,就會(huì)調(diào)用 handoffp 來釋放蚓再。

搶占調(diào)度

在 Go1.1 版本中,是沒有搶占調(diào)度的包各,當(dāng)前 G 只有涉及到鎖操作摘仅,讀寫 channel 才會(huì)觸發(fā)切換。若沒有搶占機(jī)制问畅,同一個(gè) M 上的其他任務(wù) G 有可能會(huì)長(zhǎng)時(shí)間執(zhí)行不到娃属,甚至?xí)凰姥h(huán)鎖住。

于是 Dmitry Vyukov 提出了《Go Preemptive Scheduler Design Doc》, 并在1.2版本中引入了初級(jí)的搶占护姆。

監(jiān)控線程 sysmon 會(huì)通過 retake 循環(huán)所有的 P矾端,發(fā)現(xiàn)運(yùn)行時(shí)間超出 forcePreemptNS 限制(10ms)的 P,就會(huì)通過 preemptone 發(fā)起搶占卵皂。

Goroutine 的負(fù)載均衡

內(nèi)核線程M不是從全局任務(wù)隊(duì)列中得到G秩铆,而是從M本地維護(hù)的G緩存中獲取任務(wù)。如果某個(gè)M的G執(zhí)行完了灯变,而別的M還有很多G殴玛,這時(shí)如果G不能切換將造成CPU的浪費(fèi)。

這部分的實(shí)現(xiàn)是在 M 的啟動(dòng)函數(shù) mstartschedule 的調(diào)用來實(shí)現(xiàn)添祸,它會(huì)先查找本地隊(duì)列滚粟,然后查找全局隊(duì)列,最后是隨機(jī)偷取其他 P 的一半 G刃泌,直到取到 G 或停掉 M凡壤。為了防止全局隊(duì)列被“餓死”,每61次調(diào)度蔬咬,會(huì)先在全局隊(duì)列中查找鲤遥。

調(diào)度器相關(guān)源碼

調(diào)度器部分的代碼主要集中在 src/runtime/runtime2.gosrc/runtime/proc.go 這兩個(gè)文件中。

調(diào)度器的4個(gè)基本結(jié)構(gòu):g林艘、m、p混坞、schedt狐援,都在 runtime2.go 中,schedt 可能有些陌生究孕,它是調(diào)度器的核心結(jié)構(gòu)啥酱,也是全局資源池,用來存儲(chǔ) G 的全局隊(duì)列厨诸,空閑的 P 鏈表 pidle镶殷,空閑的 M 鏈表 midle 等等。

調(diào)度器的具體實(shí)現(xiàn)函數(shù)都在 proc.go 中微酬,用戶的所有代碼都是運(yùn)行在 goroutine 中绘趋,Go 在運(yùn)行時(shí)會(huì)將 main 中的代碼放入 main goroutine 中運(yùn)行颤陶,這時(shí)還會(huì)啟動(dòng)監(jiān)控系統(tǒng) sysmon

更多關(guān)于調(diào)度器的細(xì)節(jié)陷遮,例如加鎖滓走,與 GC 的交互等,需要通過進(jìn)一步的閱讀源碼來了解帽馋。

結(jié)束語(yǔ)

看到這里搅方,相信大家對(duì)“并發(fā)”會(huì)有全新的認(rèn)識(shí),本文旨在講清 Go 語(yǔ)言的并發(fā)性绽族,在以后的 Go 語(yǔ)言編程過程中姨涡,希望更傾向于并發(fā)編程。并發(fā)編程不僅結(jié)構(gòu)清晰吧慢,通常來說也會(huì)更容易并行運(yùn)行绣溜,使得程序運(yùn)行效率提高。

參考文章

《Go Concurrency Patterns》

《Concurrency is not Parallelism》

《go-under-the-hood》

《也談goroutine調(diào)度器》

《Goroutine淺析》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末娄蔼,一起剝皮案震驚了整個(gè)濱河市怖喻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌岁诉,老刑警劉巖锚沸,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異涕癣,居然都是意外死亡哗蜈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門坠韩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來距潘,“玉大人,你說我怎么就攤上這事只搁∫舯龋” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵氢惋,是天一觀的道長(zhǎng)洞翩。 經(jīng)常有香客問我,道長(zhǎng)焰望,這世上最難降的妖魔是什么骚亿? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮熊赖,結(jié)果婚禮上来屠,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好俱笛,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布捆姜。 她就那樣靜靜地躺著,像睡著了一般嫂粟。 火紅的嫁衣襯著肌膚如雪娇未。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天星虹,我揣著相機(jī)與錄音零抬,去河邊找鬼。 笑死宽涌,一個(gè)胖子當(dāng)著我的面吹牛平夜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播卸亮,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼忽妒,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了兼贸?” 一聲冷哼從身側(cè)響起段直,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎溶诞,沒想到半個(gè)月后鸯檬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡螺垢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年喧务,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枉圃。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡功茴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孽亲,到底是詐尸還是另有隱情坎穿,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布墨林,位于F島的核電站赁酝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏旭等。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一衡载、第九天 我趴在偏房一處隱蔽的房頂上張望搔耕。 院中可真熱鬧,春花似錦、人聲如沸弃榨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鲸睛。三九已至娜饵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間官辈,已是汗流浹背肝劲。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工参歹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓海雪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親添瓷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子每庆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容