翻譯原文鏈接? ?轉(zhuǎn)帖/轉(zhuǎn)載請注明出處
英文原文鏈接 發(fā)表于2014/02/24
Go語言
如果你剛剛接觸Go語言从橘,或者說你并不理解“并發(fā)不等于并行”這句話的含義纪岁,那么Rob Pike的講座值得一看(在youtube上)代嗤。這個視頻有30分鐘長涵妥,我保證花30分鐘看這段視頻是非常值得的促脉。
這里摘錄一段他提到的并發(fā)和并行之間的區(qū)別:“當大家聽到并發(fā)這個詞的時候悦污,他們往往想到的是并行。并行是一個相關悼沈,但卻完全不同的概念贱迟。當我們編程的時候姐扮,并發(fā)指的是多個獨立運行的進程絮供,而并行是指同時運行的多個計算衣吠。并發(fā)是為了一下子處理很多東西。并行是為了同時做很多事情壤靶「壳危” [1] (注:這里的概念有點繞。其實本質(zhì)的區(qū)別在“同時”這個詞上贮乳。并行強調(diào)的時候幾個進程同時進行忧换。而并發(fā)指的是運行多個進程,但這些進程并不需要同時被執(zhí)行向拆。它們可以是被調(diào)度在同一個CPU分時運行的亚茬。)
Go為我們寫并發(fā)程序提供了便利。它提供了goroutine以及它們之間通信的功能浓恳。在這里我們主要討論goroutine刹缝。
Goroutine和線程的區(qū)別
Go語言使用的是goroutine,而像Java這樣的語言大多使用線程颈将。它們之間的區(qū)別是什么呢梢夯?讓我們從三個方面來看看它們的區(qū)別:內(nèi)存占用,創(chuàng)建和銷毀晴圾,以及切換開銷颂砸。
內(nèi)存占用
創(chuàng)建一個goroutine不需要太多的內(nèi)存 - 大概2KB左右的棧空間死姚。如果需要更多的椚伺遥空間,就從堆里分配額外的空間來使用都毒。[2][3] 新創(chuàng)建的線程會占用1MB的內(nèi)存空間(這大約是goroutine的500倍)色罚。這還不包括守護頁(guard page)的空間。守護頁是用來保護線程之間的內(nèi)存空間不會被相互竄改温鸽。[7]
因此一個處理很多請求的服務可以為每個請求創(chuàng)建一個goroutine保屯。但是如果為每個請求去創(chuàng)建一個線程,那么它很快就會碰到OutOfMemoryError涤垫。這不是Java獨有的問題姑尺,任何使用操作系統(tǒng)線程作為主要并發(fā)手段的編程語言都會碰到這個問題。
創(chuàng)建和銷毀的開銷
線程需要從操作系統(tǒng)里請求資源并在用完之后釋放回去蝠猬,因此創(chuàng)建和銷毀線程的開銷非常大切蟋。為了避免這些開銷,我們通常的做法是維護一個線程池榆芦。Goroutine的創(chuàng)建和銷毀是由運行環(huán)境(runtime)完成的柄粹。這些操作的開銷就比較小喘鸟。Go語言不支持手工管理goroutine。
切換開銷
當一個線程阻塞的時候驻右,另外一個線程需要被調(diào)度到當前處理器上運行什黑。線程的調(diào)度是搶占式的(preemptively)。當切換一個線程的時候堪夭,調(diào)度器需要保存/恢復所有的寄存器愕把。這包括16個通用寄存器,程序指針(program counter)森爽,棧指針(stack pointer)恨豁,段寄存器(segment registers)和16個XMM寄存器,浮點協(xié)處理器狀態(tài)爬迟,16個AVX寄存器橘蜜,所有的特殊模塊寄存器(MSR)等。當在線程間快速切換的時候這些開銷就變得非常大了付呕。
Goroutine的調(diào)度是協(xié)同合作式的(cooperatively)计福。當切換goroutine的時候,調(diào)度器只需要保存和恢復三個寄存器 - 程序指針凡涩,棧指針和DX棒搜。切換的開銷就小多了。
前面已經(jīng)談到了活箕,goroutine的數(shù)目會比線程多很多力麸,但這并不影響切換的時間。有兩個原因:第一育韩,只有可以運行的goroutine才會被考慮克蚂,正在阻塞的goroutine會被忽略。第二筋讨,現(xiàn)代的調(diào)度器的復雜度都是O(1)的埃叭。這意味著選擇的數(shù)目(線程或者是goroutine)不會影響切換的時間。[5]
Goroutine的運行
前面談到悉罕,運行環(huán)境負責goroutine的創(chuàng)建赤屋,調(diào)度和銷毀。運行環(huán)境被會分配一些線程壁袄,用來運行所有的goroutine类早。在任何一個時間點,每個線程只會運行一個goroutine嗜逻。如果一個goroutine被阻塞涩僻,另外一個goroutine會來替換它在對應的線程上運行。[6]
因為goroutine的調(diào)度是協(xié)同合作式的,如果一個goroutine不停的循環(huán)逆日,其它的goroutine就沒有機會被調(diào)度運行了嵌巷。在Go 1.2里,這個問題的解決辦法是在調(diào)用一個函數(shù)的時候去偶爾觸發(fā)Go的調(diào)度器室抽。這樣一個循環(huán)里如果調(diào)用了沒有被內(nèi)聯(lián)的函數(shù)搪哪,它就可以被搶占了。
Goroutine的阻塞
Goroutine是廉價的狠半,在下面這些阻塞情況下它們也不會造成運行的線程被阻塞:
- 網(wǎng)絡收發(fā)
- 睡眠
- channel操作
- sync包里的一些會阻塞的基本操作
即使創(chuàng)建了成千上萬的goroutine并且大多數(shù)被阻塞了噩死,也不會造成太多的系統(tǒng)資源浪費颤难。因為運行環(huán)境會調(diào)度另外的goroutine來運行神年。
簡而言之,goroutine是對線程的輕量化抽象行嗤。Go語言的程序員不需要直接操作線程已日。與此同時操作系統(tǒng)也不知道goroutine的存在。從操作系統(tǒng)的角度來看栅屏,一個Go程序有點像一個事件驅(qū)動的C程序飘千。[5]
線程和處理器
雖然我們不能直接控制運行環(huán)境創(chuàng)建多少線程,我們可以設置程序使用的處理器核數(shù)栈雳。這是通過調(diào)用runtime.GOMAXPROCS(n)函數(shù)設置GOMAXPROCS變量來實現(xiàn)的护奈。(注:也可以通過直接設置環(huán)境變量來控制)。增加處理器核數(shù)并不意味著程序性能的提高哥纫。這取決于程序本身的設計霉旗。你的程序需要用到多少個內(nèi)核數(shù)可以用剖析(profiling)工具來找到答案。
結束語
和其它語言類似蛀骇,避免多個goroutine同時訪問一個共享資源是非常重要的厌秒。goroutine之間,最好是用channel來傳輸數(shù)據(jù)擅憔。有興趣的可以讀一讀“do not communicate by sharing memory; instead, share memory by communicating”鸵闪。
最后,我強烈推薦讀一下C. A. R. Hoare寫的“Communicating Sequential Processes”暑诸。他是個天才蚌讼。在這篇論文(1978年發(fā)表的)里,他預測了單核處理器性能最終會遇到瓶頸个榕,然后芯片制造商們會增加處理器的內(nèi)核數(shù)篡石。他的思想對Go語言的設計影響深遠。
參考文獻
1. Concurrency is not parallelism by Rob Pike
3. Goroutine stack size was decreased from 8kB to 2kB in Go 1.4
4. Goroutine stacks became contiguous in Go 1.3
5. Scheduling of goroutines by Dmitry Vyukov
6. Analysis of the Go runtime scheduler
7. 5 things that make Go fast by Dave Cheney