C++ コア ガイドライン:子スレッドの処理

新しい子スレッドを作成するときは、重要な質問に答える必要があります:子スレッドを待つべきか、それとも自分自身を切り離すべきか?新しく作成された子から自分自身を切り離し、作成者としてのあなたの人生にバインドされている変数を子供が使用すると、新しい問題が生じます:変数は子スレッドの存続期間中有効なままになりますか?

子スレッドの有効期間と変数を慎重に処理しないと、未定義の動作が発生する可能性が高くなります。

子スレッドとその変数の寿命の問題を正確に扱う今日のルールは次のとおりです。

  • CP.23:thread の結合について考えてみましょう スコープ付きコンテナとして
  • CP.24:thread を考えてみてください グローバルコンテナとして
  • CP.25:gsl::joining_thread を優先 std::thread以上
  • CP.26:detach() しないでください スレッド

今日のルールは相互に強く依存しています。

スコープ コンテナーとグローバル コンテナーに関するルール CP.23 と CP.24 は少し奇妙に聞こえるかもしれませんが、参加または切り離す子スレッドの違いを説明するのに非常に適しています。

CP.23:thread の結合を考える スコープ付きコンテナと CP.24 として:thread を考えてください グローバルコンテナとして

以下は、C++ コア ガイドラインのコード スニペットのわずかなバリエーションです:

void f(int* p)
{
 // ...
 *p = 99;
 // ...
}

int glob = 33;

void some_fct(int* p) // (1)
{
 int x = 77;
 std::thread t0(f, &x); // OK
 std::thread t1(f, p); // OK
 std::thread t2(f, &glob); // OK
 auto q = make_unique<int>(99);
 std::thread t3(f, q.get()); // OK
 // ...
 t0.join();
 t1.join();
 t2.join();
 t3.join();
 // ...
}

void some_fct2(int* p) // (2)
{
 int x = 77;
 std::thread t0(f, &x); // bad
 std::thread t1(f, p); // bad
 std::thread t2(f, &glob); // OK
 auto q = make_unique<int>(99);
 std::thread t3(f, q.get()); // bad
 // ...
 t0.detach();
 t1.detach();
 t2.detach();
 t3.detach();
 // ...
}

関数 some_fct (1) と some_fct2 (2) の唯一の違いは、最初のバリエーションは作成されたスレッドに参加しますが、2 番目のバリエーションはすべての作成されたスレッドを切り離すことです。

まず、子スレッドを結合または分離する必要があります。そうしないと、子スレッドのデストラクタで std::terminate 例外が発生します。この問題については、次のルール CP.25 で書きます。

子スレッドの結合と切り離しの違いは次のとおりです:

  • 参加するには スレッドとは、ガイドラインによると、スレッドがスコープ付きコンテナーの一種であることを意味します。何?その理由は、スレッド thr での thr.join() 呼び出しが同期ポイントであるためです。 thr.join() は、スレッドの作成者がその子が完了するまで待機することを保証します。逆に言えば。子スレッド thr は、それが作成された外側のスコープのすべての変数 (状態) を使用できます。したがって、関数 f のすべての呼び出しは明確に定義されています。
  • 逆に、デタッチすると、これは成立しません すべての子スレッド。切り離すということは、あなたは子供のハンドルを失い、あなたの子供はあなたよりも長生きすることさえあります.このため、グローバル スコープの子スレッド変数でのみ安全に使用できます。ガイドラインによると、子スレッドは一種のグローバル コンテナーです。この場合、外側のスコープから変数を使用することは、未定義の動作です。

離れたスレッドにイライラする場合は、例えを挙げましょう。ファイルを作成し、ファイルへのハンドルを失った場合、ファイルはまだ存在します。同じことが切り離されたスレッドにも当てはまります。スレッドをデタッチすると、「実行スレッド」は引き続き実行されますが、「実行スレッド」へのハンドルが失われます。お察しのとおり、t0 は std::thread t0(f, &x) の呼び出しで開始された実行スレッドのハンドルです。

すでに述べたように、子スレッドに参加するか切り離す必要があります。

CP.25:gsl::joining_thread を優先 std::thread以上

次のプログラムでは、スレッド t に参加するのを忘れていました。

// threadWithoutJoin.cpp

#include <iostream>
#include <thread>

int main(){

 std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;});

}

プログラムの実行は突然終了します。

そして今、説明:

作成されたスレッド t の存続期間は、その呼び出し可能ユニットで終了します。作成者には 2 つの選択肢があります。まず、子が完了するまで待機します (t.join())。 2 番目:t.detach() により、自身を子から切り離します。呼び出し可能ユニットを持つスレッド t (呼び出し可能ユニットなしでスレッドを作成できます) は、t.join() または t.detach() 呼び出しが発生しない場合、結合可能と呼ばれます。結合可能なスレッドのデストラクタは、std::abort で終了する std::terminate 例外をスローします。したがって、プログラムは終了します。

gsl::joining_thread はスコープの最後で自動的に結合するため、ルールは「std::thread よりも gsl::joining_thread を優先する」と呼ばれます。悲しいことに、ガイドライン サポート ライブラリに gsl::joining_thread の実装が見つかりませんでした。 Anthony Williams の scoped_thread のおかげで、これは実際には問題になりません:
// scoped_thread.cpp

#include <iostream>
#include <thread>
#include <utility>


class scoped_thread{
 std::thread t;
public:
 explicit scoped_thread(std::thread t_): t(std::move(t_)){
 if ( !t.joinable()) throw std::logic_error("No thread");
 }
 ~scoped_thread(){
 t.join();
 }
 scoped_thread(scoped_thread&)= delete;
 scoped_thread& operator=(scoped_thread const &)= delete;
};

int main(){

 scoped_thread t(std::thread([]{std::cout << std::this_thread::get_id() << std::endl;}));

}

scoped_thread は、指定されたスレッドが結合可能かどうかをコンストラクターでチェックし、デストラクタで指定されたスレッドを結合します。

CP.26:detach() しないでください スレッド

このルールは奇妙に聞こえます。 C++11 標準では、スレッドのデタッチをサポートしていますが、それを行うべきではありません!その理由は、スレッドの切り離しが非常に困難になる可能性があるためです。ルール C.25 が言ったように:CP.24:thread を考えてください グローバルコンテナとして。もちろん、これは、デタッチされたスレッドでグローバル スコープを持つ変数のみを使用する場合、まったく問題がないことを意味します。いいえ!

持続時間が静的なオブジェクトでさえ、重要な場合があります。たとえば、動作が定義されていないこの小さなプログラムを見てください。

#include <iostream>
#include <string>
#include <thread>

void func(){ std::string s{"C++11"}; std::thread t([&s]{ std::cout << s << std::endl;}); t.detach(); }

int main(){
func();
}

かんたんだよ。ラムダ関数は参照によって s を取ります。子スレッド t がスコープ外の変数 s を使用するため、これは未定義の動作です。止まる!これは明らかな問題ですが、隠れた問題は std::cout です。 std::cout には静的な期間があります。これは、std::cout の有効期間がプログラムの終了で終了し、さらに競合状態があることを意味します:スレッド t はこの時点で std::cout を使用する可能性があります。

次は?

C++ コア ガイドラインの同時実行の規則はまだ完了していません。次の投稿では、さらに多くのルールが続きます。それらは、データをスレッドに渡し、スレッド間で所有権を共有し、スレッドの作成と破棄のコストに関するものです。