執行緒池的自我修養

雀觀程式碼發表於2019-04-06

  原文連結-執行緒池的自我修養      

  最近重構行情服務端的框架,其中有一部分就是重寫mysql執行緒池,執行緒池是一個很獨立的東西,今天就拿出來給大家分享, 怎樣設計一個執行緒池, 以及我是怎麼做的.

為什麼要使用執行緒池

  常見的執行緒池使用場景分為兩種

  1. 大量計算, 充分利用多核

  這個很好理解, 當程式需要大量計算, 單核CPU跑到100%, 這個時候可以將計算任務分解, 分多個執行緒計算, 如果我們有4核, 那這個時候我們可以跑到400%, 理想情況下, 可以節省3倍的時間. 當然這個不是絕對的, 具體情況要具體分析. 總而言之, 是為了讓程式充分打滿CPU.

  1. 同步阻塞,轉非同步回撥

  如果這個是web程式, 非同步絕對是提高併發的神器. 在我們的C++伺服器中, 也會有大量的阻塞任務, 可能是讀取mysql, 可能是讀取mongodb, 或者任意需要同步等待完成的事情, 那麼在等待的時候, 我們的工作執行緒是完全沒法做別的工作的, 這個時候我們就把等待的過程, 變成一個任務, 讓執行緒池去做, 主執行緒繼續處理別的工作, 等執行緒池完成之後, 再接管任務, 繼續往下面執行.

  這是兩種完全不同的工作內容, 看上去都是執行緒池, 需要注意的細節, 是完全不一樣的, 比如開啟的執行緒數量, 大量計算的時候, 我們開的執行緒, 儘量是小於CPU數量的, mysql訪問的時候, 執行緒數一定是不能高於mysql的併發數的. 這種細節很多, 不同的情景情況不一樣, 不能一概而就.

執行緒池的自我修養

  今天我要給大家分享的執行緒池, 拋開任務的細節, 主要講我們應該怎樣去設計一個執行緒池.

一圖勝千言萬語

執行緒池的自我修養

  不管任務多麼複雜, 最終都在這個模型上. 重點可以分為下面幾個:

  • 執行緒間的通訊
  • 排程執行緒的設計
  • 任務的抽象

  每個點的設計, 不同的人有不同的方法, 向大家分享我的方法, 主要針對的是mysql執行緒池的設計, 僅供大家參考.

執行緒間通訊

  執行緒間通訊有很多種方法, 可能是訊號, 可能是管道, 可能是套接字, 我比較喜歡更高階的封裝zmq. 不管怎樣的通訊方式, 我們需要保證下面兩點:

  • 全非同步

  不管是主工作執行緒與排程執行緒之間, 還是排程執行緒與執行緒池執行緒之間, 一定是非同步完成, 絕對不允許同步, 任何地方有同步邏輯, 將成為整個執行緒池的瓶頸.

  • 一問一答

  一個請求, 只能返回一次, 絕對不能一問多答, 更不能只問不答. 執行緒池要向主工作執行緒保證, 過來的請求, 一定會返回, 並且有且只有一次返回. 同時我建議, 如果執行緒池內部發生執行異常, 不要做二次嘗試, 直接將異常標記返回.

  通訊模組的設計, 要保證簡單高效, 給外面暴露的介面簡單到只有接收任務和傳送結果兩個介面, 過多冗餘的設計, 只是無畏的增加了複雜度.

排程執行緒

先上張圖

執行緒池的自我修養

排程執行緒需要關注的也是兩點:

  • 外部訊息佇列

  這部分我也喜歡交給zmq去做, 有任何訊息的時候直接回撥, 這裡我將外部主執行緒訊息與執行緒池訊息都放在一個訊息佇列, 既符合先進先出的模式, 也符合單執行緒同步執行的邏輯.

  • 任務佇列

  當過來的任務超過執行緒池真實併發數量的時候, 我們會將任務快取在佇列, 然後當工作執行緒執行完任務的時候,或者有新的任務過來的時候, 我們都會去檢查是否有空餘的工作執行緒, 然後將任務分配給工作執行緒.

任務的抽象

  將所有的工作抽象成通用的任務, 得益於C++的型別轉換, 我們可以將所有的入參, 和出參都打包成一個void*, 然後將具體執行任務的過程, 使用一個靜態函式, 這樣打包一個通用的工作任務.

/**
 * @brief 給db層傳送的引數
 */
struct DBParam
{
    DBParam():
        m_type(fund_begin),
        m_seq(0)
    {}
    //! 需要執行的sql
    std::string m_str_sql;
    //! db的型別
    db_res_type m_type;
    //! 請求的seq
    uint64_t m_seq;
};

/**
 * @brief 從db返回的資料
 */
class ResFund:
        public ResBase
{
public:
    ResFund(){}
    //! 基礎資料集合
    std::vector<FundInfo> m_vec_funds;
};

//! 交給各個服務的正真執行sql的回撥函式
typedef void (*DBQueryHandler)(MYSQL* con, void* param, void* res)

class DbMessage: 
    public MessageBase{
public:
	/**
	 * @brief 建構函式
	 */
	DbMessage();
	/**
	 * @brief 解構函式
	 */
	virtual ~DbMessage();
	//! 需要執行的引數
	void* m_params;
	//! 執行之後, 產生的結果資訊
	void* m_msg;
	//! 執行mysql的回撥
	DBQueryHandler m_handle_fun;
};
複製程式碼

  上面的程式碼刪除了一些敏感的資訊, 將主體拿出來, 大致表示我是怎麼打包一個任務的. 事實上不管執行緒池做得多麼的好, 業務千變萬化, 我們很難滿足的, 而我們這個任務的封裝最主要的就是把業務封裝到任務裡面, 我們通過一個DBQueryHandler的回撥函式, 主工作執行緒將自己的業務寫到回撥裡面, 交由工作執行緒完成, 進而實現業務的千變萬化.

無鎖程式設計

  看過大多執行緒池的實現, 很多人都喜歡用鎖, 比如訊息佇列, 任務佇列, 用各種鎖來競爭, 進而實現任務的分發, 不敢說這個效能怎麼樣, 但是一旦扯上鎖, 整個程式碼複雜度就上去了, 一處用鎖, 到處加鎖. 這個執行緒池的設計是完全沒有任何鎖的, 單執行緒內部完全是訊息驅動, 執行緒間訊息投遞, 簡單高效.

簡單,簡單,再簡單

  執行緒池的設計見仁見智, 不同的設計可能基於不同的需求, 沒有銀彈. 但是一定要把介面設計得簡單, 不要有酷炫吊炸天的功能, 良好的文件, 對使用者友好, 一眼就能看懂的介面, 才是我們要追求的, 一句話, 簡單,簡單,再簡單.   

歡迎大家訂閱雀觀程式碼, 我將給你講述, 中小企業程式設計師, 淘金路上的故事.

執行緒池的自我修養

相關文章