常見多線程與併發服務器設計方案舉例

一、3點基礎知識

1、一個主機的端口號爲所有進程所共享,但普通用戶進程綁定bind不了一些特殊端口號如20、80等。 
    多個進程不能同時監聽listen同一個端口,會失敗。
    關注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/ 中有這樣一個例子,因爲數獨求解是計算密集型任務。

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/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章