如果要在一台多核機器上提供一種服務或執行一個任務,可用的模式有
- 運行一個單線程的進程
- 運行一個多線程的進程
- 運行多個單線程的進程
- 運行多個多線程的進程
這些模式之間的比較已經是老生常談,簡單地總結
- 模式 1 是不可伸縮的 (scalable),不能發揮多核機器的計算能力;
- 模式 3 是目前公認的主流模式。它有兩種子模式:
3a 簡單地把模式 1 中的進程運行多份,如果能用多個 tcp port 對外提供服務的話;
3b 主進程+woker進程,如果必須綁定到一個 tcp port,比如 httpd+fastcgi。 - 模式 2 是很多人鄙視的,認為多線程程序難寫,而且不比模式 3 有什么優勢;
- 模式 4 更是千夫所指,它不但沒有結合 2 和 3 的優點,反而匯聚了二者的缺點。
據我所知,有兩種場合必須使用單線程
- 程序可能會 fork()。
- 限制程序的 CPU 占用率。
單線程程序的優缺點
- event loop 有一個明顯的缺點,它是非搶占的(non-preemptive)。假設事件 a 的優先級高於事件 b,處理事件 a 需要 1ms,處理事件 b 需要 10ms。如果事件 b 稍早於 a 發生,那么當事件 a 到來時,程序已經離開了 poll() 調用開始處理事件 b。事件 a 要等上 10ms 才有機會被處理,總的響應時間為 11ms。這等於發生了優先級反轉。這可缺點可以用多線程來克服,這也是多線程的主要優勢。
- 如果用很少的 CPU 負載就能讓的 IO 跑滿,或者用很少的 IO 流量就能讓 CPU 跑滿,那么多線程沒啥用處。
多線程的適用場景
- 提高響應速度,讓 IO 和“計算”相互重疊,降低 latency。雖然多線程不能提高絕對性能,但能提高平均響應性能。
模式 2 和模式 3a 該如何取舍
如果工作集較大,那么就用多線程,避免 CPU cache 換入換出,影響性能;否則,就用單線程多進程,享受單線程編程的便利。
一個程序要做成多線程的,大致要滿足
- 有多個 CPU 可用。單核機器上多線程的優勢不明顯;
- 線程間有共享數據。如果沒有共享數據,用模型 3b 就行。雖然我們應該把線程間的共享數據降到最低,但不代表沒有;
- 共享的數據是可以修改的,而不是靜態的常量表。如果數據不能修改,那么可以在進程間用 shared memory,模式 3 就能勝任;
- 提供非均質的服務。即,事件的響應有優先級差異,我們可以用專門的線程來處理優先級高的事件。防止優先級反轉;
- latency 和 throughput 同樣重要,不是邏輯簡單的 IO bound 或 CPU bound 程序;
- 利用異步操作。比如 logging。無論往磁盤寫 log file,還是往 log server 發送消息都不應該阻塞 critical path;
- 能 scale up。一個好的多線程程序應該能享受增加 CPU 數目帶來的好處,目前主流是 8 核,很快就會用到 16 核的機器了;
- 具有可預測的性能。隨着負載增加,性能緩慢下降,超過某個臨界點之后急速下降。線程數目一般不隨負載變化;
- 多線程能有效地划分責任與功能,讓每個線程的邏輯比較簡單,任務單一,便於編碼。而不是把所有邏輯都塞到一個 event loop 里,就像 Win32 SDK 程序那樣。
一個多線程服務程序中的線程大致可分為 3 類
- IO 線程,這類線程的的主循環是 io multiplexing,等在 select/poll/epoll 系統調用上。這類線程也處理定時事件。當然它的功能不止 IO,有些計算也可以放入其中;
- 計算線程,這類線程的主循環是 blocking queue,等在 condition variable 上。這類線程一般位於 thread pool 中;
- 第三方庫所用的線程,比如 logging,又比如 database connection。
參考:《Linux多線程服務端編程》。
