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

多線程探討

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

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

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

  可是咱們依然要考慮以下的集中狀況:多線程

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

  若是咱們給每一個套接字配一把鎖,讓每次只能有一個線程得到鎖來讀或者寫這個套接字,這可以解決以上的問題。可是在Reactor模式中,咱們應該儘可能避免阻塞線程的操做。若是此時某個線程中的事件處理器競爭鎖失敗被阻塞,將會致使該線程以後的其餘事件處理也所有被阻塞。併發

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

  在以上符合以上原則的狀況下,咱們將每一個鏈接套接字的讀寫操做依舊註冊在單一Reactor反應器中。同時咱們在以前章節描述過,每一個Reactor模式都包含一個線程大循環,所以每一個Reactor反應器都應該是單線程的,能夠支持註冊多個鏈接套接字。可是若是將全部鏈接套接字都註冊在一個線程中,咱們的系統就退化爲了單線程服務器了。所以咱們應該將每個新鏈接平均分配到不一樣的反應器事件循環中,讓多個線程平均註冊不一樣的鏈接事件,讓每一個線程處理該線程內的全部反應器事件。oop

One loop per thread模型介紹

  根據以前分析,咱們服務器系統的多線程模型已經大體清晰,即採用non-blocking IO + one loop per thread模式。在該模式下,建立多個線程,而且每一個線程都建立Reactor反應器,每一個反應器又存在一個事件循環(event loop),用於等待註冊事件和處理事件的讀寫。當咱們須要讓哪一個線程幹活,咱們就把某個新的鏈接套接字註冊到該線程所在的反應器中便可。post

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

  咱們能夠將這種模式的優勢總結以下:線程

  • 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的性能,又經過固定線程數避免了系統整體處理能力不會隨鏈接數增長而降低。同時因爲一個鏈接徹底由一個線程管理,保證了對該鏈接的讀寫及事件處理的可以按照順序執行,簡化了多線程下實際業務邏輯的處理過程。

相關文章
相關標籤/搜索