程式和執行緒
當執行一個應用程式的時候,作業系統會給這個應用程式啟動一個程式。我們可以將程式看作一個包含應用程式在執行中需要用到和維護的各種資源的容器。一個程式至少包含一個執行緒,這個執行緒就是主執行緒。作業系統會排程執行緒到不同的CPU上執行,這個CPU不一定就是程式所在的CPU。
- 程式:資源的所有權
- 執行緒:執行和排程的基本單位
- 同一程式下的各個執行緒共享資源,但暫存器、棧、PC不共享
Go排程
基本術語
Go Runtime管理排程,垃圾收集和Goroutine的執行時環境。這裡我們只談排程器。
Runtime排程器通過把Goroutine繫結到作業系統執行緒來執行它們。Goroutine可以看作是輕量級的執行緒。每個Goroutine用G來表示,它包含了用來跟蹤棧的欄位和當前狀態。
Runtime跟蹤每一個G並且把它們繫結到P(Logical Processor)。P可以被看作抽象資源或者上下文,作業系統執行緒用M來表示(OS Thread)需要獲取它以便來執行G。你可以通過runtime.GOMAXPROCS(numLogicalProcessors)
來調整P(Logical Processors)。
G-P-M排程模型
在Go 1.1中實現了G-P-M排程模型和work stealing演算法,這個模型一直沿用至今:
每一個G(Goroutine)需要繫結到P(Logical Processor)才能排程執行;就像每一個User-level Thread繫結到Kernel Thread才能被排程執行。
M、P 和 G 之間的互動
建立一個Goroutine並準備執行,這個Goroutine會被放到排程器的Global佇列中。然後排程器就將這些佇列中的Goroutine分配給一個P(Logical Processor),並放到這個P對應的Local佇列中。Local佇列中的Goroutine會一直等待直到自己被分配的P執行。
如果正在執行的Goroutine被一個系統呼叫阻塞,如開啟一個檔案。當這種情況發生時,M2(OS Thread)和Goroutine會從P0(Logical Processor)上分離,這個M2會一直阻塞直到系統呼叫返回(見上圖右邊)。與此同時,這個P0就失去了用來執行的M2。所以,排程器會建立一個新的M3,並將其繫結到該P0上。之後,排程器會從Local佇列中選擇另外一個Goroutine執行。一旦剛才阻塞的系統呼叫執行完畢並返回,對應的Goroutine會放回到Local佇列。
併發(Concurrency)不是並行(Parallelism)
上圖解釋了併發和並行的區別
併發 – 一個咖啡機同時服務兩個佇列
並行 – 兩個咖啡機服務同時服務兩個佇列
如果希望讓Goroutine並行,必須使用多於一個P(Logical Processor)。當有多個P時,排程器會將Goroutine平等分配到每個P上。這會讓Goroutine在不同M(OS Thread)上執行。不過要想真的實現並行的效果,使用者需要讓自己的程式執行在有多個物理處理器的機器上。
參考資料。