1. 程式人生 > >Muduo 網路庫原始碼分析 之 關鍵技術點總結

Muduo 網路庫原始碼分析 之 關鍵技術點總結

最近又把muduo網路庫仔細研究了一遍,收穫良多。本文將對muduo中的設計思想以及關鍵的技術細節進行總結和分析,當然由於篇幅的原因這裡更多的是對關鍵技術的簡略提及,具體細節還需要讀者自己去查詢學習資料。

muduo/base

  • Date類
    • 日期類的封裝,使用Julian(儒略日)可以方便的計算日期差。具體公式和思想見 儒略日的計算
  • Exception類
    • 異常類的封裝,對外提供what()輸出錯誤資訊和stacktrace()函式進行棧追蹤,使用時需要throw muduo::Exception("oops");,外部使用catch (const muduo::Exception& ex)
      捕獲並使用ex.what()/stackTrace()獲取詳細資訊。
  • Atomic類
    • 原子性操作比鎖的開銷小,所以我們可以使用gcc提供的自增自減原子操作;最小的執行單元是彙編語句,不是語言語句。
  • CountDownLatch類
    • 既可用於所有子執行緒等待主執行緒發起“起跑”,也可用於主執行緒等待子執行緒初始化完畢才開始工作。其中使用RAII技法封裝MutextLockGuard,hodler表示鎖屬於哪一個執行緒。
  • TimeStamp類
    • TimeStamp 繼承至less_than_comparable<>,使用模板超程式設計,只需要實現<,可自動實現>,<=,>=。
  • BlockingQueue

    • BlockingQueue和BoundedBlockingQueue分別是無界有界佇列,本質上是生產者消費者問題,使用訊號量或者條件變數解決。ThreadPool的本質也是生產者消費者問題,任務佇列中是任務函式(生產者),執行緒佇列就相當於消費者。基本流程如下圖:這裡寫圖片描述
  • 非同步日誌類

    • 對於一般的日誌類的實現,(1) 過載<<格式化輸出 (2)級別處理 (3)緩衝區。
    • 為了提高效率並防止阻塞業務執行緒,用一個背景執行緒負責收集日誌訊息,並寫入日誌檔案,其他業務執行緒只管往這個日誌執行緒傳送日誌訊息,這稱為非同步日誌。基本實現仍然是生產者(業務執行緒)與消費者(日誌執行緒)和緩衝區,但是這樣簡單的模型會造成寫檔案操作比較頻繁,因為每一次signal我們就需要進行寫操作,將訊息全部寫入檔案,效率較低。muduo使用多緩衝機制,即mutliple buffering,使用多個緩衝區,當一塊緩衝區寫滿或者時間超時才signal;如果發生訊息堆積,會丟棄只剩2塊記憶體塊。另外使用swap公共緩衝區來避免競爭,一次獲得所有的訊息並寫入檔案。
      這裡寫圖片描述
  • __type_traits技法
    • StringPiece是google的一個高效字串類,其中使用了__type_traits對不同型別進行了進一步優化。在STL中為了提供通用的操作而又不損失效率,traits就是通過定義一些結構體或者類,並利用模板給型別賦予一些特性,這些特性根據型別的不同而異。在程式設計中可以使用這些traits來判斷一個型別的一些特性,實現同一種操作因型別不同而異的效果。可以參照 這篇文章 瞭解。

muduo/net

  • reactor

    • reactor+執行緒池適合CPU密集型,multiple reactors適合突發I/O型,一般一個千兆網口一個rector;multiple rectors(執行緒) + thread pool 更能適應突發I/O和密集計算。其中multiple reactors中的main reactor只註冊OP_ACCEPT事件,並派發註冊I/O事件到sub reactor上監聽,多個sub reactor採用round-robin的機制分配。具體實現見EventLoopThread.
  • TcpConnection

    • TcpConnection是對已連線套接字的抽象;Channnel是selectable IO channel,負責註冊和響應IO事件,但並不擁有file descriptor,
      Channel是Acceptor、Connector、EventLoop、TimerQueue、TcpConnection的成員,生命期由後者控制。
  • TimerQueue類
    • timers_和activeTimers_儲存的是相同的資料,timers_是按到期時間排序,activeTimers_按照物件地址排序,並且timerQueue只關注最早
      的那個定時器,所以當發生可讀事件的時候,需要使用getExpired()獲取所有的超時事件,因為可能有同一時刻的多個定時器。
  • runInLoop
    • runInLoop的實現:需要使用eventfd喚醒的兩種情況 (1) 呼叫queueInLoop的執行緒不是當前IO執行緒。(2)是當前IO執行緒並且正在呼叫pendingFunctor。
  • rvo優化
    • C++的函式返回vector之類或者自定義型別可以避免產生額外的拷貝建構函式、解構函式開銷,返璞歸真。
  • shared_from_this()
    • 獲得自身物件的shared_ptr物件,直接強制轉換會導致引用計數+1
  • TcpConnection生命週期
    • TcpConnection物件不能由removeConnection銷燬,因為如果此時Channel中的handleEvent()還在執行的話,會造成core dump,我們使用shared_ptr管理引用計數為1,在Channel中維護一個weak_ptr(tie_),將這個shared_ptr物件賦值給tie_,引用技術仍為1;當連線關閉,在handleEvent中將tie_提升,得到一個shared_ptr物件,引用計數就為2。
  • Buffer類
    • 自己設計的可變緩衝區,成員變數vector<char>readIndexwriteIndex,同時處理粘包問題。Buffer::readFd()中的extraBuffer通過堆上和棧上空間的結合,避免了記憶體資源的鉅額開銷。先加入棧空間再擴充和直接擴充的區別就是明確知道多少資料,避免巨大的buffer浪費並且減少read系統呼叫。

muduo/examples

  • chargen測試伺服器的吞吐量;千兆網絡卡跑滿大概100M/s(1000M/8),機械硬碟的讀寫速度也就差不多也是這樣,固態硬碟可以達到500M/s。

  • Filetransfer檔案傳輸的時候,每次傳送64k,然後再設定WriteCompleteCallback_再小塊傳送可以避免應用層緩衝區佔用大量記憶體。

  • chat 多人聊天室,mutex保護vector,多條訊息不能並行傳送,存在較高的鎖競爭。優化一:使用shared_ptr實現
    copy_on_write,通過建立副本,達到並行傳送訊息的目的。優化二:訊息到達第一個客戶端和最後一個客戶端之間有延遲,可以放在自己的IO執行緒中傳送。

  • NTP網路時間同步

 Client   server
    |   |
T1  |   |  T2
    |   |  T3
T4  |   |               

RTT = (T4-T1)-(T3-T2)
clock offset = [(T4+T1)-(T2+T3)]/2 (自己加一個係數K,代表客戶端與服務端的時間差,公式輕易推出)

其他關鍵技術點

  • volatile
    • 防止編譯器對程式碼進行優化,對於用這個關鍵字宣告的變數,系統總是從它所在的記憶體讀取資料,不會使用暫存器中的備份。在Atomic.h中使用。
  • __thread
    • __thread修飾的變數是執行緒區域性儲存的,僅限於POD型別和類的指標;非POD型別可以使用執行緒特定資料TSD。
  • 單例模式
    • 一個類只有一個例項,並提供一個訪問他的全域性訪問點。(1)私有建構函式。(2)類定義中含有該類的靜態私有物件。(3)靜態公有函式獲取
      靜態私有物件。
  • 非同步回撥的理解
    • 所謂的非同步回撥,主執行緒使用poll/epoll進行事件迴圈,事件包括各種IO時間和timerfd實現的定時器事件。沒有事件發生時阻塞在poll/epoll處,有事件發生時會對activeChannel進行遍歷呼叫其中的回撥函式。至於定時器是使用硬體時鐘中斷實現的,與sleep這種軟體阻塞不同。所以我們常說的通過寫訊息來喚醒執行緒的含義就是觸發一個IO事件,使得poll/epoll解除阻塞,向下得以執行回撥函式。
  • CAS無鎖操作
    • CAS原語有三個引數,記憶體地址,期望值,新值。如果記憶體地址的值==期望值,表示該值未修改,此時可以修改成新值。否則表示修改失敗,返回false。無鎖佇列的實現可參照CoolShell
    • 注意無鎖結構不一定比有鎖結構更快,鎖指令本身很簡單,真正影響效能的是鎖爭用(Lock Contention)。當contention發生的時候,有鎖的情況會陷入核心睡覺,無鎖情況會不斷spin,其中陷入核心睡覺是有開銷的,這個開銷當臨界區很小的時候所佔的比重就很大,這也就是lockfree在這種情況下效能提高很高的原因。lockfree的意義不在於絕對的高效能,他比mutex的有點是使用lockfree可以避免死鎖/活鎖,優先順序翻轉等問題,但是ABA problem、memory order等問題使得lockfree比mutex難實現得多。除非瓶頸已經確定,否則最好還是老老實實的使用mutex+condvar吧。
  • 避免重複include多餘標頭檔案
    • 在標頭檔案中使用引用或者指標,而不是使用值的,使用前置宣告,而不是直接包含他的標頭檔案。
    • 使用impl手法,簡單來說就是類裡面包含類的指標,在cpp裡面實現。
  • (void)ret
    • 防止編譯警告,變數未使用(限於release版本中 int n = …; assert(n==6))
  • vim行首添加註釋
    • % 1,10s/^/#/g 在1-10行首新增#註釋

參考資料

  • muduo原始碼
  • muduo使用手冊
  • 《linux多執行緒服務端程式設計》