Go 語言相比Java等一個很大的優(yōu)勢就是可以方便地編寫并發(fā)程序。Go 語言內(nèi)置了 goroutine 機(jī)制,使用goroutine可以快速地開發(fā)并發(fā)程序, 更好的利用多核處理器資源。這篇文章學(xué)習(xí)goroutine 的應(yīng)用及其調(diào)度實現(xiàn)。
一、Go語言對并發(fā)的支持
使用goroutine編程
使用 go 關(guān)鍵字用來創(chuàng)建 goroutine 。將go聲明放到一個需調(diào)用的函數(shù)之前,在相同地址空間調(diào)用運行這個函數(shù),這樣該函數(shù)執(zhí)行時便會作為一個獨立的并發(fā)線程。這種線程在Go語言中稱作goroutine。
goroutine的用法如下:
//go 關(guān)鍵字放在方法調(diào)用前新建一個 goroutine 并執(zhí)行方法體
go GetThingDone(param1, param2);
//新建一個匿名方法并執(zhí)行
go func(param1, param2) {
}(val1, val2)
//直接新建一個 goroutine 并在 goroutine 中執(zhí)行代碼塊
go {
//do someting...
}
因為 goroutine 在多核 cpu 環(huán)境下是并行的。如果代碼塊在多個 goroutine 中執(zhí)行,我們就實現(xiàn)了代碼并行。
如果需要了解程序的執(zhí)行情況,怎么拿到并行的結(jié)果呢?需要配合使用channel進(jìn)行。
使用Channel控制并發(fā)
Channels用來同步并發(fā)執(zhí)行的函數(shù)并提供它們某種傳值交流的機(jī)制。
通過channel傳遞的元素類型、容器(或緩沖區(qū))和傳遞的方向由“-”操作符指定。
可以使用內(nèi)置函數(shù) make分配一個channel:
i := make(chan int) // by default the capacity is 0
s := make(chan string, 3) // non-zero capacity
r := make(-chan bool) // can only read from
w := make(chan- []os.FileInfo) // can only write to
配置runtime.GOMAXPROCS
使用下面的代碼可以顯式的設(shè)置是否使用多核來執(zhí)行并發(fā)任務(wù):
GOMAXPROCS的數(shù)目根據(jù)任務(wù)量分配就可以,但是不要大于cpu核數(shù)。
配置并行執(zhí)行比較適合適合于CPU密集型、并行度比較高的情景,如果是IO密集型使用多核的化會增加cpu切換帶來的性能損失。
了解了Go語言的并發(fā)機(jī)制,接下來看一下goroutine 機(jī)制的具體實現(xiàn)。
二、區(qū)別并行與并發(fā)
進(jìn)程、線程與處理器
在現(xiàn)代操作系統(tǒng)中,線程是處理器調(diào)度和分配的基本單位,進(jìn)程則作為資源擁有的基本單位。每個進(jìn)程是由私有的虛擬地址空間、代碼、數(shù)據(jù)和其它各種系統(tǒng)資源組成。線程是進(jìn)程內(nèi)部的一個執(zhí)行單元。 每一個進(jìn)程至少有一個主執(zhí)行線程,它無需由用戶去主動創(chuàng)建,是由系統(tǒng)自動創(chuàng)建的。 用戶根據(jù)需要在應(yīng)用程序中創(chuàng)建其它線程,多個線程并發(fā)地運行于同一個進(jìn)程中。
并行與并發(fā)
并行與并發(fā)(Concurrency and Parallelism)是兩個不同的概念,理解它們對于理解多線程模型非常重要。
在描述程序的并發(fā)或者并行時,應(yīng)該說明從進(jìn)程或者線程的角度出發(fā)。
- 并發(fā):一個時間段內(nèi)有很多的線程或進(jìn)程在執(zhí)行,但何時間點上都只有一個在執(zhí)行,多個線程或進(jìn)程爭搶時間片輪流執(zhí)行
- 并行:一個時間段和時間點上都有多個線程或進(jìn)程在執(zhí)行
非并發(fā)的程序只有一個垂直的控制邏輯,在任何時刻,程序只會處在這個控制邏輯的某個位置,也就是順序執(zhí)行。如果一個程序在某一時刻被多個CPU流水線同時進(jìn)行處理,那么我們就說這個程序是以并行的形式在運行。
并行需要硬件支持,單核處理器只能是并發(fā),多核處理器才能做到并行執(zhí)行。
- 并發(fā)是并行的必要條件,如果一個程序本身就不是并發(fā)的,也就是只有一個邏輯執(zhí)行順序,那么我們不可能讓其被并行處理。
- 并發(fā)不是并行的充分條件,一個并發(fā)的程序,如果只被一個CPU進(jìn)行處理(通過分時),那么它就不是并行的。
舉一個例子,編寫一個最簡單的順序結(jié)構(gòu)程序輸出"Hello World",它就是非并發(fā)的,如果在程序中增加多線程,每個線程打印一個"Hello World",那么這個程序就是并發(fā)的。如果運行時只給這個程序分配單個CPU,這個并發(fā)程序還不是并行的,需要部署在多核處理器上,才能實現(xiàn)程序的并行。
三、幾種不同的多線程模型
用戶線程與內(nèi)核級線程
線程的實現(xiàn)可以分為兩類:用戶級線程(User-LevelThread, ULT)和內(nèi)核級線程(Kemel-LevelThread, KLT)。用戶線程由用戶代碼支持,內(nèi)核線程由操作系統(tǒng)內(nèi)核支持。
多線程模型
多線程模型即用戶級線程和內(nèi)核級線程的不同連接方式。
(1)多對一模型(M : 1)
將多個用戶級線程映射到一個內(nèi)核級線程,線程管理在用戶空間完成。 此模式中,用戶級線程對操作系統(tǒng)不可見(即透明)。
優(yōu)點: 這種模型的好處是線程上下文切換都發(fā)生在用戶空間,避免的模態(tài)切換(mode switch),從而對于性能有積極的影響。
缺點:所有的線程基于一個內(nèi)核調(diào)度實體即內(nèi)核線程,這意味著只有一個處理器可以被利用,在多處理器環(huán)境下這是不能夠被接受的,本質(zhì)上,用戶線程只解決了并發(fā)問題,但是沒有解決并行問題。如果線程因為 I/O 操作陷入了內(nèi)核態(tài),內(nèi)核態(tài)線程阻塞等待 I/O 數(shù)據(jù),則所有的線程都將會被阻塞,用戶空間也可以使用非阻塞而 I/O,但是不能避免性能及復(fù)雜度問題。
(2) 一對一模型(1:1)
將每個用戶級線程映射到一個內(nèi)核級線程。
每個線程由內(nèi)核調(diào)度器獨立的調(diào)度,所以如果一個線程阻塞則不影響其他的線程。
優(yōu)點:在多核處理器的硬件的支持下,內(nèi)核空間線程模型支持了真正的并行,當(dāng)一個線程被阻塞后,允許另一個線程繼續(xù)執(zhí)行,所以并發(fā)能力較強(qiáng)。
缺點:每創(chuàng)建一個用戶級線程都需要創(chuàng)建一個內(nèi)核級線程與其對應(yīng),這樣創(chuàng)建線程的開銷比較大,會影響到應(yīng)用程序的性能。
(3)多對多模型(M : N)
內(nèi)核線程和用戶線程的數(shù)量比為 M : N,內(nèi)核用戶空間綜合了前兩種的優(yōu)點。
這種模型需要內(nèi)核線程調(diào)度器和用戶空間線程調(diào)度器相互操作,本質(zhì)上是多個線程被綁定到了多個內(nèi)核線程上,這使得大部分的線程上下文切換都發(fā)生在用戶空間,而多個內(nèi)核線程又可以充分利用處理器資源。
四、goroutine機(jī)制的調(diào)度實現(xiàn)
goroutine機(jī)制實現(xiàn)了M : N的線程模型,goroutine機(jī)制是協(xié)程(coroutine)的一種實現(xiàn),golang內(nèi)置的調(diào)度器,可以讓多核CPU中每個CPU執(zhí)行一個協(xié)程。
理解goroutine機(jī)制的原理,關(guān)鍵是理解Go語言scheduler的實現(xiàn)。
調(diào)度器是如何工作的
Go語言中支撐整個scheduler實現(xiàn)的主要有4個重要結(jié)構(gòu),分別是M、G、P、Sched, 前三個定義在runtime.h中,Sched定義在proc.c中。
- Sched結(jié)構(gòu)就是調(diào)度器,它維護(hù)有存儲M和G的隊列以及調(diào)度器的一些狀態(tài)信息等。
- M結(jié)構(gòu)是Machine,系統(tǒng)線程,它由操作系統(tǒng)管理的,goroutine就是跑在M之上的;M是一個很大的結(jié)構(gòu),里面維護(hù)小對象內(nèi)存cache(mcache)、當(dāng)前執(zhí)行的goroutine、隨機(jī)數(shù)發(fā)生器等等非常多的信息。
- P結(jié)構(gòu)是Processor,處理器,它的主要用途就是用來執(zhí)行g(shù)oroutine的,它維護(hù)了一個goroutine隊列,即runqueue。Processor是讓我們從N:1調(diào)度到M:N調(diào)度的重要部分。
- G是goroutine實現(xiàn)的核心結(jié)構(gòu),它包含了棧,指令指針,以及其他對調(diào)度goroutine很重要的信息,例如其阻塞的channel。
Processor的數(shù)量是在啟動時被設(shè)置為環(huán)境變量GOMAXPROCS的值,或者通過運行時調(diào)用函數(shù)GOMAXPROCS()進(jìn)行設(shè)置。Processor數(shù)量固定意味著任意時刻只有GOMAXPROCS個線程在運行g(shù)o代碼。
參考這篇傳播很廣的博客:http://morsmachine.dk/go-scheduler
我們分別用三角形,矩形和圓形表示Machine Processor和Goroutine。
在單核處理器的場景下,所有g(shù)oroutine運行在同一個M系統(tǒng)線程中,每一個M系統(tǒng)線程維護(hù)一個Processor,任何時刻,一個Processor中只有一個goroutine,其他goroutine在runqueue中等待。一個goroutine運行完自己的時間片后,讓出上下文,回到runqueue中。 多核處理器的場景下,為了運行g(shù)oroutines,每個M系統(tǒng)線程會持有一個Processor。
在正常情況下,scheduler會按照上面的流程進(jìn)行調(diào)度,但是線程會發(fā)生阻塞等情況,看一下goroutine對線程阻塞等的處理。
線程阻塞
當(dāng)正在運行的goroutine阻塞的時候,例如進(jìn)行系統(tǒng)調(diào)用,會再創(chuàng)建一個系統(tǒng)線程(M1),當(dāng)前的M線程放棄了它的Processor,P轉(zhuǎn)到新的線程中去運行。
runqueue執(zhí)行完成
當(dāng)其中一個Processor的runqueue為空,沒有g(shù)oroutine可以調(diào)度。它會從另外一個上下文偷取一半的goroutine。
五、對并發(fā)實現(xiàn)的進(jìn)一步思考
Go語言的并發(fā)機(jī)制還有很多值得探討的,比如Go語言和Scala并發(fā)實現(xiàn)的不同,Golang CSP 和Actor模型的對比等。
了解并發(fā)機(jī)制的這些實現(xiàn),可以幫助我們更好的進(jìn)行并發(fā)程序的開發(fā),實現(xiàn)性能的最優(yōu)化。
關(guān)于三種多線程模型,可以關(guān)注一下Java語言的實現(xiàn)。
我們知道Java通過JVM封裝了底層操作系統(tǒng)的差異,而不同的操作系統(tǒng)可能使用不同的線程模型,例如Linux和windows可能使用了一對一模型,solaris和unix某些版本可能使用多對多模型。JVM規(guī)范里沒有規(guī)定多線程模型的具體實現(xiàn),1:1(內(nèi)核線程)、N:1(用戶態(tài)線程)、M:N(混合)模型的任何一種都可以。談到Java語言的多線程模型,需要針對具體JVM實現(xiàn),比如Oracle/Sun的HotSpot VM,默認(rèn)使用1:1線程模型。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- golang實現(xiàn)并發(fā)數(shù)控制的方法