這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文內容
- 進程
- 線程
- 協程
- Go 中的 goroutine
- 參考資料
最近,看一些文章,提到“協程”的概念,心想,進程,線程,協程,前兩個很容易,任何一本關於作業系統的書都有說,開發時也經常用,但是協程呢?之前也遇到這個詞,但是今天,查了一下資料。下面是一個人的總結。
電話面試被問到go的協程……雖然用 python 時候在 Eurasia 和 eventlet 裡瞭解過協程,但自己對協程的概念也就是輕量級線程,還有一個很通俗的紅綠燈說法:線程要守規則,協程看到紅燈但是沒有車仍可以通行。現在總結各個資料,從個人理解上說明下進程、線程、輕量級進程、協程,以及 go 中的 goroutine 那些事兒。
進程
作業系統中最核心的概念是進程,分布式系統中最重要的問題是處理序間通訊。
進程是“程式執行的一個執行個體”,擔當分配系統資源的實體。進程建立必須分配一個完整的獨立地址空間。
進程切換隻發生在核心態,兩步:
- 切換頁全域目錄以安裝一個新的地址空間;
- 切換核心態堆棧和硬體上下文。
另一種說法類似:
- 儲存CPU環境(寄存器值、程式計數器、堆棧指標);
- 修改記憶體管理單元MMU的寄存器;
- 轉換後備緩衝器TLB中的地址轉換緩衝內容標記為無效。
線程
線程是進程的一個執行流,獨立執行它自己的程式碼。線程(thread)是作業系統能夠進行運算調度的最小單位。
線程上下文一般只包含CPU上下文及其他的線程管理資訊。線程建立的開銷主要取決於為線程堆棧的建立而分配記憶體的開銷,這些開銷並不大。線程環境切換發生在兩個線程需要同步的時候,比如進入共用資料區段。切換隻CPU寄存器值需要儲存,並隨後用將要切換到的線程的原先儲存的值重新載入到CPU寄存器中去。
使用者級線程主要缺點在於對引起阻塞的系統調用的調用會立即阻塞該線程所屬的整個進程。核心實現線程則會導致線程環境切換的開銷跟進程一樣大,所以折衷的方法是輕量級進程(Lightweight)。在 Linux 中,一個線程組基本上就是實現了多線程應用的一組輕量級進程。進程中存在使用者線程、輕量級進程、核心線程。
語言層面實現輕量級進程的比較少,stackless python,erlang 支援,java 並不支援。
協程
協程是輕量級的線程,一個進程可輕鬆建立數十萬計的協程。從維基百科上看,從Knuth老爺子的基本演算法卷上看“子程式其實是協程的特例”。子程式是什嗎?子程式(Subroutine, procedure, function, routine, method, subprogram),就是函數嘛!所以協程也沒什麼了不起的,就是更一般意義的程式組件,那你記憶體空間夠大,建立多少個函數還不是隨你嗎?
協程可以通過yield來調用其它協程。通過yield方式轉移執行權的協程之間不是調用者與被調用者的關係,而是彼此對稱、平等的。協程的起始處是第一個進入點,在協程裡,返回點之後是接下來的進入點。子常式的生命期遵循後進先出(最後一個被調用的子常式最先返回);相反,協程的生命期完全由他們的使用的需要決定。
線程和協程的區別?
一旦建立完線程,你就無法決定他什麼時候獲得時間片,什麼時候讓出時間片了,你把它交給了核心。而協程編寫者,一是可控的切換時機,二是很小的切換代價。從作業系統有沒有調度權上看,協程就是因為不需要進行核心態的切換,所以會使用它,會有這麼個東西。賴永浩和dccmx 這個定義我覺得相對準確 協程-使用者態的輕量級的線程。(http://blog.dccmx.com/2011/04/coroutine-concept/)
為什麼要用協程?
協程有助於實現:
- 狀態機器:在一個子常式裡實現狀態機器,這裡狀態由該過程當前的出口/進入點確定;這可以產生可讀性更高的代碼。
- 角色模型:並行的角色模型,例如電腦遊戲。每個角色有自己的過程(這又在邏輯上分離了代碼),但他們自願地向順序執行各角色過程的中央調度器交出控制(這是合作式多任務的一種形式)。
- 產生器:它有助於輸入/輸出和對資料結構的通用遍曆。
| 語言/平台 |
實現版本 |
協程名稱 |
備忘 |
| GoLang |
原生支援 |
goroutine |
|
| Erlang |
原生支援 |
process |
函數式編程 |
| Scala |
原生支援 |
actor |
函數式編程 |
| Python |
2.5 版本後 |
coroutine |
官方 Python 不完全實現,Stackless Python 支援 |
| Perl |
6.0 版本後 |
coroutine |
|
| Ruby |
1.9 版本後 |
fiber |
|
| Lua |
原生支援 |
coroutine |
|
| C# |
.Net 2.0 版本後 |
fiber |
|
Go 中的 goroutine
go中的Goroutine, 普遍認為是協程的go語言實現。《Go語言編程》中說goroutine是輕量級線程(即協程coroutine, 原書90頁). 在第九章進階話題中, 作者又一次提到, "從根本上來說, goroutine就是一種go語言版本的協程(coroutine)" (原書204頁). 但作者Rob Pike並不這麼說。
“一個Goroutine是一個與其他goroutines 並發運行在同一地址空間的Go函數或方法。一個啟動並執行程式由一個或更多個goroutine組成。它與線程、協程、進程等不同。它是一個goroutine。”
在棧實現上,它的編譯器分支下的實現gccgo是線程pthread,6g上是多工threads(6g/8g/5g分別代表64位、32位及Arm架構編譯器)
infoQ一篇文章介紹特性也說道: goroutine是Go語言運行庫的功能,不是作業系統提供的功能,goroutine不是用線程實現的。具體可參見Go語言源碼裡的pkg/runtime/proc.c
老趙認為goroutine就是把類庫功能放進了語言裡。
goroutine的並發問題:goroutine在共用記憶體中運行,通訊網路可能死結,多線程問題的調試糟糕透頂等等。一個比較好的建議規則:不要通過共用記憶體通訊,相反,通過通訊共用記憶體。
並行與並發區別:
並行是指程式的運行狀態,要有兩個線程正在執行才能算是Parallelism;並髮指程式的邏輯結構,Concurrency則只要有兩個以上線程還在執行過程中即可。簡單地說,Parallelism要在多核或者多處理器情況下才能做到,而 Concurrency則不需要。(http://stackoverflow.com/questions/1050222/concurrency- vs-parallelism-what-is-the-difference)
參考資料
- https://zh.wikipedia.org/wiki/Go
- https://zh.wikipedia.org/wiki/進程
- https://zh.wikipedia.org/wiki/線程