常見多執行緒與併發伺服器設計方案舉例

s1mba發表於2013-11-04
一、3點基礎知識

1、一個主機的埠號為所有程式所共享,但普通使用者程式繫結bind不了一些特殊埠號如20、80等。 
    多個程式不能同時監聽listen同一個埠,會失敗。當然父程式可以先listen然後fork多個子程式,多個子程式都可以accept這個sock,即搶奪式響應(驚群效應)。
    關注4元組是否能唯一確定一個連線?

2、每個程式都有自己的檔案描述符(包括file fd, socket fd, timer fd, event fd, signal fd),一般是1024,可以通過ulimit -n 設定,但所有程式開啟的檔案描述符總數有上限,跟主機的記憶體有關。

3、一個程式內的所有執行緒共享程式的檔案描述符。

二、常見併發伺服器方案:

1、迴圈式/迭代式( iterative )伺服器
無法充分利用多核CPU,不適合執行時間較長的服務,即適用於短連線。如果是長連線則需要在read/write之間迴圈,那麼只能服務一個客戶端。


2、併發式(concurrent)伺服器
one connection per process/one connection per thread
適合執行時間比較長的服務


one connection per process : 主程式每次fork 之後要關閉connfd,子程式要關閉listenfd
one connection per thread : 主執行緒每次accept 回來就建立一個子執行緒服務,由於執行緒共享檔案描述符,故不用關閉。

3、prefork or pre threaded(UNP2e 第27章)(容易發生“驚群”現象,即多個子程式都處於accept狀態)


4、反應式( reactive )伺服器 (reactor模式)(select/poll/epoll)
併發處理多個請求,實際上是在一個執行緒中完成。無法充分利用多核CPU
不適合執行時間比較長的服務,所以為了讓客戶感覺是在“併發”處理而不是“迴圈”處理,每個請求必須在相對較短時間內執行。


5、reactor + thread per request(過渡方案)

6、reactor + worker thread(過渡方案)

7、reactor + thread pool(能適應密集計算)


muduo庫中的/example/suduku/ 中有這樣一個例子,因為數獨求解是計算密集型任務。

在實踐中為了reactor能快速回到事件迴圈去響應請求,經常將讀到的資料put到一個環形記憶體佇列(一般記憶體or共享記憶體),而thread pool的執行緒則從中讀取進行資料計算。

8、multiple reactors(能適應更大的突發I/O)
reactors in threads(one loop per thread)
reactors in processes
一般來說一個subReactor適用於一個千兆網口



9、multiple reactors + thread pool(one loop per thread + threadpool)(突發I/O與密集計算)
subReactor可以有多個,但threadpool只有一個。


10、proactor伺服器(proactor模式,基於非同步I/O)
理論上proactor比reactor效率要高一些
非同步I/O能夠讓I/O操作與計算重疊。充分利用DMA特性。
Linux非同步IO
glibc aio(aio_*),有bug
kernel native aio(io_*),也不完美。目前僅支援 O_DIRECT 方式來對磁碟讀寫,跳過系統快取。要自已實現快取,難度不小。
boost asio實現的proactor,實際上不是真正意義上的非同步I/O,底層是用epoll來實現的,模擬非同步I/O的。



常見併發伺服器方案比較


三、一些常見問題

1、Linux能同時啟動多少個執行緒?
對於 32-bit Linux,一個程式的地址空間是 4G,其中使用者態能訪問 3G 左右,而一個執行緒的預設棧 (stack) 大小是 8M,心算可知,一個程式大約最多能同時啟動 350 個執行緒左右。

2、多執行緒能提高併發度嗎?
如果指的是“併發連線數”,不能。

假如單純採用 thread per connection 的模型,那麼併發連線數大約350,這遠遠低於基於事件的單執行緒程式所能輕鬆達到的併發連線數(幾千上萬,甚至幾萬)。所謂“基於事件”,指的是用 IO multiplexing event loop 的程式設計模型,又稱 Reactor 模式。


3、多執行緒能提高吞吐量嗎?
對於計算密集型服務,不能。

如果要在一個8核的機器上壓縮100個1G的文字檔案,每個core的處理能力為200MB/s,那麼“每次起8個程式,一個程式壓縮一個檔案”與“只啟動一個程式(8個執行緒併發壓縮一個檔案)”,這兩種方式總耗時相當,但是第二種方式能較快的拿到第一個壓縮完的檔案。

4、多執行緒能提高響應時間嗎?
可以。參考問題3

5、多執行緒程式日誌庫要求

執行緒安全,即多個執行緒可以併發寫日誌,兩個執行緒的日誌訊息不會出現交織。
用一個全域性的mutex保護IO
每個執行緒單獨寫一個日誌檔案
前者造成全部執行緒搶佔一個鎖(序列寫入)
後者有可能讓業務執行緒阻塞在寫磁碟操作上。(磁碟IO時間比較長)

解決辦法:用一個logging執行緒負責收集日誌訊息,並寫入日誌檔案,其他業務執行緒只管往這個“日誌執行緒”傳送日誌訊息(如通過BlockingQueue提供介面),這稱為“非同步日誌”,也是一個經典的生產者消費者模型。


6、線程池大小的選擇

如果池中執行任務時,密集計算所佔時間比重為P(0<P<=1),而系統一共有C個CPU,為了讓C個CPU跑滿而不過載,執行緒池大小的經驗公式T=C/P,即T*P=C(讓CPU剛好跑滿 )
假設C=8,P=1.0,執行緒池的任務完全密集計算,只要8個活動執行緒就能讓CPU飽和
假設C=8,P=0.5,執行緒池的任務有一半是計算,一半是IO,那麼T=16,也就是16個“50%繁忙的執行緒”能讓8個CPU忙個不停。

7、執行緒分類
I/O執行緒(這裡特指網路I/O)
計算執行緒
第三方庫所用執行緒,如logging,又比如database


參考:
《UNP》
muduo manual.pdf
《linux 多執行緒伺服器程式設計:使用muduo c++網路庫》
http://www.ibm.com/developerworks/cn/linux/l-async/
https://domsch.com/linux/lpc2010/Scaling_techniques_for_servers_with_high_connection%20rates.pdf

相關文章