This page looks best with JavaScript enabled

Goroutine 為啥那麼快 (Scheduling in Go) Part I

 ·  ☕ 4 min read

關於這系列文

之前看了 Ardan labs 寫的下面這個系列文還滿不錯的
https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html
主要是在講 Goroutine 和一般我們看到的 thread 比起來到底厲害在哪裡,也舉了一些範例讓讀者知道 Goroutine 用在哪裡才是正確的

這系列文章就是拿來記個重點XD
他的系列文分為三篇,我也就分三篇記

Part I: OS Scheduler

這個章節不講 Goroutine 講一些 OS 要知道的預備知識,也不會涉及太多細節。

Process、Thread、Coroutine

下圖是 Learning Concurrency in Kotlin 這本書裡面的一張圖,解釋了 Process、Thread、Coroutine 的關係。

process_thread_coroutine

Process

Process 是 OS 分配資源的基本單位 (記憶體之類的)
簡單來說把一個程式 (program) 跑起來就是一個 process,一開始 process 內會有一條初始的 thread 來跑主要程式,這條 thread 可以建立出更多 thread,每個 process 裡面可以裝一堆 thread。

Thread

Thread (kernel thread) 是 OS 分配 CPU 執行時間的單位,只能存在於 process 內,常被叫做 light-weight process。
在大多我們使用的作業系統中,thread 的排程都是 preemptive (可搶佔),代表著一條 thread 就算現在在 CPU 上執行,隨時也可能被各種原因換下來,基本上都是由 OS 來管理的。

  • CPU 從正在執行 thread A 的任務鍾,換到執行 thread B 的任務,這個行為叫做 context switch

Thread 之間有哪些共用與不共用的東西這邊就不多講網路上很多文章可以查到。
提供兩篇文章參考
https://medium.com/@yovan/os-process-thread-user-kernel-%E7%AD%86%E8%A8%98-aa6e04d35002
https://www.itread01.com/content/1546525452.html

大學上的 OS 課程大概就是介紹了上面那些東西而已。

Coroutine

再來介紹的 coroutine (goroutine 就是一種),基本上作業系統不知道有這個東西的存在,OS 只負責執行 thread 上面的一堆任務而已。coroutine 都是由我們使用的程式語言來處理,而每個程式語言提供的 coroutine 運作方式都有些不同。
coroutine 也叫做 light-weight thread 或者是 user-level thread,是跑在 kernel thread 上的一堆東西。

基本上 coroutine 做的事情就是可以讓 function 執行到一半中斷,把 CPU 時間讓給別的 coroutine 來執行他們的工作
舉個簡單的應用例子,當 Coroutine A 做事情做到一半,發現要等待讀取檔案或是等一些網路傳輸的資料才可以執行下一步,而這些工作又是不需要 CPU 的,那 coroutine A 就可以把 CPU 的時間讓給下一個 coroutine B,等待需要的東西回來之後再從中斷點繼續工作。

與 thread 相比, coroutine 的 size 更小,像是在 Go 創建一個新的 goroutine 就只花費幾 KB,一條 java thread 卻要到 1~2 MB。
而上面那種切換 coroutine 的行為其實是種 coroutine context switch。如果 coroutine A 與 coroutine B 是在同一個 thread,切換時時 OS 並不會發現,也不會出現 thread 的 context switch。
Coroutine 的 context switch 與 thread 的 比起來速度也快很多,要儲存與載入的資料量相差很大。

這部分之後講 goroutine 時會再提到。

越多 Thread 真的會讓程式跑越快嗎 ?

答案是不會
這部分先不討論 coroutine,單純就 multi-thread 的程式來說明

進入正題前必須先理解,一個 thread 會做的工作主要分為這兩種

  • CPU-bound
    指的是 CPU 大部分時間都很忙的工作類型。譬如計算一個超大 int 陣列的總和,CPU 就是一直做加法,超忙。

  • IO-bound
    CPU 常常會沒事做,可能都在等著硬碟讀完檔案的通知,或者網路傳輸結束的通知等等。
    簡單的例子像是把一堆檔案一個一個讀進來,做一點點修改再寫回去,就會花很多時間在讀寫檔案,CPU 沒什麼事情好做。

基本上 context switch 在 IO-bound 的工作上是好的,當一個任務需要等待 IO 時,便換下來讓需要 CPU 的人用,避免讓 CPU idle。
在 CPU-bound 的工作上 context switch 需要多花時間,只會拖慢完成全部任務的時間而已,但有時為了讓使用者覺得每個任務都有在進行,context switch 是必須的。

回來講 thread 數量的問題
當機器有 n 個 core,代表同時最多也只能執行 n 個 thread,若 thread 數量少於 n 的話,必然會有 core 沒事情做,顯然是不好。
如果 thread 數量大於 n 太多,那就會常常在執行 context switch 而浪費時間,且 thread 會佔用不少記憶體。

要取得平衡必須知道自己的系統是在做 CPU-bound 還是 IO-bound 的任務多,前者 thread 少點 (但還是要 >= n),後者 thread 多點。
當寫一個有 database 的 web service,最常使用的 thread 數量是 n * 3,就是一個大家的經驗法則而已。

總結,我們希望每個 CPU 無時無刻都在工作,才能有最大的產出
當一個 thread 等待 IO 時就把他換下來,讓 CPU 繼續工作
當一個 thread 正在被 CPU 執行時,我們就盡量不要打擾他

接著 Part II 會來介紹 Go 的 scheduler 是怎麼運作的,他為什麼讓 Goroutine 比 thread 還有效率

Share on

Marko Peng
WRITTEN BY
Marko Peng
Good man