C++20 でのスレッドの共同割り込み

私の C++ セミナーでの典型的な質問は、「スレッドを強制終了できますか?」です。 C++20 より前では、私の答えはノーです。 C++20 では、スレッドの中断を丁寧に尋ねることができます。

初めに。スレッドを強制終了するのが得策ではないのはなぜですか?答えはとても簡単です。スレッドを強制終了したときにスレッドがどの状態にあるかはわかりません。考えられる悪意のある結果は 2 つあります。

  • スレッドの仕事はまだ半分しか終わっていません。したがって、そのジョブの状態がわからないため、プログラムの状態もわかりません。あなたは未定義の行動で終わり、すべての賭けはオープンです。
  • スレッドがクリティカル セクションにあり、ミューテックスをロックしている可能性があります。スレッドがミューテックスをロックしているときにスレッドを強制終了すると、高確率でデッドロックが発生します。

わかりました、スレッドを強制終了するのは良い考えではありません。たぶん、スレッドフレンドリーに停止するかどうかを尋ねることができます.これはまさに、C++20 での協調割り込みが意味するものです。あなたがスレッドに尋ねると、スレッドはあなたの中断の希望を受け入れるか無視することができます.

共同中断

C++20 の協調割り込みスレッドの追加機能は、std::stop_token に基づいています。 、std::stop_callback 、および std::stop_source データ型。

std::stop_tokenstd::stop_callback 、および std::stop_source

std::stop_token std::stop_callback 、または std::stop_source スレッドが非同期的に実行の停止を要求したり、実行が停止シグナルを受け取ったかどうかを尋ねたりできるようにします。 std::stop_token 操作に渡した後、停止リクエストのトークンをアクティブにポーリングするか、std::stop_callback を介してコールバックを登録するために使用できます .停止要求は std::stop_source によって送信されます .この信号は関連するすべての std::stop_tokenに影響します . 3 つのクラス std::stop_sourcestd::stop_token 、および std::stop_callback 関連する停止状態の所有権を共有します。呼び出し request_stop()stop_requested() 、および stop_possible() アトミックです。

std::stop_source を構築できます 2 つの方法で:

stop_source(); // (1)
explicit stop_source(std::nostopstate_t) noexcept; // (2)

デフォルトのコンストラクタ (1) は std::stop_source を構築します 新しい停止状態で。 std::nostopstate_t を取るコンストラクタ (2) 空の std::stop_source を構築します 関連する停止状態なし。
コンポーネント std::stop_source src 停止リクエストを処理するための次のメンバー関数を提供します。

src.stop_possible() src という意味です 関連する停止状態があります。 src.stop_requested() true を返します srcのとき 関連する停止状態があり、以前に停止するように求められていません。 src.request_stop() 成功し、true を返します src の場合 関連する停止状態があり、以前に停止するように要求されていません。

呼び出し src.get_token() 停止トークン stokenを返します . stoken に感謝 関連する停止ソース src に対して停止要求が行われたか、または行うことができるかどうかを確認できます。 .停止トークン stoken 停止ソース src を観察します .

次の表は、std::stop_token stoken のメンバー関数を示しています。 .

停止状態が関連付けられていない、デフォルトで構築されたトークン。 stoken.stop_possible true も返します stoken の場合 関連する停止状態があります。 stoken.stop_requested() true を返します 停止トークンに関連付けられた停止状態があり、既に停止要求を受け取っている場合。


std::stop_token の場合 一時的に無効にする必要がある場合は、デフォルトで構築されたトークンに置き換えることができます。デフォルトで構築されたトークンには、関連する停止状態がありません。次のコード スニペットは、停止要求を受け入れるスレッドの機能を無効および有効にする方法を示しています。

std::jthread jthr([](std::stop_token stoken) {
 ...
 std::stop_token interruptDisabled;
 std::swap(stoken, interruptDisabled); // (1)
 ... // (2)
 std::swap(stoken, interruptDisabled);
 ...
}

std::stop_token interruptDisabled 関連する停止状態はありません。これはスレッド jthrを意味します 行 (1) と (2) を除くすべての行で、停止要求を受け入れることができます。

コード スニペットを注意深く調べると、使用されている std::jthread. std::jthread について疑問に思うかもしれません。 C++20 ではエクステンド std::thread です C++11 で。 j jthread で デストラクタで自動的に結合するため、joinable の略です。最初の名前は ithread でした .理由は次のとおりです: 割り込み可能を表します。 std::jthread を提示します 次の投稿で。

次の例は、std::jthread. を使用したコールバックの使用を示しています。

// invokeCallback.cpp

#include <chrono>
#include <iostream>
#include <thread>
#include <vector>

using namespace::std::literals;

auto func = [](std::stop_token stoken) { // (1)
 int counter{0};
 auto thread_id = std::this_thread::get_id();
 std::stop_callback callBack(stoken, [&counter, thread_id] { // (2)
 std::cout << "Thread id: " << thread_id 
 << "; counter: " << counter << '\n';
 });
 while (counter < 10) {
 std::this_thread::sleep_for(0.2s);
 ++counter;
 }
 };

int main() {
 
 std::cout << '\n';
 
 std::vector<std::jthread> vecThreads(10);
 for(auto& thr: vecThreads) thr = std::jthread(func);
 
 std::this_thread::sleep_for(1s); // (3)
 
 for(auto& thr: vecThreads) thr.request_stop(); // (4)

 std::cout << '\n';
 
}

10 個のスレッドのそれぞれがラムダ関数を呼び出します func (1)。コールバック (2) はスレッド id を表示します そして counter .メインスレッドの 1 秒間のスリープ (3) と子スレッドのスリープにより、コールバックが呼び出されたときのカウンターは 4 です。呼び出し thr.request_stop() 各スレッドでコールバックをトリガーします。

次は?

前述のとおり、std::thread from C++11 には大きな弱点が 1 つあります。参加するのを忘れると、そのデストラクタは std::terminate を呼び出します 、そしてあなたのプログラムはクラッシュしました。 std::jthread (C++20) は、この直感に反する弱点を克服し、割り込み可能でもあります。