スレッドの有効期間

親は子供の世話をしなければなりません。この単純な考え方は、スレッドの存続期間に大きな影響を与えます。次のプログラムは、その ID を表示するスレッドを開始します。

// 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.join() または t.detach 呼び出しがない場合、呼び出し可能ユニット (呼び出し可能ユニットなしでスレッドを作成できます) を持つスレッド t は結合可能です。結合可能なスレッド デストラクタが std::terminate 例外をスローします。したがって、プログラムは終了します。そのため、実際の実行は予期せず終了しました。

この問題の解決策は簡単です。 t.join() を呼び出すことにより、プログラムは本来の動作をします。

// threadWithJoin.cpp

#include <iostream> #include <thread> int main(){ std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;}); t.join(); }

簡単な補足:デタッチの課題

もちろん、上記のプログラムでは t.join() の代わりに t.detach() を使用できます。スレッド t はもう結合できず、そのデストラクタは std::terminate を呼び出しませんでした。オブジェクト std::cout の有効期間が保証されていないため、プログラムの動作が未定義になっているため、悪いようです。プログラムの実行が少しおかしくなりました。

この問題については、次の記事で詳しく説明します。

スレッドの移動

今までは、とても簡単でした。しかし、それは永遠である必要はありません。

スレッドをコピーすること (セマンティックのコピー) はできません。スレッドの移動 (セマンティックの移動) のみが可能です。スレッドが移動された場合、その存続期間を適切に処理することははるかに困難です。

// threadMoved.cpp

#include <iostream> #include <thread> #include <utility> int main(){ std::thread t([]{std::cout << std::this_thread::get_id();}); std::thread t2([]{std::cout << std::this_thread::get_id();}); t= std::move(t2); t.join(); t2.join(); }

両方のスレッド (t1 と t2) は、ID を出力するという単純な仕事をする必要があります。それに加えて、スレッド t2 は t:t=std::move(t2) に移動されます。最後に、メイン スレッドはその子を処理し、それらに参加します。ちょっと待って。それは私の期待とはかけ離れています:

何がうまくいかないのですか? 2 つの問題があります:

<オール>
  • スレッド t2 を移動 (所有権を取得) することにより、t は新しい呼び出し可能ユニットを取得し、そのデストラクタが呼び出されます。したがって、t のデストラクタは std::terminate を呼び出します。これはまだ結合可能であるためです。
  • スレッド t2 には、関連付けられた呼び出し可能ユニットがありません。呼び出し可能なユニットのないスレッドで join を呼び出すと、例外 std::system_error が発生します。
  • 両方のエラーを修正しました。

    // threadMovedFixed.cpp

    #include <iostream> #include <thread> #include <utility> int main(){ std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;}); std::thread t2([]{std::cout << std::this_thread::get_id() << std::endl;}); t.join(); t= std::move(t2); t.join(); std::cout << "\n"; std::cout << std::boolalpha << "t2.joinable(): " << t2.joinable() << std::endl; }

    その結果、スレッド t2 はもう参加できません。

    scoped_thread

    スレッドの存続期間を手動で処理するのが面倒な場合は、独自のラッパー クラスに std::thread をカプセル化できます。このクラスは、デストラクタで自動的に join を呼び出す必要があります。もちろん、逆に detach を呼び出すこともできます。しかし、デタッチにはいくつかの問題があります。

    Anthony Williams は、このような貴重なクラスを作成しました。彼はそれを scoped_thread と呼びました。コンストラクターでは、スレッドが結合可能であることを確認し、最終的にデストラクタで結合します。コピー コンストラクターとコピー代入演算子は削除として宣言されているため、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;})); }

    次は?

    次の投稿では、スレッドへのデータの受け渡しについて説明します。 (校正者 Alexey Elymanov)