一、動態(tài)棧(Growable Stacks)
棧(stack):當前正在被調(diào)用或被掛起(旨在調(diào)用其他函數(shù))的函數(shù)的內(nèi)部變量(local variables)被存放在棧中。
| 棧 | 操作系統(tǒng)(OS)線程 | goroutine |
|---|---|---|
| 類型 | 固定 | 動態(tài) |
| 大小 | 通常2MB | 2KB~1GB |
| 特征 | 若線程所需內(nèi)存較少,會造成浪費,比如需要大量功能簡單的線程時線程數(shù)量會受到限制; 若需要復(fù)雜或深層的遞歸調(diào)用,則可能會不夠用。 |
一般從2KB大小的棧開始生命周期,根據(jù)實際需要動態(tài)伸縮。 |
二、Goroutine調(diào)度(Scheduling)
OS內(nèi)核調(diào)度
OS線程由OS內(nèi)核調(diào)度,每幾毫秒,一個硬件計時器會中斷處理器,這會調(diào)用(invoke)一個叫scheduler的內(nèi)核函數(shù)進行線程調(diào)度
scheduler函數(shù):1. 掛起當前執(zhí)行的線程并保存其寄存器的內(nèi)容到內(nèi)存中;2. 選擇出下一個準備執(zhí)行的線程,從內(nèi)存中恢復(fù)其寄存器的數(shù)據(jù),恢復(fù)執(zhí)行該線程的現(xiàn)場并開始執(zhí)行線程。
缺點:線程的切換需要完整的上下文切換[1](包括 a. 保存一個用戶線程的狀態(tài)到內(nèi)存;b. 恢復(fù)另一個線程的到寄存器;c. 更新調(diào)度器的數(shù)據(jù)結(jié)構(gòu))。這個操作很慢,因為相關(guān)數(shù)據(jù)在內(nèi)存中的位置往往較分散(poor locality),需要大量的內(nèi)存訪問。
Go的運行期調(diào)度(runtime scheduling)
Go runtime包含自己的scheduler,其使用一種名為m:n調(diào)度(m:n scheduling)的技術(shù)。
m:n調(diào)度(m:n scheduling):在n個OS線程上多工(multiplex)[2]調(diào)度m個goroutine
Go runtime的scheduler的工作內(nèi)容與OS的scheduler是類似的,不過只關(guān)注單獨(single)Go程序中的goroutines(即某一個Go程序中的goroutine只會與該程序中的其他goroutine交換)。
何時觸發(fā)調(diào)度:不由硬件計時器觸發(fā),而是由一些Go語言的結(jié)構(gòu)(constructs)隱式地觸發(fā)。例如當一個goroutine調(diào)用了time.Sleep(),或者被channel或者mutex操作阻塞時,調(diào)度器會使其進入休眠并開始執(zhí)行另一個goroutine。直到Sleep()時間結(jié)束或者channel或mutex的阻塞解除后再喚醒第一個goroutine。
關(guān)于Go的線程調(diào)度是搶占式的還是協(xié)同式的:
go在1.4版本加入了搶占式邏輯,之前的版本確實是非搶占式的,1.4以后版本的rutime sysmon會定期喚醒作系統(tǒng)狀態(tài)檢查,即使P處于阻塞的系統(tǒng)調(diào)也能被調(diào)用,不至于餓死,而且還檢查某個G是否過多的占用了的cpu時間,并在某個時刻剝奪其cpu運行時間。[3]
優(yōu)點:這種調(diào)度方式不需要進入內(nèi)核的上下文,所以調(diào)度一個goroutine比調(diào)度一個線程代價要低得多。
三、GOMAXPROCS
GOMAXPROCS變量
- 決定會有多少個操作系統(tǒng)的線程同時執(zhí)行Go的代碼(m:n調(diào)度中的n)
- 默認值是CPU的核心數(shù)
- 休眠中或者通信(communication)阻塞中的goroutine不需要對應(yīng)的系統(tǒng)線程
- 在被I/O或其他系統(tǒng)調(diào)用阻塞時,或調(diào)用非Go語言函數(shù)時,goroutine是需要一個對應(yīng)的操作系統(tǒng)線程的,不過GOMAXPROCS不需要考慮這些情況
- 修改方法:1. 修改環(huán)境變量
GOMAXPROCS=n;2. 運行時調(diào)用runtime.GOMAXPROCS(n)函數(shù)
最佳線程數(shù)
最佳線程數(shù)與CPU核心數(shù)的關(guān)系并沒有定論,得具體情況具體分析,原則是活躍線程數(shù)為 CPU(核)數(shù)時最佳[4]。
介紹CPU時有的會提到n核m線程,這里的m可以理解為CPU中單一核心支持的最大并行(parallel)線程數(shù)。
描述CPU時所說的多線程:Intel的超線程(Hyper-threading)[5]技術(shù),旨在充分利用CPU核心中的資源。簡言之,假設(shè)有兩個線程A和B,若A和B都(僅)需要核心中50%的某資源進行運算,則在應(yīng)用了超線程的核心中,A和B兩個線程可以同時進行運算(微觀上的并行)。不過使用超線程技術(shù)并不一定意味著性能的提升,在一些情況下,性能甚至可能下降[6]。
四、Goroutine沒有識別符(Identity)
識別符帶來的問題
線程的身份信息會使得做一個抽象化的thread-local storage(線程本地存儲,多線程編程中不希望其它線程訪問的內(nèi)容)變得很容易,比如一個以線程的id為key的map。而這可能會導(dǎo)致一個函數(shù)的行為不是僅由其參數(shù),而還由其運行在的線程所決定。
Go鼓勵更簡單的編程風格
影響函數(shù)行為的參數(shù)(parameters)都應(yīng)被顯式地指出。這樣不僅使程序變得更易讀,而且會讓我們向一些給定的函數(shù)分配子任務(wù)時不用擔心其身份信息會影響執(zhí)行結(jié)果。
1/16/18