1. 程式人生 > >Mudo C++網路庫第三章學習筆記

Mudo C++網路庫第三章學習筆記

多執行緒伺服器的適用場合與常用程式設計模型

  • 程序間通訊與執行緒同步;
  • 以最簡單規範的方式開發功能正確、執行緒安全的多執行緒程式;
  • 多執行緒伺服器是指執行在linux作業系統上的獨佔式網路應用程式;
  • 不考慮分散式儲存, 只考慮分散式計算;

程序與執行緒

  • 程序(process)是作業系統裡最重要的兩個概念之一(另一個是檔案), 粗略的講, 一個程序是"記憶體中正在執行的程式";
  • 每個程序有自己獨立的地址空間(adress space), "在同一個程序"還是"不在同一個程序"是系統功能劃分的重要決策點;
  • 把程序比喻成人, 電話談只能通過週期性的心跳來判斷對方是否還活著;
    • 容錯, 萬一有人突然死了;
    • 擴容, 新人中途加進來;
    • 負載均衡, 把甲的活挪給乙做;
    • 退休, 甲要修復bug, 先別派新任務, 等他做完手上的事情就把他重啟等等各種場景, 十分便利;
  • 執行緒的特點是共享地址空間, 從而可以高效地共享資料;
  • 如果多個程序大量共享記憶體, 等於是把多程序程式當成多執行緒來寫, 掩耳盜鈴;
  • "多執行緒"的價值, 是為了更好地發揮多核處理器(multi-cores)的效能;
  • 單核用狀態機的思路去寫程式是最高效的;

單執行緒伺服器的常用程式設計模型

  • I/O模型, 客戶端/伺服器設計正規化;
    • 用得最廣的是"non-blocking IO + IO multiplexing"這種模型(非阻塞IO+IO多路複用), 即Reactor模式;
      • lighttpd, 單執行緒伺服器(Nginx與之類似, 每個工作程序都有一個eventloop事件迴圈);
      • libevent, libev;
      • ACE, Poco C++ libraries;
      • Java NIO, 包括Apache Mina 和 Netty;
      • POE(Perl);
      • Twisted(Python);
    • "non-blocking IO + IO multiplexing"這種模型(非阻塞IO+IO多路複用)中, 程式的基本結構是一個事件迴圈(event loop), 以事件驅動(event-driven)和事件回撥的方式實現業務邏輯;
    • select/poll有伸縮性方面的不足, Linux下用epoll來進行替換;
    • Reactor模型的優點很明顯, 程式設計不難, 效率也不錯;
      • 不僅可以用於讀寫socket, 連線的建立(connect/accept)甚至DNS解析都可以用非阻塞方式進行;
      • 以提高併發度和吞吐量(throught), 對於IO密集的應用是個不錯的選擇;
      • lighttpd內部的fdevent結構十分精妙, 值得學習;
    • 基於事件驅動的程式設計模型也有其本質的缺點, 它要求事件回撥函式必須是非阻塞的;
    • 對於涉及網路IO的請求響應式協議, 它容易割裂業務邏輯, 使其散佈於多個回撥函式中, 相對不容易理解和維護;

多執行緒伺服器的常用程式設計模型

  • 大概有幾種:
    • 每請求建立一個執行緒, 使用阻塞式IO操作; 可惜伸縮不佳;
    • 使用執行緒池, 同樣使用阻塞式IO操作, 這是提高效能的措施;
    • 使用non-blocking IO + IO multiplexing; 即Java NIO的方式;
    • Leader/Follower等;
  • 預設情況下, 使用non-blocking IO + IO multiplexing模式來編寫多執行緒C++網路服務程式;
    • 執行緒數目基本固定, 可以在程式啟動的時候設定, 不會頻繁建立與銷燬;
    • 可以很方便地線上程間調配負載;
    • IO事件發生的執行緒是固定的, 同一個TCP連結不必考慮事件併發;
    • Eventloop代表了執行緒的主迴圈, 需要讓哪個執行緒幹活, 就把timer或IO channel(如TCP連線)註冊到哪個執行緒的loop裡即可;
  • 多執行緒程式對event loop提出了更高的要求, 那就是執行緒安全;
    • 多執行緒的Reactor;

執行緒池

  • 對於沒有IO光有計算任務的執行緒, 使用event loop有點浪費, 一種補充方案是用blocking queue實現的任務佇列(TaskQueue);
  • BlockingQueue
  • 無界的BlockingQueue和有界的BoundedBlockingQueue;
    • Intel Threading Building Blocks裡的concurrent_queue
  • 推薦模式:
    • 推薦的C++多執行緒伺服器模式為: one(event) loop per thread + thread pool;
    • event loop(也叫IO loop)用作IO multiplexing, 配合non-blocking IO和定時器;
    • thread pool用來做計算, 具體可以是任務佇列或是生產者消費者佇列;
    • 寫這種方式的伺服器程式, 需要一個優質的基於Reactor模式的網路庫來支撐, muduo正是這樣的網路庫;

程序間通訊只用TCP

  • IPC(程序間通訊)主要有: 匿名管道(pipe), 有名管道(FIFO), POSIX訊息佇列, 共享記憶體, 訊號(signals), 套接字(sockets), 訊號量(semaphore);
  • 同步原語(synchronization primitives): 互斥量(mutex), 條件變數(condition variable), 讀寫鎖(reader-writer lock), 檔案鎖(record locking), 訊號量(semaphore);
  • 貴精不貴多;
  • TCP是雙向的, Linux的pipe是單向的;
  • pipe有一個經典的應用場景, 那就是寫Reactor/event loop時用來非同步喚醒select(或等價的poll/epoll_wait)呼叫;
  • TCP port由一個程序獨佔, 且作業系統會自動回收(listening port和已建立連線的TCP socket都是檔案描述符, 在程序結束時作業系統會關閉所有檔案描述符);
    • 快速failover(故障容錯), 應用層的心跳也必不可少;
  • TCP協議的一個天生的好處是"可記錄, 可重現";
  • TCP連線是可再生的, 連線的任何一方都可以退出再啟動, 重建連線之後就能繼續工作, 對開發牢靠的分散式系統意義重大;
  • TCP這種位元組流(byte stream)方式通訊, 會有marshal/unmarshal(編碼/解碼)的開銷;
    • 這要求我們選用合適的訊息格式(準確地說是wire format-有線格式), 推薦 Google Protocol Buffers;
  • TCP的local吞吐量一點都不低;
  • TCP是位元組流, 只能順序讀取, 有寫緩衝;
    • 共享記憶體是訊息協議, a程序填好一塊記憶體讓b程序來讀, 基本是"stop wait(停等)"方式;
    • 要將這兩種方式揉到一個程式裡, 需要建立一個抽象層, 封裝兩個IPC;
      • 這會增加測試的複雜度(因為不透明);
    • 生產環境下的資料庫伺服器往往是獨立的高配置伺服器, 一般不會同時執行其他佔資源的程式;
  • TCP是個資料流協議, 除了直接使用它通訊外, 還可以在此之上構建RPC/HTTP/SOAP之類的上層通訊協議;
  • 除點對點的通訊之外, 應用級的廣播協議也是非常有用的, 可方便地構建可觀可控的分散式系統;

分散式系統中使用TCP長連線通訊

  • 分散式的軟體設計和功能劃分一般以"程序"為單位;
  • 分散式系統採用TCP長連線通訊;
  • 必要時可以藉助多執行緒來提高效能;
  • 對整個分散式系統, 要做到能scale out, 即享受增加機器帶來的好處;
  • 使用TCP長連線的好處有兩點:
    • 容易定位分散式系統中的伺服器之間的依賴關係;
      • netstat -tpna | grep : port能立即列出客戶端地址;
    • 二是通過接收和傳送佇列的長度也較容易定位網路或程式故障;

多執行緒伺服器的適用場合

  • 開發伺服器端程式的一個基本任務是處理併發連線, 現在服務端網路程式設計處理併發連線主要有兩種方式:
    • 當執行緒很廉價時, 一臺機器上可以建立遠高於CPU數目的執行緒;
    • 當執行緒很寶貴時, 一臺機器上只能建立與CPU數目相當的執行緒;
  • 必須用單執行緒的場合:
    • 程式可能fork;
    • 限制程式的CPU佔用率;
  • 一個程式fork之後, 一般有兩種行為:
    • 立刻執行exec(), 變身為另一個程式(負責啟動job的守護程序);
    • 不呼叫exec(), 繼續運行當前程式;
      • 要麼通過共享的檔案描述符與父程序通訊, 協同完成任務;
      • 要麼接過父程序傳來的檔案描述符, 完成獨立的任務;
  • 只有看門狗執行緒必須堅持單執行緒, 其他的均可替代為多執行緒程式(從功能上講);
  • 單執行緒程式能限制程式的CPU佔用率;
    • 做成單執行緒的能避免過分搶奪系統的計算資源;
  • 單執行緒程式的優缺點:
    • 單執行緒程式的優勢: 簡單, 一個基於IO multiplexing的event loop;
    • event loop有一個明顯的缺點, 它是非搶佔式的(non-preemptive), 有點類似優先順序反轉;
      • 這個缺點可以用多執行緒來克服, 這也是多執行緒的主要優勢;
  • 多執行緒一般沒有效能上的優勢:
    • 用很少的CPU負載就能讓IO跑滿, 或者用很少的IO流量就能讓CPU跑滿, 那麼多執行緒就沒有啥用處;
  • 適用多執行緒程式的場景:
    • 提高響應速度, 讓IO和計算互相重疊, 降低latency(延遲);
    • 雖然多執行緒不能提高絕對效能, 但多執行緒能提高平均響應效能;
    • 一個程式要想做多執行緒, 大致要滿足:
      • 有多個CPU可用;(單核機器上多執行緒沒有效能優勢, 或許能簡化併發業務邏輯的實現);
      • 執行緒間有共享資料, 即記憶體中的全域性狀態, 如果沒有共享資料, 用執行多個單執行緒的程序就行;
      • 提供非均質的服務; 事件的響應有優先順序的差異, 用專門的執行緒來處理優先順序高的事件, 防止優先順序反轉;
      • latency和throughout同樣重要, 不是邏輯簡單的IO bound或CPU bound程式(程式有相當的計算量);
      • 利用非同步操作;
      • 能scale up, 一個好的執行緒程式能享受增加CPU數目帶來的好處;
      • 具有可預測的效能, 執行緒數一般不隨負載變化;
      • 多執行緒能有效地劃分責任和功能, 讓每個執行緒的邏輯比較簡單, 任務單一, 便於編碼;
  • 執行緒的分類:
    • IO執行緒, 這類執行緒的主迴圈是IO multiplexing, 阻塞地等在select/poll/epoll_wait系統呼叫上;
    • 計算執行緒, 這類的主迴圈是blocking queue, 阻塞地等待在condition variable上, 這類執行緒主要位於thread pool中;
    • 第三方庫所用的執行緒, 比如logging, 或database connection;
  • 學習多執行緒程式設計還有一個好處, 即訓練非同步思維, 提高分析併發事件的能力;
    • 執行在多臺機器上的服務程序本質上是非同步的;
    • 熟悉多執行緒程式設計的話, 很容易就能發現分散式系統在訊息和事件處理方面的race condition;

多執行緒伺服器的適用場合

  • 32位Linux, 一個程序的地址空間是4GB, 其中使用者態能訪問3GB左右, 而一個執行緒的預設棧(stack)大小是10MB, 一個程序大概能開300個執行緒;
  • 所謂基於事件指的是用IO multiplexing event loop的程式設計模型, 又稱Reactor模式;
  • 單個的event loop處理1萬個併發長連線並不稀罕, 一個multi-loop的多執行緒程式應該能輕鬆支援5萬併發連線;
  • thread per connection 不適合高併發場合, 其scalability不佳, one loop per thread 的併發度足夠大, 且與CPU數目成正比;
  • 多執行緒能提高吞吐量嗎?
    • 對於計算密集型服務, 不能;
    • 為了在併發請求數很高時也能保持穩定額吞吐量, 我們可以用執行緒池, 執行緒池的大小應該滿足"阻抗匹配原則";
    • 執行緒池也不是萬能的, 如果響應一次請求需要做比較多的計算可以用執行緒池;
    • 如果一次請求響應中, 主要時間是等待IO, 那麼為了進一步提高吞吐量, 往往要用其他程式設計模型, 比如Proactor;
  • 多執行緒能降低響應時間麼?
    • 如果設計合理, 充分利用多核資源的話, 多執行緒可以降低響應時間, 在突發(burst)請求時效果尤為明顯;
  • 多執行緒程式如何讓IO和計算互相重疊, 降低latency(延遲)?
    • 把IO操作(通常是寫操作)通過BlockingQueue交給別的執行緒去做, 自己不必等待;
  • 為什麼第三方庫往往要用自己的執行緒?
    • event loop模型沒有標準實現;
    • libmemcached只支援同步操作;
    • MySQL的官方C API不支援非同步操作;
  • 什麼是執行緒池大小的阻抗匹配原則?
    • 執行緒池的經驗公式T=C/P;
    • 考慮作業系統能靈活、合理地排程sleeping/writing/running執行緒;
  • 除了你推薦的Reactor+thread pool, 還有別的non-trivial多執行緒程式設計模型嗎?
    • 有, Proactor, 如果一次請求響應中要和別的程序打多次交道, 那麼Proactor模型往往能做到更高的併發度;
    • Proactor模式依賴作業系統或庫來高效地排程這些子任務, 每個子任務都會阻塞, 因此能用比較少的執行緒達到很高的IO併發度;
    • Proactor能提高吞吐, 但不能降低延遲;
    • Proactor模式讓程式碼非常破碎, 在C++中使用Proactor是很痛苦的;
  • 多執行緒的程序和多個相同的單執行緒程序如何取捨?
    • 在其他條件相同的情況下, 可以根據工作集(work set)的大小來取捨;
    • 工作集是指服務程式響應一次請求訪問記憶體大小;
    • 如果工作集較大, 那麼就用多執行緒, 避免CPU cache換入換出, 影響效能;否則, 就用單執行緒多程序, 享受單執行緒程式設計的便利;
    • 執行緒不能減少工作量, 即不能減少CPU時間;