C++服務器設計(三):多線程模型設計


多線程探討

  如今大多數CPU都具有多個核心,為了最大程度的發揮多核處理器的效能,提高服務器的並發性,保證系統對於多線程的支持是十分必要的。我們在之前的設計都是基於單線程而言,在此章我們將對系統進行改進,在進一步提升系統性能的同時保證系統對於多線程的支持。

  首先考慮這么幾個問題,我們之前已經選定了基於I/O復用的Reactor模式,那么在多線程環境下我們該如何處理這些I/O?多線程同時處理同一個套接字描述符安全嗎?Reactor模式支持多線程嗎?

  根據查閱文檔可知,針對文件描述符的常見系統調用如read、write是線程安全的,我們不用擔心多個線程同時操作文件描述符會導致進程崩潰發生。同時根據UNPv1描述,在兩個線程中分別對同一個套接字進行read操作和write操作是線程安全的,因為TCP套接字是雙向I/O。

  但是我們依然要考慮如下的集中情況:

  • l  兩個線程同時讀同一個套接字,此時兩個線程各自收到同一條消息的一部分數據,如何把這兩部分數據合並成一條完整的消息?
  • l  兩個線程同時寫同一個套接字,此時每個線程都只發送出去了半條消息,接收方將如何處理接收到的數據?

  如果我們給每個套接字配一把鎖,讓每次只能有一個線程獲得鎖來讀或者寫這個套接字,這能夠解決以上的問題。但是在Reactor模式中,我們應該盡量避免阻塞線程的操作。如果此時某個線程中的事件處理器競爭鎖失敗被阻塞,將會導致該線程之后的其他事件處理也全部被阻塞。

  因此,我們認為雖然描述符常見系統調用是線程安全的,但是由於將一個描述符置於多線程環境中將會使整個業務邏輯復雜化,雖然一定程度上我們可以通過應用層I/O緩沖加鎖機制解決,但是這依舊會導致線程阻塞現象和服務器性能下降,這是得不償失的。因此我們認為在多線程環境下我們依然要確保每個文件描述符只能有一個線程進行操作。這樣既可解決消息收發的順序問題,同時也避免了各種鎖競爭現象。

  在以上符合以上原則的情況下,我們將每個連接套接字的讀寫操作依舊注冊在單一Reactor反應器中。同時我們在之前章節描述過,每個Reactor模式都包含一個線程大循環,因此每個Reactor反應器都應該是單線程的,可以支持注冊多個連接套接字。但是如果將所有連接套接字都注冊在一個線程中,我們的系統就退化為了單線程服務器了。因此我們應該將每一個新連接平均分配到不同的反應器事件循環中,讓多個線程平均注冊不同的連接事件,讓每個線程處理該線程內的所有反應器事件。

One loop per thread模型介紹

  根據之前分析,我們服務器系統的多線程模型已經大致清晰,即采用non-blocking IO + one loop per thread模式。在該模式下,創建多個線程,並且每個線程都創建Reactor反應器,每個反應器又存在一個事件循環(event loop),用於等待注冊事件和處理事件的讀寫。當我們需要讓哪個線程干活,我們就把某個新的連接套接字注冊到該線程所在的反應器中即可。

  在這種模式中,雖然我們要注意每個套接字只能注冊到一個線程反應器中,不能跨反應器使用,但是這種可以分配套接字所在線程的方式依舊能夠給我們的系統帶來很大的負載彈性。比如對於實時性要求較高的連接可以單獨占用一個線程;處理數據量大的連接也可以獨占一個線程,並把某些數據處理任務分攤到另外幾個計算線程中;而某些相對次要的輔助性連接可以多個共享一個線程,只要保證每個連接的處理器無阻塞,依舊能夠保證事件處理延遲並不會太高。

  我們可以將這種模式的優點總結如下:

  • l  線程數量在程序啟動時設置,數量確定,並通過線程池管理,不會頻繁創建與銷毀線程的開銷。
  • l  可以很方便的在線程間調節負載。
  • l  對於同一個TCP連接而言,整個連接期間所在線程固定,不必考慮事件並發的可能。

線程間任務隊列模型設計

  線程間任務隊列模型是一種多線程處理的形式。處理過程中將需要在某個線程中運行的任務注冊到該線程的任務隊列中,當該線程檢測到任務隊列中存在任務時,將會取出任務並執行。當任務隊列中的任務全部都執行完畢后,該線程將會被阻塞,直到有新的任務被注冊導致線程被喚醒。

  首先我們不考慮Reactor模式,設計一個符合以上需求的模型。在這個模型中線程的關鍵數據結構是任務隊列。

  任務隊列類似於緩沖無限大的多生產者多消費者模型。緩沖通過條件變量進行多線程保護。生產者和消費者均在不同線程中,生產者通過post操作向緩沖尾部添加任務,消費者通過take操作從緩沖頭部獲取任務。可能存在多個生產者的情況,因此如果有某個生產者期望添加任務,需要獲取同步鎖后才能進行添加操作。而消費者不但需要獲取同步鎖,而且還要檢查當前任務隊列是否存在可用任務,如果存在則取出,如果不存在則通過條件變量被阻塞,直到存在某個生產者添加了新的任務並執行喚醒操作。

  任務隊列的緩沖部分應該支持從頭部讀取,從尾部寫入的隊列功能。同時最好能夠支持緩沖動態增長,使其在生產者的角度看來緩沖應該類似無限大,以保證不會出現生產者寫入過多任務導致操作被阻塞的情況。在STL庫中,deque結構作為動態增長分段連續的雙向容器,可以很好的滿足以上需求,因此我們采用STL庫的std::deque作為緩沖實現。

  同時緩沖的任務數據部分,類似於之前章節我們分析的回調函數。它是對象,能夠以數據結構的形式被寫入緩沖中,當從緩沖中讀取出來后,它又能夠以類似於函數的形式被調用,最好還能帶有自身參數的管理。boost庫中的function<void>函數對象實現了這一功能,它可以通過普通函數賦值,也可以通過同為boost庫中的bind函數綁定帶參數函數或某個成員函數。它能夠被當做一個對象供緩沖容器保存,同時也能夠作為回調函數被執行。

  通過以上研究設計,我們的任務隊列是一個以條件變量進行多線程保護的緩沖,該緩沖的底層數據結構實現為std::deque<boost::function<void()> >。

圖3-12 線程間任務隊列模型

  最終的線程間任務隊列設計實現如圖所示。線程主體是一個任務循環,它會反復從任務隊列中take可用任務。如果當前任務隊列沒有任務,take操作將使線程阻塞,直到其他線程中添加了新的可用任務到該任務隊列,才會將該線程喚醒並獲取任務。當線程獲取到任務后,將會在本線程中執行任務回調。當任務執行結束后,線程將重新進入循環,再次期待從任務隊列中take到可用任務。

  通過該線程間任務隊列模型,我們可以將期望的任務操作從某個線程轉移到另一個線程中執行。

線程模型與Reactor模式結合

  之前的線程間任務隊列模型設計中,我們並沒有考慮到Reactor模式的特性,更沒有聯系到服務器系統的具體需求場景中。因此我們仍需要對該模型進行改造,使之融入到我們的整個服務器系統中。

  Reactor模式下的系統原型類似於圖3-6,其主體是事件循環下的事件分離器監聽事件產生,並回調具體事件的handler進行處理。我們給每個反應器添加一個任務隊列結構,用於緩沖其他線程向該線程注冊的任務。同時我們需要知道其他線程是何時向該Reactor反應器添加了任務。因為之前的Reactor反應器並不能監聽任務隊列的數量,並且Reactor可能會被阻塞在epoll事件監聽中,如果長期沒有事件被監聽,整個反應器線程將會被長期阻塞,即使此時有其他線程向該反應器添加了任務,也無法得到及時執行。

  我們通過給每個反應器額外創建一個管道,並將該管道的描述符可讀事件注冊到該反應器中。該描述符同樣向其他線程暴露,當其他線程通過該反應器的任務隊列向其添加了新任務后,再獲取該反應器的管道描述符,並執行寫操作。此時我們只需寫入隨便一字節數據,目的是喚醒可能處於事件監聽而阻塞的該反應器,通知它任務隊列存在可用任務,需要執行處理。

 

圖3-13 支持任務隊列的Reactor反應器模型

  我們設計的支持任務隊列的Reactor反應器如圖3-13所示。在反應器初始化階段創建一個管道,並將該管道描述符注冊到反應器中,以便其他線程能夠喚醒該反應器。同時在反應器處理完所有激活事件的handler后,會檢查自身的任務隊列是否為空。在這里不同於之前的線程模型設計,如果任務隊列為空,表明當前沒有任務可執行,反應器不能夠被阻塞於此,而是直接跳過進入下一輪循環;如果任務隊列非空,就把任務隊列中的所有任務全部讀取出來,並依次回調執行,執行完后進入下一輪循環。因為反應器的要求就是盡可能的非阻塞,它的核心是事件處理,而我們的任務隊列類似於承屬於管道描述符的特殊事件處理。因此對於該事件而言,它不同於線程模型,是有任務則處理,無任務則跳過。

  而在其他線程中,如果想對某個反應器添加任務,只需先獲取該反應器的任務隊列,向任務隊列添加線程,再獲取該反應器的管道描述符,通過寫入任意數據將該反應器喚醒即可。

服務器系統中多線程的運用

  在服務器系統中,我們使用支持多線程的Reactor模式,並綜合新連接創建和線程分配的業務場景,確定了最后的服務器底層模型。

 

圖3-14 多線程的Reactor模型

  如圖3-14所示,系統中存在一個main Reactor負責監聽accept連接。每當有新的連接產生時,反應器回調監聽套接字處理器,並在其中創建一個任務,該任務是將這個新連接注冊到某個指定的反應器中,並向該反應器發送喚醒事件。

  同時系統通過線程池管理多個工作反應器,工作反應器的數量是可以設置的,可以根據CPU的數目來確定恰當的數量。每當監聽Reactor中有新連接產生時,將會通過Round Robin輪詢調度從線程池中選出一個工作反應器,作為新任務的發送對象。被選中的這個工作反應器也將會作為該連接的實際管理者,這個連接的所有操作都會在這個工作反應器所在線程中完成。

  通過以上設計,我們的系統不但能夠通過多線程充分利用到了多核CPU的性能,又通過固定線程數避免了系統總體處理能力不會隨連接數增加而下降。同時由於一個連接完全由一個線程管理,保證了對該連接的讀寫及事件處理的能夠按照順序執行,簡化了多線程下實際業務邏輯的處理過程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM