golang goroutine的調度

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

golang goroutine的調度

1、什麼是協程?    協程是一種使用者態的輕量級線程。2、進程、線程、協程的關係和區別:    * 進程擁有自己獨立的堆和棧,既不共用堆,亦不共用棧,進程由作業系統調度。        * 線程擁有自己獨立的棧和共用的堆,共用堆,不共用棧,線程亦由作業系統調度(標準線程是的)。        * 協程和線程一樣共用堆,不共用棧,協程由程式員在協程的代碼裡顯示調度。        * 協程和線程的區別是:協程避免了無意義的調度,由此可以提高效能。        * 執行協程只需要極少的棧記憶體(大概是4~5KB),預設情況下,線程棧的大小為1MB。                goroutine就是一段代碼,一個函數入口,以及在堆上為其分配的一個堆棧。所以它非常廉價,我們可以很輕鬆的建立上萬個goroutine,但它們並不是被作業系統所調度執行。                runtime。GOMAXPROCS(runtime。NumCPU()) // go version>=1.5的時候,GOMAXPROCS的預設值就是go程式啟動時可見的作業系統認為的CPU個數。                注意:在go程式中使用的作業系統線程數量包括:正服務於cgo calls的線程,阻塞於作業系統calls的線程,所以go程式中使用的作業系統線程數量可能大於GOMAXPROCS的值。        3、調度    要理解協程的實現,首先需要瞭解go中的三個非常重要的概念,它們分別是G(goroutine)、M(machine)和P(process):        G (goroutine)            G是goroutine的頭文字,goroutine可以解釋為受管理的輕量線程,goroutine使用go關鍵詞建立。                舉例來說,func main() { go other() },這段代碼建立了兩個goroutine,一個是main,另一個是other,注意main本身也是一個goroutine。                goroutine的建立,休眠,恢複,停止都受到go運行時的管理。                goroutine執行非同步作業時會進入休眠狀態,待操作完成後再恢複,無需佔用系統線程。                goroutine建立或恢複時會添加到運行隊列,等待M取出並運行。       M (machine)            M是machine的頭文字,在目前的版本的golang中等同於系統線程。                M可以運行兩種代碼:                    * go代碼,即goroutine,M運行go代碼需要一個P。                        * 原生代碼,例如阻塞的syscall,M運行原生代碼不需要P。                M會從運行隊列中取出G,然後運行G,如果G運行完畢或者進入休眠狀態,則從運行隊列中取出下一個G運行,周而復始。                有時候G需要調用一些無法避免阻塞的原生代碼,這時M會釋放持有的P並進入阻塞狀態,其他M會取得這個P並繼續運行隊列中的G。                go需要保證有足夠的M可以運行G,不讓CPU閑著,也需要保證M的數量不能過多。        P (process)            P是process的頭文字,代表M運行G所需要的資源。                           雖然P的數量預設等於cpu核心數,但可以通過環境變數GOMAXPROC修改,在實際運行時P跟cpu核心並無任何關聯。                       P也可以理解為控制go代碼的並行度的機制,                    * 如果P的數量等於1,代表當前最多隻能有一個線程(M)執行go代碼;                        * 如果P的數量等於2,代表當前最多隻能有兩個線程(M)執行go代碼。                    執行原生代碼的線程數量不受P控制。                因為同一時間只有一個線程(M)可以擁有P,P中的資料都是鎖自由(lock free)的,讀寫這些資料的效率會非常的高。            備忘:每個P會維護一個本地的運行隊列,除了每個P擁有一個本地的運行隊列外,還存在一個全域的運行隊列。            為什麼需要創造一個使用者空間調度器?            POSIX線程API是對現有Unix進程模型的一個非常大的邏輯擴充,而且線程獲得了非常多的跟進程相同的控制。                比如,線程有它自己的訊號掩碼,線程能夠被賦予CPU affinity功能(就是指定線程只能在某個CPU上運行),                線程能被添加到[cgroups]中,線程所用到的資源也可以被查詢到。                所有的這些控制增大了Go程式使用goroutines時根本不需要的特性的開銷,當你的程式有100,000個線程的時候,這些開銷會急劇增長。                另外一個問題是,基於Go模型,作業系統不能給出特別好的決策。                比如,當運行一次垃圾收集的時候,Go的垃圾收集器要求所有線程都被停止而且要求記憶體要處於一致狀態。                這個涉及到要等待全部運行時線程到達一個點,系統事Crowdsourced Security Testing道在這個點記憶體是一致的。                當很多被調度的線程分散在隨機的點上的時候,結果就是你不得不等待他們中的大多數到達一致狀態。                Go調度器能夠作出這樣的決策,就是只在記憶體保持一致的點上進行調度。                這就意味著,當程式為垃圾收集而停止的時候,程式只須等待在一個CPU核上處於活躍運行狀態的線程即可。                目前有三個常見的執行緒模式:            一個是N:1的,即多個使用者空間線程運行在一個OS線程上。這個模型可以很快的進行環境切換,但是不能利用多核系統(multi-core systems)的優勢。                另一個模型是1:1的,即可執行程式的一個線程匹配一個OS線程。這個模型能夠利用機器上的所有核心的優勢,但是環境切換非常慢,因為它不得不陷入OS(trap through the OS)。                Go試圖通過M:N的調度器去擷取這兩個世界的全部優勢。它在任意數目的OS線程上調用任意數目的goroutines。你可以快速進行環境切換,並且還能利用你系統上所有的核心的優勢。這個模型主要的缺點是它增加了調度器的複雜性。            原理:            P的數量在初始化由GOMAXPROCS決定;                程式要做的就是添加G;                G的數量超出了M的處理能力,且還有空餘P的話,runtime就會自動建立新的M;                M拿到P後才能幹活,取G的順序:本地運行隊列 > 全域運行隊列 > 其他P的運行隊列,如果所有運行隊列都沒有可用的G,M會歸還P並進入休眠。                一個G如果發生阻塞等事件會進行阻塞,G發生環境切換條件:                                                      * 系統調用;                        * 讀寫channel;                        * gosched主動放棄,會將G扔進全域隊列;                一個G發生阻塞時,M0讓出P,由M1接管其任務隊列;當M0執行的阻塞調用返回後,再將G0扔到全域隊列,自己則進入睡眠(因為沒有P,無法幹活)。                例子:                        當G0調用一個系統調用的時候。因為一個線程不能既執行代碼同時又阻塞到一個系統調用上,則需要移交對應於這個線程的P以讓這個P可以被調度。                        M0放棄了它的P以保證其它的M1可以運行它。調度器確保有足夠的線程來運行所有的P。                        M1可能僅僅是系統為了處理G0的系統調用而被建立出來,或者它可能來自一個線程池。                        這個處於系統調用中的線程M0將會保持在這個導致系統調用的G0上,因為從技術上來說,它仍然在執行,雖然阻塞在OS裡了。                        當這個系統調用返回的時候,這個線程必須嘗試擷取一個P1來運行這個返回的G0,操作的正常模式是從其它所有線程M中的其中一個線程Mn中“盜取”一個Pn。                        如果“盜取”不成功,它就會把它的G0放到一個全域運行隊列中,然後把自己放到線程池中或者轉入睡眠狀態。                        這個全域運行隊列是各個P在運行完自己的本地運行隊列後用來擷取新G的地方。                        各個P也會周期性的檢查這個全域運行隊列上的G,否則,全域運行隊列上的G可能因得不到執行而被餓死。                        備忘:Go程式要在多線程上啟動並執行原因就是因為要處理系統調用,哪怕GOMAXPROCS等於1。運行時(runtime)使用調用系統調用的goroutines,而不是線程。                    “盜取”:                        當一個P運行完要被調度的所有G的時候。如果各個P的本地運行隊列裡的G的數目不均衡,改變就會發生了,                        否則會導致一個P1在執行完它的本地運行隊列裡的G後就會結束,儘管系統中仍然有許多G要執行。                        所以為了保持運行Go代碼,一個P能夠從全域運行隊列中擷取G,但是如果全域運行隊列中也沒有G了,那麼P就不得不從其它Pn的運行隊列擷取G了。                        當一個P完成自己的任務後,它就會嘗試“盜取”另一個P運行隊列中G的一半。這將確保每個P總是有活幹,然後反過來確保所有線程M儘可能處於最大負荷。                    備忘:goroutine是按照搶佔式調度的,一個goroutine最多執行10ms就會換作下一個。                    這個和目前主流系統的的cpu調度類似(按照時間分區)                        windows:20ms                        linux:5ms-800ms

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.