1. 程式人生 > >高性能服務器開發基礎系列 (一)主線程與工作線程的分工

高性能服務器開發基礎系列 (一)主線程與工作線程的分工

c++ 服務器

服務器端為了能流暢處理多個客戶端鏈接,一般在某個線程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()。

這樣做有三個好處:

線程A只需要處理新連接的到來即可,不用處理網絡IO事件。由於網絡IO事件處理一般相對比較慢,如果在線程A裏面既處理新連接又處理網絡IO,則可能由於線程忙於處理IO事件,而無法及時處理客戶端的新連接,這是很不好的。

線程A接收的新連接,可以根據一定的負載均衡原則將新的socket fd分配給工作線程。常用的算法,比如round robin,即輪詢機制,即,假設不考慮中途有連接斷開的情況,一個新連接來了分配給B1,又來一個分配給B2,再來一個分配給B3,再來一個分配給B4。如此反復,也就是說線程A記錄了各個工作線程上的socket fd數量,這樣可以最大化地來平衡資源,避免一些工作線程“忙死”,另外一些工作線程“閑死”的現象。

即使工作線程不滿載的情況下,也可以讓工作線程做其他的事情。比如現在有四個工作線程,但只有三個連接。那麽線程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平臺上可以通過以下幾種方法實現:

管道pipe,創建一個管道,將管道綁定到epoll_fd上。需要時,向管道一端寫入一個字節,工作線程立即被喚醒。

linux 2.6新增的eventfd:

int eventfd(unsigned int initval, int flags); 

步驟也是一樣,將生成的eventfd綁定到epoll_fd上。需要時,向這個 eventfd上寫入一個字節,工作線程立即被喚醒。

第三種方法最方便。即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上去。這在寫跨兩個平臺代碼時,需要註意的地方。

歡迎關註公眾號『easyserverdev』。如果有任何技術或者職業方面的問題需要我提供幫助,可通過這個公眾號與我取得聯系,此公眾號不僅分享高性能服務器開發經驗和故事,同時也免費為廣大技術朋友提供技術答疑和職業解惑,您有任何問題都可以在微信公眾號直接留言,我會盡快回復您。
技術分享圖片

高性能服務器開發基礎系列 (一)主線程與工作線程的分工