C++線程池實現原理

jopen 9年前發布 | 10K 次閱讀 線程池 C/C++開發

 
C++線程池實現原理

『背景』

C++多線程編程是C++開發者的一個基本功, 但是很多開發者都是直接使用公司給包裝好的線程池庫, 沒有去了解具體實現,有些實現也都因為高度優化而寫得諱莫如深,讓初學者看得吃力。

所以寫這篇文章主要是想以非常簡單的方式講講實現原理, 希望初學者看完之后不是覺得「不明覺厲」,而是覺得「原來如此」。

『面朝代碼』

首先先來一段超級簡單(注釋豐富)的代碼展示多線程編程的經典寫法。

注: 該段代碼和完整運行示例請見 limonp-thread-pool-programming-example , 可以通過以下命令跑通示例代碼和輸出結果,建議嘗試以下。當然記得前提是該機器的網絡可以訪問 GitHub 的情況下。 可能因為在中國訪問 GitHub 并不是很穩定,不是每次都能成功,所以如果卡住的話就多make幾次。

git clone https://github.com/yanyiwu/practice
cd practice/cpp/limonp-v0.5.1-demo
make

#include "limonp/ThreadPool.hpp"
#include "limonp/StdExtension.hpp"

using namespace std;

const size_t THREAD_NUM = 4;

// 這個類本身沒什么實際含義,只是為了示例多線程編程而寫出來的而已。
class Foo {
 public:
  // 這個類成員函數Append會在多個線程中被調用,多個線程同時對 chars 這個類成員變量進行寫操作,所以需要加鎖保證線程安全。
  void Append(char c) {
      limonp::MutexLockGuard lock(mutex_);
      chars.push_back(c); 
  }

  string chars; // 多線程共享的對象
  limonp::MutexLock mutex_; // 線程鎖
};

void DemoClassFunction() {
  Foo foo;
  cout << foo.chars << endl;
  // 初始化一個線程池。
  limonp::ThreadPool thread_pool(THREAD_NUM);
  thread_pool.Start(); // 啟動線程池
  for (size_t i = 0; i < 20; i++) {
      char c = i % 10 + '0';
      // 使用 NewClosure 綁定 foo 對象和 Append 函數和對應參數,構造一個閉包扔進線程池中運行,關于這個 NewClosure 后面會講。
      thread_pool.Add(limonp::NewClosure(&foo, &Foo::Append, c));
  }
  thread_pool.Stop(); // 等待所有線程工作(工作是指NewClosure生成的閉包函數)都完成,然后停止所有線程。
  cout << foo.chars << endl;
}

int main() {
  DemoClassFunction();
  return 0;
}

上面代碼注釋已經非常詳細,一路看下來可能最困惑的應該是 NewClosure 閉包創建函數。 很多人談到閉包就覺得是一個很高深或者是坑很深的問題,但是此閉包非JavaScript的閉包。 是一個簡單版本,只考慮值拷貝,不考慮引用類型的參數。

limonp::NewClosure(&foo, &Foo::Append, c)

所以在該代碼中三個參數,第一個是類對象的指針,第二個是成員函數的指針,第三個是int,都是值傳遞。 在此不支持引用傳遞的參數。

『簡單閉包實現』

假設你現在已經運行了剛才的這三行命令。

git clone https://github.com/yanyiwu/practice
cd practice/cpp/limonp-v0.5.1-demo
make

一切運行正常的話,應該目前所在的目錄里面有 limonp-0.5.1 這個目錄。 然后可以在

limonp-0.5.1/include/limonp/Closure.hpp

文件中看到 NewClosure 函數的定義,NewClosure 是一個模板函數,所以才能支持多種多樣的類型。 但是也讓代碼變得晦澀很多。

但是 NewClosure 的實現原理最主要的是生成一個滿足 ClosureInterface 接口定義的對象而已。

同時這個 ClosureInterface 也異常的簡單,如下:

class ClosureInterface {
 public:
  virtual ~ClosureInterface() {
    }
  virtual void Run() = 0;
};

因為當這個 Closure 對象被扔進線程池之后,其實是進入了一個隊列中。 然后線程池的多個線程去從隊列中獲取Closure指針,然后調用該 Run() 函數。

注意到,NewClosure 其實是生成一個對象指針,我們只是構造了這個對象,但是沒有銷毀它? 是否會造成內存泄露? 顯然不會,原因是在線程池中,調用完 Closure 中的 Run() 函數之后, 會 delete 該指針,所以不會造成內存泄露。

到這里只剩下一個最關鍵的困惑點就是 Run() 的具體實現。 也就是如何通過如下代碼將 Append 這個類成員函數變成一個實現了 Run() 函數的 Closure 對象?

limonp::NewClosure(&foo, &Foo::Append, c) 

因為 NewClosure 是模板函數,支持各種參數類型,但是對于本文的例子,實際上調用的 NewClosure 函數實現如下:

template<class R, class Obj, class Arg1>
ClosureInterface* NewClosure(Obj* obj, R (Obj::* fun)(Arg1), Arg1 arg1) {
  return new ObjClosure1<Obj, R (Obj::* )(Arg1), Arg1>(obj, fun, arg1);
}

所以實際上創建的對象是 ObjClosure1 , 依然是在源碼

limonp-0.5.1/include/limonp/Closure.hpp

中可以找到 ObjClosure1 的實現,如下代碼:

template <class Obj, class Funct, class Arg1>
class ObjClosure1: public ClosureInterface {
 public:
  ObjClosure1(Obj* p, Funct fun, Arg1 arg1) {
     p_ = p;
     fun_ = fun;
     arg1_ = arg1;
    }
  virtual ~ObjClosure1() {
    }
  virtual void Run() {
      (p_->*fun_)(arg1_);
    }
 private:
  Obj* p_;
  Funct fun_;
  Arg1 arg1_;
};

『真相大白』

到這里真相基本上就浮出水面了。 類的對象指針,成員函數指針,函數的參數,都作為 ObjClosure1 的類成員變量存儲著。 然后在函數 Run() 里面再用起來。

virtual void Run() {
    (p_->*fun_)(arg1_);
}

可能看到這里的時候有點不理解,這里就涉及到類成員函數指針的調用。 顯然因為類的成員函數是需要 this 指針的,這個寫過 C++ 的應該都知道。 所以直接像C語言那樣調用函數(如下),顯然是不可能的,沒有傳入 this 指針嘛。

(*fun)(arg1_); 

所以在 C++ 中,調用類的成員函數指針,是如下這樣:

(p_->*fun_)(arg1_); 

這樣才能把 p_ 當成 this 指針傳進去。

這樣就實現了之前的 NewClosure 將 「類,類成員函數指針,函數參數」打包成一個閉包供線程池調用的意圖。

所以就有了最開始示例代碼那種寫法。

『最后』

感覺自己寫的還是很通俗易懂的,有具體代碼,而且還是可一鍵下載運行的, 然后最底層的代碼實現也都解釋了。 應該算是很對得起本文題目了吧。

『題圖』

題圖是愛因斯坦,愛因斯坦代表高智商的大腦。 而在計算機領域,單核CPU計算性能已經很難有大的突破,發展趨勢是多核計算。 所以多線程編程是這個時代的必備基礎技能。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!