C++スレッドプール実現原理

8526 ワード

背景
マルチスレッドプログラミングは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)

したがって、このコードには3つのパラメータがあり、1つ目はクラスオブジェクトのポインタであり、2つ目はメンバー関数のポインタであり、3つ目はintであり、いずれも値伝達である.リファレンス伝達のパラメータはサポートされていません.
単純クローズ実装
さっきの3行のコマンドを実行したとします.
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, fun, arg1);
}

実際に作成されたオブジェクトはObjClosure 1であり、ソースコードである
limonp-0.5.1/include/limonp/Closure.hpp

をクリックしてください.
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_;
};

真相が明らかになる
ここまで来ると真相は基本的に浮かび上がった.クラスのオブジェクトポインタ、メンバー関数ポインタ、関数のパラメータは、ObjClosure 1のクラスメンバー変数として格納されています.そして関数Run()の中でまた使います.
virtual void Run() {
    (p_->*fun_)(arg1_);
}

ここを見たときに少し理解できないかもしれませんが、ここではクラスメンバー関数ポインタの呼び出しに関連しています.明らかにクラスのメンバー関数にはthisポインタが必要なので、これはC++と書いたことがあるはずです.だから直接C言語のように関数(以下)を呼び出すのは、明らかに不可能で、thisポインタが伝わっていませんか.
(*fun)(arg1_);

したがって、C++では、クラスのメンバー関数ポインタを呼び出すと、次のようになります.
(p_->*fun_)(arg1_);

これでやっとp_このポインタとして伝わります.
これにより、以前のNewClosureで「クラス、クラスメンバー関数ポインタ、関数パラメータ」をスレッドプール呼び出しのための閉パッケージにパッケージ化する意図が実現しました.
最初のサンプルコードのような書き方があります.
最後に
自分が書いたのは分かりやすくて、具体的なコードがあって、しかもワンタッチでダウンロードして実行することができて、それから最下層のコードの実現もすべて説明しました.本文のテーマにふさわしいだろう.