2016年9月21日 星期三

再論 multi-threading

之前那篇文章陳鐘誠老師轉貼後,引起正反雙方各種討論,通篇看下來反對的人比較多。看來小弟不得不替自己辯解一下

multi-threading 是程式員的陽光、空氣、水?


這裡指的是 OS native thread。如果你的答案是「Yes」,那我們看看 TIOBE 前 10 名兩個當紅語言 Python 與 JavaScript,按照 Python Cookbook 的說法是 Python 可以產生 OS native thread,但是又會受到 GIL(global interpreter lock)的影響,建議不用。而 JavaScript 直到 Web Worker 前沒有類似 multi-thread 的東西,而 Web Worker 照小弟的粗淺理解也不能算產生 OS native thread。而 JavaScript 的同宗兄弟 Node.js 則是依靠模組達成目的。

相較於 C/C++、Java、C#,Python 與 JavaScript 對 OS native thread 的支援看來十分蹩腳。但也不見使用者吶喊「沒有它程式就寫不下去」,還是幹了很多漂亮的活。所以真的非有不可嗎?

沒有 multi-threading 就無法發揮多核心效能?


首先無論是 Windows/Linux,開機後就一堆 NT Services/Daemons 在執行了。所以你為什麼會有「8 核心都是我的程式獨占科科」這種想法呢?

如果你要干涉 OS 對 CPU 資源的分配,就小弟所知跟兩樣有關:

  1. Process/Thread Priority
  2. CPU Affinity
CPU Affinity 這個在 Windows 內,用工作管理員就可以看到:


搞不好多人還是第一次知道這個選項。再說寫 multi-threading 就是希望 OS 自動依據核心數幫我 scale,如果還要手動指定,這種程式寫起來也太累人了吧!沒有移植性問題嗎?而且使用 multi-processes,一樣可以享受多核心的好處。

哪種軟體可以 100% 從多核心中受益?


有一種軟體很少人聽過、更少人用過,就是 RTOS in Windows。這聽起來真讓人莫名其妙,到底是指 RTOS 還是 Windows 啊...Windows 又不是 RTOS,到底是 2#$^&...。簡單來說,這是工業控制中一種特殊 RTOS,他可以與 Windows 「並存」,在 x86 ring 0 直接分掉核心。


這種同時執行的雙作業系統不同於Virtual Machine,聽起來很玄。但若往實務面去想,他就是要利用人人接受的 Windows UI 與 x86 過剩的計算能力進行工業控制,就一點都不奇怪了。這方面比較知名的產品有 IntervalZero RTX(因有版權問題,上圖是簡化再簡化過的,請自行打開連結觀看)、ON TIME RTOS 等等

另外,Linux 也可以藉由設定核心,讓 interrupt 發至指定的 CPU,也是一種 100% 利用多核心的方式(感覺跟小弟多年前搞的 FxxA 好像喔XDDDD)

但以上這些程式,似乎與大多數人最常用的 user mode program 沒多大關係?

非得使用 multi-threading 的場合?


在 Linux多线程服务端编程 一書中,作者舉了一個很難反駁的例子,就是若是只有 IO multiplexing(e.g epoll) + single thread,若是某個 connection 發生計算過長的情形,勢必造成其他 connection latency,所以這時候要搭配 worker thread。

但換個角度想,我用 worker process 搭配 UNIX domain socket、甚至是 TCP socket 不行嗎?如果採用 TCP loopback,到時候還可以分拆 process 到不同的機器上,直接讓一台獨立的機器計算,用網路傳回結果,不是更容易擴展嗎?

為什麼難以割捨 multi-threading?


小弟認為有三大原因:
  1. 共享記憶體
  2. 啟動成本低,使用成本比 process 低
  3. 當 process 結束,所有 threads 也一併結束
1. 恐怕是最大誘因,因為 multi-threading 常常共用一種編譯模型,比方說 C++、Java、C#,不同 thread 可以共用 tree、hash、queue 等比 flat memory 更有用的資料結構。multi-process 不是不行,多年前在 CUJ 上看過把 stl map 用於 UNIX shmem上的作法,但複雜程度遠超過簡單的 map + mutex,而且也只能讓 C++ app 共用 map。所以對於 Redis、memcached 來說,乾脆就是用通訊協議來解決這個問題。而這這種作法在效能上很難超越 in-process map。

2. 啟動成本低不用多說,但根據書上的說法,32bits Linux 最多也只能產生 300 個 threads(理論值),現在這種大量產生執行緒的用法已經被唾棄了,主流作法是產生略多於核心數的執行緒,放在程式的開頭,直到 process 結束才結束這些執行緒。(nginx 的作法是產生固定 processes,但其效能也十分受肯定)

3. 是很多不負責的程序員常幹的事,反正 process game over,OS 就會幫我清除那些執行緒。但此種作法往往沒有以正確順序釋放資源,甚至是資源洩漏,有時可以正常重啟,有時不行。筆者開發程式時從未利用這種特性,而是讓執行緒的結束在掌控之中,這麼多年還沒有人跟小弟抱怨過有問題。

如何安全有效的使用 multi-threading?


如果非用不可的話

個人推薦的用法是使用作業系統的管線、TCP、UNIX domain socket 搭配 IO multiplexing,搭配 read only cache(在前面的文章已經分享過了)。把 mutex 之類的同步物件盡可能減少,如此 deadlock 的問題發生率也會大大降低,至於不 DIY queue 就難過的朋友們,請先想一下您程式的 throughput 是否真的需要自行 DIY?您管理 buffer 還是交給作業系統方便?或者是 zero copy 真的有必要?

另外一前面介紹過的 Linux 多线程服务端编程 是一本值得參考的好書,前五章對於 C++ multithreading 有非常務實的見解(相較於學校教完即丟的哲學家吃飯問題),非常值得一讀。作者雖然推薦 one loop per thread 的多執行續用法,但他也自承在他的實務經驗裡,最多也才 10 個幾個 threads,也是略多於 CPU 真實的核心數而已。

產生過多 threads 可以說是保證開啟了除錯跟維護的地獄之門,回頭是岸吧!

沒有留言:

張貼留言