《Go語言四十二章經(jīng)》第二十一章 協(xié)程(goroutine)

《Go語言四十二章經(jīng)》第二十一章 協(xié)程(goroutine)

作者:李驍

Concurrency is about dealing with lots of things at once.

Parallelism is about doing lots of things at once.

并發(fā): 指的是程序的邏輯結(jié)構(gòu)。如果程序代碼結(jié)構(gòu)中的某些函數(shù)邏輯上可以同時(shí)運(yùn)行溯泣,但物理上未必會(huì)同時(shí)運(yùn)行虐秋。

并行: 并行是指程序的運(yùn)行狀態(tài)。并行則指的就是在物理層面也就是使用了不同CPU在執(zhí)行不同或者相同的任務(wù)垃沦。

21.1 并發(fā)

并發(fā)是在同一時(shí)間處理(dealing with)多件事情客给。并行是在同一時(shí)間做(doing)多件事情。并發(fā)的目的在于把當(dāng)個(gè) CPU 的利用率使用到最高肢簿。并行則需要多核 CPU 的支持靶剑。

Go 語言在語言層面上支持了并發(fā),goroutine是Go語言提供的一種用戶態(tài)線程池充,有時(shí)我們也稱之為協(xié)程桩引。所謂的協(xié)程,某種程度上也可以叫做輕量線程收夸,它不由os而由應(yīng)用程序創(chuàng)建和管理坑匠,因此使用開銷較低(一般為4K)。我們可以創(chuàng)建很多的goroutine卧惜,并且它們跑在同一個(gè)內(nèi)核線程之上的時(shí)候厘灼,就需要一個(gè)調(diào)度器來維護(hù)這些goroutine,確保所有的goroutine都能使用cpu序苏,并且是盡可能公平地使用cpu資源手幢。

調(diào)度器的主要有4個(gè)重要部分,分別是M忱详、G围来、P、Sched匈睁,前三個(gè)定義在runtime.h中监透,Sched定義在proc.c中。

  • M (work thread) 代表了系統(tǒng)線程OS Thread航唆,由操作系統(tǒng)管理胀蛮。

  • P (processor) 銜接M和G的調(diào)度上下文,它負(fù)責(zé)將等待執(zhí)行的G與M對(duì)接糯钙。P的數(shù)量可以通過GOMAXPROCS()來設(shè)置粪狼,它其實(shí)也就代表了真正的并發(fā)度退腥,即有多少個(gè)goroutine可以同時(shí)運(yùn)行。

  • G (goroutine) goroutine的實(shí)體再榄,包括了調(diào)用棧狡刘,重要的調(diào)度信息,例如channel等困鸥。

在操作系統(tǒng)的OS Thread和編程語言的User Thread之間嗅蔬,實(shí)際上存在3種線程對(duì)應(yīng)模型,也就是:1:1疾就,1:N澜术,M:N。

N:1 多個(gè)(N)用戶線程始終在一個(gè)內(nèi)核線程上跑猬腰,context上下文切換很快鸟废,但是無法真正的利用多核。
1:1 一個(gè)用戶線程就只在一個(gè)內(nèi)核線程上跑漆诽,這時(shí)可以利用多核侮攀,但是上下文切換很慢,切換效率很低厢拭。
M:N 多個(gè)goroutine在多個(gè)內(nèi)核線程上跑兰英,這個(gè)可以集齊上面兩者的優(yōu)勢(shì),但是無疑增加了調(diào)度的難度供鸠。

M:N 綜合兩種方式(N:1畦贸,1:1)的優(yōu)勢(shì)。多個(gè) goroutines 可以在多個(gè) OS threads 上處理楞捂。既能快速切換上下文薄坏,也能利用多核的優(yōu)勢(shì),而Go正是選擇這種實(shí)現(xiàn)方式寨闹。

Go 語言中的goroutine是運(yùn)行在多核CPU中的(通過runtime.GOMAXPROCS(1)設(shè)定CPU核數(shù))胶坠。 實(shí)際中運(yùn)行的CPU核數(shù)未必會(huì)和實(shí)際物理CPU數(shù)相吻合。

每個(gè)goroutine都會(huì)被一個(gè)特定的P(某個(gè)CPU)選定維護(hù)繁堡,而M(物理計(jì)算資源)每次挑選一個(gè)有效P沈善,然后執(zhí)行P中的goroutine。

每個(gè)P會(huì)將自己所維護(hù)的goroutine放到一個(gè)G隊(duì)列中椭蹄,其中就包括了goroutine堆棧信息闻牡,是否可執(zhí)行信息等等。

默認(rèn)情況下绳矩,P的數(shù)量與實(shí)際物理CPU的數(shù)量相等罩润。當(dāng)我們通過循環(huán)來創(chuàng)建goroutine時(shí),goroutine會(huì)被分配到不同的G隊(duì)列中翼馆。 而M的數(shù)量又不是唯一的割以,當(dāng)M隨機(jī)挑選P時(shí)金度,也就等同隨機(jī)挑選了goroutine。

所以拳球,當(dāng)我們碰到多個(gè)goroutine的執(zhí)行順序不是我們想象的順序時(shí)就可以理解了审姓,因?yàn)間oroutine進(jìn)入P管理的隊(duì)列G是帶有隨機(jī)性的。

P的數(shù)量由runtime.GOMAXPROCS(1)所設(shè)定祝峻,通常來說它是和內(nèi)核數(shù)對(duì)應(yīng),例如在4Core的服務(wù)器上會(huì)啟動(dòng)4個(gè)線程扎筒。G會(huì)有很多個(gè)莱找,每個(gè)P會(huì)將goroutine從一個(gè)就緒的隊(duì)列中做Pop操作,為了減小鎖的競(jìng)爭(zhēng)嗜桌,通常情況下每個(gè)P會(huì)負(fù)責(zé)一個(gè)隊(duì)列奥溺。

runtime.NumCPU()        // 返回當(dāng)前CPU內(nèi)核數(shù)
runtime.GOMAXPROCS(2)  // 設(shè)置運(yùn)行時(shí)最大可執(zhí)行CPU數(shù)
runtime.NumGoroutine() // 當(dāng)前正在運(yùn)行的goroutine 數(shù)

P維護(hù)著這個(gè)隊(duì)列(稱之為runqueue),Go語言里骨宠,啟動(dòng)一個(gè)goroutine很容易:go function 就行浮定,所以每有一個(gè)go語句被執(zhí)行,runqueue隊(duì)列就在其末尾加入一個(gè)goroutine层亿,在下一個(gè)調(diào)度點(diǎn)桦卒,就從runqueue中取出一個(gè)goroutine執(zhí)行。

假如有兩個(gè)M匿又,即兩個(gè)OS Thread線程方灾,分別對(duì)應(yīng)一個(gè)P,每一個(gè)P調(diào)度一個(gè)G隊(duì)列碌更。如此一來裕偿,就組成的goroutine運(yùn)行時(shí)的基本結(jié)構(gòu):

  • 當(dāng)有一個(gè)M返回時(shí),它必須嘗試取得一個(gè)P來運(yùn)行g(shù)oroutine痛单,一般情況下嘿棘,它會(huì)從其他的OS Thread線程那里竊取一個(gè)P過來,如果沒有拿到旭绒,它就把goroutine放在一個(gè)global runqueue里鸟妙,然后自己進(jìn)入線程緩存里。

  • 如果某個(gè)P所分配的任務(wù)G很快就執(zhí)行完了快压,這會(huì)導(dǎo)致多個(gè)隊(duì)列存在不平衡圆仔,會(huì)從其他隊(duì)列中截取一部分goroutine到P上進(jìn)行調(diào)度。一般來說蔫劣,如果P從其他的P那里要取任務(wù)的話坪郭,一般就取run queue的一半,這就確保了每個(gè)OS線程都能充分的使用脉幢。

  • 當(dāng)一個(gè)OS Thread線程被阻塞時(shí)歪沃,P可以轉(zhuǎn)而投奔另一個(gè)OS線程嗦锐。

下面是G、 M沪曙、 P的具體結(jié)構(gòu)奕污,這不是Go代碼:

struct  G
{
    uintptr stackguard0;// 用于棧保護(hù),但可以設(shè)置為StackPreempt液走,用于實(shí)現(xiàn)搶占式調(diào)度
    uintptr stackbase;  // 棧頂
    Gobuf   sched;      // 執(zhí)行上下文碳默,G的暫停執(zhí)行和恢復(fù)執(zhí)行,都依靠它
    uintptr stackguard; // 跟stackguard0一樣缘眶,但它不會(huì)被設(shè)置為StackPreempt
    uintptr stack0;     // 棧底
    uintptr stacksize;  // 棧的大小
    int16   status;     // G的六個(gè)狀態(tài)
    int64   goid;       // G的標(biāo)識(shí)id
    int8*   waitreason; // 當(dāng)status==Gwaiting有用嘱根,等待的原因,可能是調(diào)用time.Sleep之類
    G*  schedlink;      // 指向鏈表的下一個(gè)G
    uintptr gopc;       // 創(chuàng)建此goroutine的Go語句的程序計(jì)數(shù)器PC巷懈,通過PC可以獲得具體的函數(shù)和代碼行數(shù)
};
struct P
{
    Lock;       // plan9 C的擴(kuò)展語法该抒,相當(dāng)于Lock lock;
    int32   id;  // P的標(biāo)識(shí)id
    uint32  status;     // P的四個(gè)狀態(tài)
    P*  link;       // 指向鏈表的下一個(gè)P
    M*  m;      // 它當(dāng)前綁定的M,Pidle狀態(tài)下顶燕,該值為nil
    MCache* mcache; // 內(nèi)存池
    // Grunnable狀態(tài)的G隊(duì)列
    uint32  runqhead;
    uint32  runqtail;
    G*  runq[256];
    // Gdead狀態(tài)的G鏈表(通過G的schedlink)
    // gfreecnt是鏈表上節(jié)點(diǎn)的個(gè)數(shù)
    G*  gfree;
    int32   gfreecnt;
};
struct  M
{
    G*  g0;     // M默認(rèn)執(zhí)行G
    void    (*mstartfn)(void);  // OS線程執(zhí)行的函數(shù)指針
    G*  curg;       // 當(dāng)前運(yùn)行的G
    P*  p;      // 當(dāng)前關(guān)聯(lián)的P凑保,要是當(dāng)前不執(zhí)行G,可以為nil
    P*  nextp;  // 即將要關(guān)聯(lián)的P
    int32   id; // M的標(biāo)識(shí)id
    M*  alllink;    // 加到allm涌攻,使其不被垃圾回收(GC)
    M*  schedlink;  // 指向鏈表的下一個(gè)M
};

我們可以運(yùn)行下面代碼體驗(yàn)下Go語言中通過設(shè)定runtime.GOMAXPROCS(2) 欧引,也即手動(dòng)指定CPU運(yùn)行的核數(shù),來體驗(yàn)多核CPU在并發(fā)處理時(shí)的威力癣漆。不得不提维咸,遞歸函數(shù)的計(jì)算很費(fèi)CPU和內(nèi)存,運(yùn)行時(shí)可以根據(jù)電腦配置修改循環(huán)或遞歸數(shù)量惠爽。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

var quit chan int = make(chan int)

func loop() {
    for i := 0; i < 1000; i++ {
        Factorial(uint64(1000))
    }
    quit <- 1
}
func Factorial(n uint64) (result uint64) {
    if n > 0 {
        result = n * Factorial(n-1)
        return result
    }
    return 1
}

var wg1, wg2 sync.WaitGroup

func main() {
    fmt.Println("1:", time.Now())
    fmt.Println(runtime.NumCPU()) // 默認(rèn)CPU核數(shù)
    a := 5000
    for i := 1; i <= a; i++ {
        wg1.Add(1)
        go loop()
    }

    for i := 0; i < a; i++ {
        select {
        case <-quit:
            wg1.Done()
        }
    }
    fmt.Println("2:", time.Now())
    wg1.Wait()

    fmt.Println("3:", time.Now())
    runtime.GOMAXPROCS(2) // 設(shè)置執(zhí)行使用的核數(shù)
    a = 5000
    for i := 1; i <= a; i++ {
        wg2.Add(1)
        go loop()
    }

    for i := 0; i < a; i++ {
        select {
        case <-quit:
            wg2.Done()
        }
    }

    fmt.Println("4:", time.Now())
    wg2.Wait()
    fmt.Println("5:", time.Now())
}

我的測(cè)試電腦CPU默認(rèn)是4核癌蓖,對(duì)比手動(dòng)設(shè)置CPU在2核時(shí)的運(yùn)行耗時(shí),4核耗時(shí)約8秒婚肆,2核約14秒租副,當(dāng)然這是一種比較理想化的測(cè)試,因?yàn)殡A乘很快導(dǎo)致unit64為0较性,所以這個(gè)測(cè)試并不嚴(yán)謹(jǐn)用僧,但從中我們?nèi)匀豢梢泽w驗(yàn)到Go語言在處理并發(fā)(cpu)時(shí)代碼之簡(jiǎn)單,控制之方便赞咙。

在實(shí)際中運(yùn)行速度延緩可能不一定僅僅是由于CPU的競(jìng)爭(zhēng)责循,可能還有內(nèi)存或者I/O的原因?qū)е碌模覀冃枰鶕?jù)情況仔細(xì)分析攀操。

最后院仿,runtime.Gosched()用于讓出CPU時(shí)間片,讓出當(dāng)前goroutine的執(zhí)行權(quán)限,調(diào)度器安排其他等待的任務(wù)運(yùn)行歹垫,并在下次某個(gè)時(shí)候從該位置恢復(fù)執(zhí)行剥汤。

21.2 goroutine

在Go語言中,協(xié)程(goroutine)的使用很簡(jiǎn)單排惨,直接在函數(shù)(代碼塊)前加上關(guān)鍵字 go 即可吭敢。go關(guān)鍵字就是用來創(chuàng)建一個(gè)協(xié)程(goroutine)的,后面的代碼塊就是這個(gè)協(xié)程(goroutine)需要執(zhí)行的代碼邏輯暮芭。

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 1; i < 10; i++ {
        go func(i int) {
            fmt.Println(i)
        }(i)
    }
    // 暫停一會(huì)鹿驼,保證打印全部結(jié)束
    time.Sleep(1e9)
}

time.Sleep(1e9)讓主程序不會(huì)馬上退出,以便讓協(xié)程(goroutine)運(yùn)行完成谴麦,避免主程序退出時(shí)協(xié)程(goroutine)未處理完成甚至沒有開始運(yùn)行蠢沿。

有關(guān)于協(xié)程(goroutine)之間的通信以及協(xié)程(goroutine)與主線程的控制以及多個(gè)協(xié)程(goroutine)的管理和控制,我們后續(xù)通過channel匾效、context以及鎖來進(jìn)一步說明。

本書《Go語言四十二章經(jīng)》內(nèi)容在github上同步地址:https://github.com/ffhelicopter/Go42
本書《Go語言四十二章經(jīng)》內(nèi)容在簡(jiǎn)書同步地址: http://www.reibang.com/nb/29056963

雖然本書中例子都經(jīng)過實(shí)際運(yùn)行恤磷,但難免出現(xiàn)錯(cuò)誤和不足之處面哼,煩請(qǐng)您指出;如有建議也歡迎交流扫步。
聯(lián)系郵箱:roteman@163.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末魔策,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子河胎,更是在濱河造成了極大的恐慌闯袒,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件游岳,死亡現(xiàn)場(chǎng)離奇詭異政敢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)胚迫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門喷户,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人访锻,你說我怎么就攤上這事褪尝。” “怎么了期犬?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵河哑,是天一觀的道長。 經(jīng)常有香客問我龟虎,道長璃谨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任遣总,我火速辦了婚禮睬罗,結(jié)果婚禮上轨功,老公的妹妹穿的比我還像新娘。我一直安慰自己容达,他們只是感情好古涧,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著花盐,像睡著了一般羡滑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上算芯,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天柒昏,我揣著相機(jī)與錄音,去河邊找鬼熙揍。 笑死职祷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的届囚。 我是一名探鬼主播有梆,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼意系!你這毒婦竟也來了泥耀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤蛔添,失蹤者是張志新(化名)和其女友劉穎痰催,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體迎瞧,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乓搬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年框喳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡枢贿,死狀恐怖俺猿,靈堂內(nèi)的尸體忽然破棺而出乔煞,到底是詐尸還是另有隱情褂傀,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布编检,位于F島的核電站胎食,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏允懂。R本人自食惡果不足惜厕怜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧粥航,春花似錦琅捏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缀程,卻和暖如春搜吧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杨凑。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國打工滤奈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撩满。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓蜒程,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伺帘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子搞糕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

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