スレッドは、コピーまたは参照によってデータを取得します。デフォルトでは、コピーして使用する必要があります。なんで?スレッドが参照によってデータを取得する場合、引数の有効期間について細心の注意を払う必要があります。
スレッド引数
スレッドは可変個引数のテンプレートです。したがって、任意の数の引数を取得できます。
しかしここで、コピーまたは参照による引数の取得の違いについて説明します。
std::string s{"C++11"} std::thread t([=]{ std::cout << s << std::endl;}); t.join(); std::thread t2([&]{ std::cout << s << std::endl;}); t2.detach()
正確に言うと、この例で引数を取得するのはスレッドではなく、ラムダ関数です。しかし、それは私の議論に違いはありません。したがって、最初のスレッド t1 はコピーごとにデータを取得し ([=])、2 番目のスレッド t2 は参照によってデータを取得します ([&])。
これらの行にはどのような危険が隠されていますか?スレッド t2 は参照によって文字列 s を取得し、その後、その作成者の存続期間から切り離されます。一方では、文字列の有効期間は呼び出しコンテキストの有効期間にバインドされ、他方では、グローバル オブジェクト std::cout の有効期間はメイン スレッドの有効期間にバインドされます。そのため、文字列 s の有効期間または std::cout の有効期間が、スレッド t2 の有効期間よりも短い場合があります。現在、私たちは未定義の動作の領域に深く入っています.
納得できませんか?未定義の動作がどのように見えるか、詳しく見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // threadArguments.cpp #include <chrono> #include <iostream> #include <thread> class Sleeper{ public: Sleeper(int& i_):i{i_}{}; void operator() (int k){ for (unsigned int j= 0; j <= 5; ++j){ std::this_thread::sleep_for(std::chrono::milliseconds(100)); i += k; } std::cout << std::this_thread::get_id() << std::endl; } private: int& i; }; int main(){ std::cout << std::endl; int valSleeper= 1000; std::thread t(Sleeper(valSleeper),5); t.detach(); std::cout << "valSleeper = " << valSleeper << std::endl; std::cout << std::endl; } |
問題は、26 行目の valSleeper の値はどれかということです。valSleeper はグローバル変数です。スレッド t は、作業パッケージとして、変数 valSleeper と数値 5 と共に Sleeper 型の関数オブジェクトを取得します (27 行目)。重要な観察は、スレッドが参照によって valSleeper を取得し (9 行目)、メイン スレッドの有効期間から切り離されることです (28 行目)。次に、関数オブジェクトの call 演算子を実行します (10 ~ 16 行目)。このメソッドでは、0 から 5 までカウントし、各反復で 1/10 秒スリープし、i を k ずつインクリメントします。最後に、その ID を画面に表示します。 Adam Riese (ドイツのことわざ) に従って、結果は 1000 + 6 * 5 =1030 でなければなりません。
しかし何が起こった?何かが完全に間違っています。
2 つの問題があります。一方では valSleeper は 1000 ですが、他方ではコンソールに ID がありません。したがって、それは未定義の動作です。その理由は、子スレッドが計算を実行するか、その ID を std::cout に書き込む前に、メイン スレッドの有効期間が終了するためです。
この子スレッドが処理を完了するまで、メイン スレッドが t.join() を介して待機する場合、期待される結果が得られます。
int main(){ std::cout << std::endl; int valSleeper= 1000; std::thread t(Sleeper(valSleeper),5); t.join(); std::cout << "valSleeper = " << valSleeper << std::endl; std::cout << std::endl; }
次は?
スレッドの存続期間について考えることが、スレッドに関する唯一の問題ではありません。メイン スレッドと子スレッドの両方のスレッドが 2 つのオブジェクトを共有します。それは std::cout と変数 valSleeper です。ここで、データ競合、または別の言い方をすれば、未定義の動作の古典的なレシピについて説明しました。次の投稿では、スレッド間の共有変数を扱います。 (校正者 Arne Mertz、 Alexey Elymanov )