This page looks best with JavaScript enabled

Scheduling in Go Part III

 ·  ☕ 4 min read

Part III: Examples

Goroutine 的運作原理其實在前面兩篇就有很好的解釋拉

這個部分主要是給一些實際的例子,會介紹一下平行與並行適合的是哪些情況,Goroutine 到底什麼時候開多才可以讓處理事情的效率增加

接下來簡短回憶一下 Part I 寫過的這兩個重點

1. Parallelism vs Concurrency

  • Parallelism 平行
    把不同的任務放到不同的 core 上執行,同一個時間點同時執行不同的任務,需要有多於一個的 core 才可辦到,基本上效能會比只使用一個 core 好,除非有什麼互相卡資源的問題,等等的範例會介紹一些例外 -> 如下圖的 G1、G2 就是平行執行

  • Concurrency 並行
    同一時間點執行一個任務,不同的任務必須輪流(順序不確定),且結果與依序(sequential)執行所有任務相同,然後希望效能會比依序執行來得好 -> 如下圖的 G1 G4 G6 與 G2 G3 G5

2. CPU Bound 與 I/O Bound 的任務類型

  • CPU Bound:
    I/O 設備效能相對好。需要 core 的計算能力更強,或著更多 core 才可以有效加速這一些任務們的執行
    此種類型大部分 CPU 使用率都接近滿載

  • I/O Bound:
    任務們被 CPU 執行到一半常常需要等待一些東西 (讀檔案、網路傳輸資料),才能再回來讓 CPU 繼續計算,如果網路傳輸速度或是硬碟讀取速度加快,可以大幅加速這些任務
    CPU 的 Loading 大部分不高

在 Part I 講解了 context switch 在 I/O Bound 的任務上是好的,需要等待 I/O 時先把 CPU 讓給其他人使用,等到 I/O 結果時再回到佇列中等待被 CPU 計算,避免 CPU 空閒沒事做。
在 Part II 時我們講解了 Goroutine 相比普通的 OS thread 優勢是在它的記憶體用量與 context switch 的速度。

所以我們知道 Goroutine 應該會適合用在 I/O Bound 的任務上,
文章內也舉了幾個實際的例子,我就不把全部都 copy 過來了,下面簡單的介紹一下

Adding numbers

程式碼在 Listing 1 內,目的是要把一個陣列裡的數字全部加起來,任務都是需要做計算,沒有等待 I/O
分為兩種做法

  1. 一種是 sequential 的方法,用一個 Gorutine 把全部加起來
  2. 第二種是 concurrent,有八個 Goroutine 各自把八分之一的陣列加總,最後再加在一起

Listing 4 列出來使用一個 core 的結果,顯然 sequential 方法較快,
在只有一個 core 的情況下,這種 CPU Bound 的任務,多了一些 Goroutine 只是會發生多餘的 context switch 導致總時間拉長而已。

Listing 5 列出了使用八個 core 的結果,每個 core 上各有一個 Goroutine,計算能力大幅增加,速度應該會提升
實際上速度快了 40% 左右,為什麼不是八倍呢?
大概是程式一開始只有一個 thread,要去開啟剩下七個 os thread 跑在不同的 core 上造成的延遲
可以看到 BenchmarkConcurrent-8 3362643 ns/op,每加總一次也才花了 3ms 左右,thread 的創建搞不好就佔了很大一部分,如果把數字陣列的長度拉大,計算時間更久,效能應該會更接近八倍才對

Reading Files

文章中在這部分給的範例我覺得滿奇怪的
在一個 Core 上使用多個 Goroutine (thread) 的優勢是發現 G 被 block 住時可以讓出 CPU,盡可能使用 CPU,使 I/O 等待與 CPU 運算可以同時進行

但範例中使用 time.Sleep 來模擬讀檔案
他這樣的做法應該可以使 CPU 運算部分 (strings.Contains 與其他 overhead) 和 sleep 部分同時進行沒錯,可能有達到一些加速的效果

但正常來說讀檔案是會互相干擾的,例如在只有一個硬碟的狀況下,Goroutine 再多,硬碟的效能也是相同,大家應該要排隊讀,但 Sleep 的狀況卻是這些 Goroutine 可以一起在睡著的狀態(模擬了同時讀檔案)

Listing 14 使用了 1 個 core 出來的結果,大部分其實就是 8 個 Goroutine 可以同時睡眠的加速而已

Listing 15 發現 8 core + 8 G 出來發現沒有加速(相比 1C 8G),我推測應該是原本運算的地方就佔比不高了,一個 core 就夠處理運算部分,大部分都在等 sleep,所以多了幾個 core,只用了 8 個 Goroutine,能同時睡著的 G 也只有 8 個,等的時間還是差不多

下面用一張圖來解釋在何種情況下開多個 Goroutine 或者使用更多 Core 能夠增加效能,縮短任務完成時間。
下圖假設

  1. 四種情況都有三組任務需要執行。每組都需要 core 計算 10 秒,接著 I/O 5 秒,兩者順序不能調換
  2. 不計算 context switch 所花費的時間
  3. 不同 Core 可以同時執行計算 (CPU - 10 那個部分)
  4. I/O 是大家共用,同時只能有一組 I/O 執行,在做完一組 I/O 之前不會切到另一組

第 (1) 組是序列執行,沒有 context switch ,I/O 時完全無法利用 core 來做事,很慢
第 (2) 組讓一個 core 上跑了 3 個 G,G1 被 I/O block 住時,G2的計算可以先開始,比 (1) 的效率好點,但 I/O 還是有空窗期
第 (3) 組就是多了一個 core,可以看到在 10 秒之後 I/O 都被排滿了,都有有效利用
第 (4) 組再多了一個 core 執行時間並無縮短,因為 10 之後 I/O 都排滿了,而且多幾個 core 也無法縮短一開始的計算時間 10 秒 (除非可以把計算拆小但此處不考慮)

上圖其實比較像是在解釋平行與並行在這種任務類型下的表現,因為其實把 Goroutine 看成 thread 也說得通。在 Part II 時最後有說過,其實他們切換的流程看起來是類似的,只是 Goroutine context switch 的速度快多了,資源消耗也少點。

這系列的文章的內容差不多就是這樣了
總結一下第一章介紹了 Go scheduler 需要的基本 OS 知識
第二章再透過簡單的圖片來看看 Go scheduler 運作的方式
最後以例子來看看不同的任務類型在 Concurrency 與 Parallelism 上面運作情況

了解了這些之後,如果遇到自己的程式有效能上的問題,或許就能知道從哪裡著手分析,看是要多開幾個 Goroutine 還是多租幾個 CPU 來用啦~

Share on

Marko Peng
WRITTEN BY
Marko Peng
Good man