伺服器端編程心得(一)—— 主線程與工作線程的分工
伺服器端為了能流暢處理多個客戶端鏈接,一般在某個線程A裡面accept新的客戶端連接並生成新連接的socket fd,然後將這些新連接的socketfd給另外開的數個工作線程B1、B2、B3、B4,這些工作線程處理這些新連接上的網路IO事件(即收發數據),同時,還處理系統中的另外一些事務。這裡我們將線程A稱為主線程,B1、B2、B3、B4等稱為工作線程。工作線程的代碼框架一般如下:
while (!m_bQuit) { epoll_or_select_func(); handle_io_events(); handle_other_things(); }
在epoll_or_select_func()中通過select()或者poll/epoll()去檢測socket fd上的io事件,若存在這些事件則下一步handle_io_events()來處理這些事件(收發數據),做完之後可能還要做一些系統其他的任務,即調用handle_other_things()。
這樣做有三個好處:
1. 線程A只需要處理新連接的到來即可,不用處理網路IO事件。由於網路IO事件處理一般相對比較慢,如果在線程A裡面既處理新連接又處理網路IO,則可能由於線程忙於處理IO事件,而無法及時處理客戶端的新連接,這是很不好的。
2. 線程A接收的新連接,可以根據一定的負載均衡原則將新的socket fd分配給工作線程。常用的演算法,比如round robin,即輪詢機制,即,假設不考慮中途有連接斷開的情況,一個新連接來了分配給B1,又來一個分配給B2,再來一個分配給B3,再來一個分配給B4。如此反覆,也就是說線程A記錄了各個工作線程上的socket fd數量,這樣可以最大化地來平衡資源,避免一些工作線程「忙死」,另外一些工作線程「閑死」的現象。
3. 即使工作線程不滿載的情況下,也可以讓工作線程做其他的事情。比如現在有四個工作線程,但只有三個連接。那麼線程B4就可以在handle_other_thing()做一些其他事情。
下面討論一個很重要的效率問題:
在上述while循環裡面,epoll_or_selec_func()中的epoll_wait/poll/select等函數一般設置了一個超時時間。如果設置超時時間為0,那麼在沒有任何網路IO時間和其他任務處理的情況下,這些工作線程實際上會空轉,白白地浪費cpu時間片。如果設置的超時時間大於0,在沒有網路IO時間的情況,epoll_wait/poll/select仍然要掛起指定時間才能返回,導致handle_other_thing()不能及時執行,影響其他任務不能及時處理,也就是說其他任務一旦產生,其處理起來具有一定的延時性。這樣也不好。那如何解決該問題呢?
其實我們想達到的效果是,如果沒有網路IO時間和其他任務要處理,那麼這些工作線程最好直接掛起而不是空轉;如果有其他任務要處理,這些工作線程要立刻能處理這些任務而不是在epoll_wait/poll/selec掛起指定時間後才開始處理這些任務。
我們採取如下方法來解決該問題,以linux為例,不管epoll_fd上有沒有文件描述符fd,我們都給它綁定一個默認的fd,這個fd被稱為喚醒fd。當我們需要處理其他任務的時候,向這個喚醒fd上隨便寫入1個位元組的,這樣這個fd立即就變成可讀的了,epoll_wait()/poll()/select()函數立即被喚醒,並返回,接下來馬上就能執行handle_other_thing(),其他任務得到處理。反之,沒有其他任務也沒有網路IO事件時,epoll_or_select_func()就掛在那裡什麼也不做。
這個喚醒fd,在linux平台上可以通過以下幾種方法實現:
1. 管道pipe,創建一個管道,將管道綁定到epoll_fd上。需要時,向管道一端寫入一個位元組,工作線程立即被喚醒。
2. linux 2.6新增的eventfd:
int eventfd(unsigned int initval, int flags);
步驟也是一樣,將生成的eventfd綁定到epoll_fd上。需要時,向這個eventfd上寫入一個位元組,工作線程立即被喚醒。
3. 第三種方法最方便。即linux特有的socketpair,socketpair是一對相互連接的socket,相當於伺服器端和客戶端的兩個端點,每一端都可以讀寫數據。
int socketpair(int domain, int type, int protocol, int sv[2]);
調用這個函數返回的兩個socket句柄就是sv[0],和sv[1],在一個其中任何一個寫入位元組,在另外一個收取位元組。
將收取的位元組的socket綁定到epoll_fd上。需要時,向另外一個寫入的socket上寫入一個位元組,工作線程立即被喚醒。
如果是使用socketpair,那麼domain參數一定要設置成AFX_UNIX。
由於在windows,select函數只支持檢測socket這一種fd,所以windows上一般只能用方法3的原理。而且需要手動創建兩個socket,然後一個連接另外一個,將讀取的那一段綁定到select的fd上去。這在寫跨兩個平台代碼時,需要注意的地方。
推薦閱讀:
※伺服器端編程心得(七)——開源一款即時通訊軟體的源碼
※關於伺服器你知道多少
※深挖NUMA
※loki設計構想
TAG:伺服器架構 |