Go語言中goroutine的分析

Goroutine是Go里的一種輕量級線程——協(xié)程催享。相對線程,協(xié)程的優(yōu)勢就在于它非常輕量級茎刚,進行上下文切換的代價非常的小譬正。對于一個goroutine ,每個結構體G中有一個sched的屬性就是用來保存它上下文的草穆。這樣,goroutine 就可以很輕易的來回切換。由于其上下文切換在用戶態(tài)下發(fā)生,根本不必進入內核態(tài)雕崩,所以速度很快。而且只有當前goroutine 的 PC, SP等少量信息需要保存波俄。

在Go語言中晨逝,每一個并發(fā)的執(zhí)行單元為一個goroutine。當我們開始運行一個Go程序時懦铺,它的入口函數(shù) main 實際上就是運行在一個goroutine 里。

Goroutine之間的通信

Go 語言編寫的程序通過不同的goroutine 運行支鸡,但是goroutine之間是相互獨立的冬念,各自運行在不同的上下文中。 每個 goroutine 之間的通信需要借助 channel 牧挣,channel 是Go 語言里的一種通信機制急前。Channel 也是Go語言里的一種引用類型,通過make函數(shù)瀑构,我們可以很容易的聲明一個channel裆针。


ch:=make(chanstring)

Channel 有單方向,雙方向之分:

  • chan int, 雙方向寺晌,可用來收發(fā)數(shù)據(jù)世吨。

  • chan <- int,單方向,只能用來發(fā)送數(shù)據(jù)

  • <- chan int,單方向呻征,只能用來接收數(shù)據(jù)

另外耘婚,channel還有有緩存無緩存之分。通過make函數(shù)創(chuàng)建一個帶緩存的channel陆赋。


ch:=make(chanstring, n)

無緩存的channel保證了每次發(fā)送數(shù)據(jù)的同步接收操作沐祷。而帶緩存的channel解耦了發(fā)送與接收間的操作嚷闭,這樣不但是影響程序的性能還有可能引起死鎖的問題。

調度器sheduler

每個goroutine的運行都是由Go語言里的調度器(scheduler)決定的赖临。

先說操作系統(tǒng)的線程調度胞锰。在POSIX 中有一個sheduler的內核函數(shù),每過幾ms會被執(zhí)行一次兢榨。每次執(zhí)行時嗅榕,會掛起當前執(zhí)行線程,同時保存它寄存器中信息色乾,接著查看線程列表決定下一個線程的運行誊册, 從內存中必復其寄存器信息和現(xiàn)場并開始執(zhí)行。不同線程之間存在上下文切換暖璧,這包括保存一個用戶線程的狀態(tài)到內存案怯,恢復另一個線程的信息到寄存器,同時還要更新sheduler相關的數(shù)據(jù)結構澎办。這些操作都很耗時嘲碱。

Go 語言的Runtime有自己的sheduler,通過它我們可以在n個操作系統(tǒng)的線程上調度m個goroutine局蚀。實際上Go 的sheduler與操作系統(tǒng)的sheduler是非常相似的麦锯,只不過它只關心goroutine的調度。與操作系統(tǒng)sheduler不同的是琅绅,Go的sheduler不使用硬件定時器扶欣,當一個goroutine 調用了time.Sleep、觸發(fā)一個channel 操作或者使用 mutex千扶, scheduler 會使這個 goroutine 進行睡眠料祠,進而去喚醒另外一個goroutine,這種調度方式?jīng)]有上下文之間的切換澎羞,它的代價比操作系統(tǒng)的線程調度要小得多髓绽。

Go的調度的實現(xiàn),涉及到幾個重要的數(shù)據(jù)結構妆绞。運行時庫用這幾個數(shù)據(jù)結構來實現(xiàn)goroutine的調度顺呕,管理goroutine和物理線程的運行。這些數(shù)據(jù)結構分別是結構體G括饶,結構體M株茶,結構體P,以及 Sched 結構體巷帝。這三個結構定義在文件 runtime/runtime.h 中忌卤,而Sched的定義在 runtime/proc.c 中。在Go語言中scheduler 通過一個GOMAXPROCS變量來決定有多少個操作系統(tǒng)的線程來運行Go程序楞泼,默認值為CPU的核心數(shù)驰徊。

結構體G

G 是 goroutine 的縮寫笤闯,相當于操作系統(tǒng)中的進程控制塊,在這里就是 goroutine 的控制結構棍厂,是對goroutine的抽象颗味。其中包括 goid 是這個 goroutine 的ID, status 是這個goroutine 的狀態(tài)牺弹,如 Gidle, Grunnable, Grunning, Gsyscall, Gwaiting,Gdead 等浦马。


structG
{
    uintptr    stackguard;    // 分段棧的可用空間下界
    uintptr    stackbase;    // 分段棧的棧基址
    Gobuf    sched;        //進程切換時张漂,利用sched域來保存上下文
    uintptr    stack0;
    FuncVal*    fnstart;        // goroutine運行的函數(shù)
    void*    param;        // 用于傳遞參數(shù)晶默,睡眠時其它goroutine設置param,喚醒時此goroutine可以獲取
    int16    status;        // 狀態(tài)    Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
    int64    goid;        // goroutine的id號
    G*    schedlink;
    M*    m;        // for debuggers, but offset not hard-coded
    M*    lockedm;    // G被鎖定只能在這個m上運行
    uintptr    gopc;    // 創(chuàng)建這個goroutine的go表達式的pc
...
};

結構體G中的部分域如上所示航攒』嵌福可以看到,其中包含了棧信息stackbase和stackguard漠畜,有運行的函數(shù)信息fnstart币他。這些就足夠成為一個可執(zhí)行的單元了,只要得到CPU就可以運行憔狞。

goroutine 切換時蝴悉,上下文信息保存在結構體的 sched 域中。goroutine 是輕量級的線程或者稱為協(xié)程瘾敢,切換時并不必陷入到操作系統(tǒng)內核中拍冠,所以保存過程很輕量〈氐郑看一下結構體 G 中的 Gobuf倦微,其實只保存了當前棧指針,程序計數(shù)器正压,以及 goroutine 自身。


structGobuf
{

    // The offsets of these fields are known to (hard-coded in) libmach.
    uintptr    sp;
    byte*    pc;
    G*    g;
...
};

記錄g是為了恢復當前goroutine的結構體G指針责球,運行時庫中使用了一個常駐的寄存器extern register G* g焦履,這個是當前goroutine的結構體G的指針。這樣做是為了快速地訪問goroutine中的信息雏逾,比如嘉裤,Go的棧的實現(xiàn)并沒有使用%ebp寄存器,不過這可以通過g->stackbase快速得到栖博。"extern register"是由6c屑宠,8c等實現(xiàn)的一個特殊的存儲。在ARM上它是實際的寄存器仇让;其它平臺是由段寄存器進行索引的線程本地存儲的一個槽位典奉。在linux系統(tǒng)中躺翻,對g和m使用的分別是0(GS)和4(GS)。需要注意的是卫玖,鏈接器還會根據(jù)特定操作系統(tǒng)改變編譯器的輸出公你,例如,6l/linux下會將0(GS)重寫為-16(FS)假瞬。每個鏈接到Go程序的C文件都必須包含runtime.h頭文件陕靠,這樣C編譯器知道避免使用專用的寄存器。

結構體M

M是machine的縮寫脱茉,是對機器的抽象剪芥,每個m都是對應到一條操作系統(tǒng)的物理線程。M必須關聯(lián)了P才可以執(zhí)行Go代碼琴许,但是當它處理阻塞或者系統(tǒng)調用中時税肪,可以不需要關聯(lián)P。


structM
{
    G*    g0;        // 帶有調度棧的goroutine
    G*    gsignal;    // signal-handling G 處理信號的goroutine
    void    (*mstartfn)(void);
    G*    curg;        // M中當前運行的goroutine
    P*    p;        // 關聯(lián)P以執(zhí)行Go代碼 (如果沒有執(zhí)行Go代碼則P為nil)
    P*    nextp;
    int32    id;
    int32    mallocing; //狀態(tài)
    int32    throwing;
    int32    gcing;
    int32    locks;
    int32    helpgc;        //不為0表示此m在做幫忙gc虚吟。helpgc等于n只是一個編號
    bool    blockingsyscall;
    bool    spinning;
    Note    park;
    M*    alllink;    // 這個域用于鏈接allm
    M*    schedlink;
    MCache    *mcache;
    G*    lockedg;
    M*    nextwaitm;    // next M waiting for lock
    GCStats    gcstats;
...
};

和G類似寸认,M中也有alllink域將所有的M放在allm鏈表中。lockedg是某些情況下串慰,G鎖定在這個M中運行而不會切換到其它M中去偏塞。M中還有一個MCache,是當前M的內存的緩存邦鲫。M也和G一樣有一個常駐寄存器變量灸叼,代表當前的M。同時存在多個M庆捺,表示同時存在多個物理線程古今。結構體M中有兩個G是需要關注一下的,一個是curg滔以,代表結構體M當前綁定的結構體G捉腥。另一個是g0,是帶有調度棧的goroutine你画,這是一個比較特殊的goroutine抵碟。普通的goroutine的棧是在堆上分配的可增長的棧,而g0的棧是M對應的線程的棧坏匪。所有調度相關的代碼拟逮,會先切換到該goroutine的棧中再執(zhí)行。

結構體P

Go1.1中新加入的一個數(shù)據(jù)結構适滓,它是Processor的縮寫敦迄。結構體P的加入是為了提高Go程序的并發(fā)度,實現(xiàn)更好的調度。M代表OS線程罚屋。P代表Go代碼執(zhí)行時需要的資源苦囱。當M執(zhí)行Go代碼時,它需要關聯(lián)一個P沿后,當M為idle或者在系統(tǒng)調用中時沿彭,它也需要P。有剛好GOMAXPROCS個P尖滚。所有的P被組織為一個數(shù)組喉刘,在P上實現(xiàn)了工作流竊取的調度器。


structP
{
    Lock;
    uint32    status;  // Pidle或Prunning等
    P*    link;
    uint32    schedtick;  // 每次調度時將它加一
    M*    m;    // 鏈接到它關聯(lián)的M (nil if idle)
    MCache*    mcache;
    G*    runq[256];
    int32    runqhead;
    int32    runqtail;
    // Available G's (status == Gdead)
    G*    gfree;
    int32    gfreecnt;
    byte    pad[64];
};

結構體P中也有相應的狀態(tài):Pidle, Prunning, Psyscall, Pgcstop, Pdead漆弄。跟G不同的是睦裳,P 不存在waiting狀態(tài)。跟G不同的是撼唾,P不存在waiting狀態(tài)廉邑。MCache被移到了P中,但是在結構體M中也還保留著倒谷。在P中有一個Grunnable的goroutine隊列蛛蒙,這是一個P的局部隊列。當P執(zhí)行Go代碼時渤愁,它會優(yōu)先從自己的這個局部隊列中取牵祟,這時可以不用加鎖,提高了并發(fā)度抖格。如果發(fā)現(xiàn)這個隊列空了诺苹,則去其它P的隊列中拿一半過來,這樣實現(xiàn)工作流竊取的調度雹拄。這種情況下是需要給調用器加鎖的收奔。

結構體Sched

Sched是調度實現(xiàn)中使用的數(shù)據(jù)結構,該結構體的定義在文件proc.c中滓玖。

structSched {
    Lock;
    uint64    goidgen;
    M*    midle;    // idle m's waiting for work
    int32    nmidle;    // number of idle m's waiting for work
    int32    nmidlelocked; // number of locked m's waiting for work
    int3    mcount;    // number of m's that have been created
    int32    maxmcount;    // maximum number of m's allowed (or die)
    P*    pidle;  // idle P's
    uint32    npidle;  //idle P的數(shù)量
    uint32    nmspinning;
      // Global runnable queue.
    G*    runqhead;
    G*    runqtail;
    int32    runqsize;
      // Global cache of dead G's.
    Lock    gflock;
    G*    gfree;
    int32    stopwait;
    Note    stopnote;
    uint32    sysmonwait;
    Note    sysmonnote;
    uint64    lastpoll;
    int32    profilehz;    // cpu profiling rate
}

大多數(shù)需要的信息都已放在了結構體M坪哄、G和P中,Sched結構體只是一個殼势篡∷鸾可以看到,其中有M的idle隊列殊霞,P的idle隊列,以及一個全局的就緒的G隊列汰蓉。Sched結構體中的Lock是非常必須的绷蹲,如果M或P等做一些非局部的操作,它們一般需要先鎖住調度器。

Goroutine的特點

Goroutine是Go Runtime所提供的祝钢,并非操作系統(tǒng)層面上支持的比规,goroutine不是用線程實現(xiàn)的。goroutine就是一段代碼拦英,一個函數(shù)入口蜒什,以及在堆上為其分配的一個堆棧。這個棧通常很小疤估,一般為2kB, 所以它非常廉價灾常,我們可以很輕松的創(chuàng)建上成千萬個goroutine,這是很普遍的铃拇。Goroutine 的棧大小不是固定的钞瀑,這一點和操作系統(tǒng)的線程是不一樣的,它可以動態(tài)的擴展慷荔,最大值可達1GB雕什。

Goroutine是協(xié)作式調度的,如果goroutine會執(zhí)行很長時間显晶,而且不是通過等待讀取或寫入channel的數(shù)據(jù)來同步的話贷岸,就需要主動調用Gosched()來讓出CPU。

Go語言封裝了異步IO磷雇,所以可以寫出貌似并發(fā)數(shù)很多的服務端偿警,可即使我們通過調整GOMAXPROCS來充分利用多核CPU并行處理,其效率也不如我們利用IO事件驅動設計的倦春、按照事務類型劃分好合適比例的線程池户敬。在響應時間上,協(xié)作式調度是硬傷睁本。

每個goroutine是沒有身份標識的尿庐,這是為了避免像Thread Local Storage那樣被爛用,一個函數(shù)的行為不可能由其本身變量決定呢堰。

Goroutine最大的優(yōu)點是在并發(fā)開發(fā)中實現(xiàn)了對線程池的動態(tài)擴展抄瑟,不會由于某個任務的阻塞而導致死鎖。隨著其運行庫的不斷發(fā)展和完善及多核大行其道的年代枉疼,其優(yōu)勢會日益凸顯皮假。

下面來看一個實例。

實例:生產者消費者問題

通過goroutine實現(xiàn)生產者消費者問題骂维,利用 channel 通信惹资。只需要短短幾行代碼,我們自己根本不需要編寫代碼考慮線程的同步問題航闺。

需要事先聲明的變量褪测,goods 是生產消費所共享的數(shù)據(jù)猴誊,聲明為一個chan 類型,存放整型數(shù)據(jù)侮措。接著聲明一個隨機數(shù)種子懈叹,根據(jù)系統(tǒng)時間生成偽隨機數(shù)。done 也是一個 chan 類型分扎,里面只存儲一個空的struct澄成,其作用是為了保證主線程在其它 goroutine 結束之后結束。

    var goods chan int
    var r  = rand.New(rand.NewSource(time.Now().UnixNano()))//定義一個隨機數(shù)種子
    vardone chan struct{}

生產者函數(shù)畏吓,循環(huán)10次墨状,依次向 goods里寫入1到10,每寫完一次后,隨機睡眠1~3秒庵佣。


funcproduce()  {
    for i:=1; i<=10; i++ {
        goods <- i
        time.Sleep(time.Duration(r.Int31n(3))*time.Second)
}
    done <-struct{}{}
}

消費者函數(shù),循環(huán)5次從 goods 里取值歉胶,每讀完一次,隨機睡眠1~5秒巴粪。


funcconsume() {
    for i:=0; i<5; i++ {
        good := <- goods
        fmt.Printf("The goods size is : %v\n",10-good+1)
        time.Sleep(time.Duration(r.Int31n(5))*time.Second)
    }
}

main 函數(shù)里開啟一個goroutine 運行 produce 函數(shù)通今,兩個goroutine 運行consume 函數(shù)。


funcmain()  {

    goods =make(chanint)
    done =make(chanstruct{})
    goproduce()
    goconsume()
    goconsume()
    <- done
}

output:

    The goods size is :10
    The goods size is :9
    The goods size is :8
    The goods size is :7
    The goods size is :6
    The goods size is :5
    The goods size is :4
    The goods size is :3
    The goods size is :2
    The goods size is :1

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末肛根,一起剝皮案震驚了整個濱河市辫塌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌派哲,老刑警劉巖臼氨,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異芭届,居然都是意外死亡储矩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門褂乍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來持隧,“玉大人,你說我怎么就攤上這事逃片÷挪Γ” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵褥实,是天一觀的道長呀狼。 經(jīng)常有香客問我,道長损离,這世上最難降的妖魔是什么哥艇? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮僻澎,結果婚禮上她奥,老公的妹妹穿的比我還像新娘瓮增。我一直安慰自己,他們只是感情好哩俭,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拳恋,像睡著了一般凡资。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谬运,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天隙赁,我揣著相機與錄音,去河邊找鬼梆暖。 笑死伞访,一個胖子當著我的面吹牛,可吹牛的內容都是我干的轰驳。 我是一名探鬼主播厚掷,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼级解!你這毒婦竟也來了冒黑?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤勤哗,失蹤者是張志新(化名)和其女友劉穎抡爹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芒划,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡冬竟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了民逼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泵殴。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖缴挖,靈堂內的尸體忽然破棺而出袋狞,到底是詐尸還是另有隱情,我是刑警寧澤映屋,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布苟鸯,位于F島的核電站,受9級特大地震影響棚点,放射性物質發(fā)生泄漏早处。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一瘫析、第九天 我趴在偏房一處隱蔽的房頂上張望砌梆。 院中可真熱鬧默责,春花似錦、人聲如沸咸包。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烂瘫。三九已至媒熊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間坟比,已是汗流浹背芦鳍。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留葛账,地道東北人柠衅。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像籍琳,于是被迫代替她去往敵國和親菲宴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容

  • http://skoo.me/go/2013/11/29/golang-schedule?hmsr=studygo...
    baboon閱讀 2,247評論 0 3
  • 輕量級進程模型: 用同步IO的方法寫程序的邏輯巩割,第二點是用盡可能多的并發(fā)進程來提升IO并發(fā)的能力裙顽。 核心思想,第...
    lifesoul閱讀 2,681評論 4 1
  • 導語 Go語言(也稱為Golang)是google在2009年推出的一種編譯型編程語言宣谈。相對于大多數(shù)語言愈犹,gola...
    star24閱讀 8,975評論 5 21
  • 操作系統(tǒng)的調度模型是大致上有兩種N:1和1:1. N:1模型中用戶態(tài)的線程運行在一個內核線程上,這種方式上下文切換...
    ieasy_tm閱讀 712評論 0 4
  • 一闻丑、思維固化的人漩怎。這樣的人腦袋里有一種判斷人的固有模式,好比一個又一個套子嗦嗡,別人的一句話一個行動勋锤,就像是一個標本一...
    助心閱讀 451評論 1 2