C++20 の新しいスレッド:std::jthread

CppCon 2018 ワークショップの参加者の 1 人が、「std::thread を中断できますか?」と尋ねました。いいえ、私の答えでしたが、これはもう正しくありません。 C++20 では、std::jthread を取得する場合があります。

CppCon 2018 の話を続けましょう。同時実行ワークショップの休憩中に、Nicolai (Josuttis) と話をしました。彼は私に、新しい提案 P0660:Cooperatively Interruptible Joining Thread についてどう思うか尋ねました。この時点で、私はその提案を知りませんでした。 Nicolai は、Herb Sutter および Anthony Williams とともに、提案の作成者の 1 人です。今日の投稿は、同時未来についてです。これは、現在および今後の C++ における同時実行の全体像です。

Cooperatively Interruptible Joining Thread という論文のタイトルから、新しいスレッドには 2 つの新しい機能があると推測されるかもしれません。割り込み可能と自動結合です。最初に自動参加について書きます。

自動参加

これは、std::thread の非直感的な動作です。 std::thread がまだ結合可能な場合、 std::terminate がそのデストラクタで呼び出されます。 thr.join() または thr.detach() が呼び出された場合、スレッド thr は結合可能です。

// threadJoinable.cpp

#include <iostream>
#include <thread>

int main(){
 
 std::cout << std::endl;
 std::cout << std::boolalpha;
 
 std::thread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }};
 
 std::cout << "thr.joinable(): " << thr.joinable() << std::endl;
 
 std::cout << std::endl;
 
}

実行されると、プログラムは終了します。

両方のスレッドが終了します。 2 回目の実行では、スレッド thr はメッセージ「Joinable std::thread」を表示するのに十分な時間があります。

次の例では、ヘッダー を「jthread.hpp」に置き換え、今後の C++ 標準の std::jthread を使用します。

// jthreadJoinable.cpp

#include <iostream>
#include "jthread.hpp"

int main(){
 
 std::cout << std::endl;
 std::cout << std::boolalpha;
 
 std::jthread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }};
 
 std::cout << "thr.joinable(): " << thr.joinable() << std::endl;
 
 std::cout << std::endl;
 
}

これで、スレッド thr は、この場合のようにまだ結合可能であれば、そのデストラクタで自動的に結合します。

std::jthread を中断する

一般的なアイデアを得るために、簡単な例を示しましょう。

// interruptJthread.cpp

#include "jthread.hpp"
#include <chrono>
#include <iostream>

using namespace::std::literals;

int main(){
 
 std::cout << std::endl;
 
 std::jthread nonInterruptable([]{ // (1)
 int counter{0};
 while (counter < 10){
 std::this_thread::sleep_for(0.2s);
 std::cerr << "nonInterruptable: " << counter << std::endl; 
 ++counter;
 }
 });
 
 std::jthread interruptable([](std::interrupt_token itoken){ // (2)
 int counter{0};
 while (counter < 10){
 std::this_thread::sleep_for(0.2s);
 if (itoken.is_interrupted()) return; // (3)
 std::cerr << "interruptable: " << counter << std::endl; 
 ++counter;
 }
 });
 
 std::this_thread::sleep_for(1s);
 
 std::cerr << std::endl;
 std::cerr << "Main thread interrupts both jthreads" << std:: endl;
 nonInterruptable.interrupt();
 interruptable.interrupt(); // (4)
 
 std::cout << std::endl;
 
}

メイン プログラムで、割り込み不可と割り込み可能の 2 つのスレッドを開始しました (1 行目と 2 行目)。スレッド nonInterruptable とは対照的に、スレッド interruptable は std::interrupt_token を取得し、3 行目でそれを使用して中断されたかどうかを確認します:itoken.is_interrupted()。割り込みの場合、ラムダ関数が戻るため、スレッドは終了します。 interruptable.interrupt() の呼び出し (4 行目) は、スレッドの終了をトリガーします。これは、効果のない以前の呼び出し nonInterruptable.interrupt() には当てはまりません。

割り込みトークン、結合スレッド、および条件変数の詳細は次のとおりです。

割り込みトークン

割り込みトークン std::interrupt_token は共有所有権をモデル化し、トークンが有効な場合に 1 回通知するために使用できます。 valid、is_interrupted、interrupt の 3 つのメソッドを提供します。

割り込みトークンを一時的に無効にする必要がある場合は、デフォルトで構築されたトークンに置き換えることができます。デフォルトで構築されたトークンは無効です。次のコード スニペットは、スレッドがシグナルを受け入れる機能を無効および有効にする方法を示しています。

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

std::interrupt_token interruptDisabled は無効です。これは、スレッドが行 (1) から (2) までの割り込みを受け入れることができないが、行 (2) の後では可能であることを意味します。

スレッドの結合

std::jhread は、割り込みを通知し、自動的に join() する追加機能を備えた std::thread です。この機能をサポートするために、std::interrupt_token があります。

条件変数の新しい待機オーバーロード

std::condition_variable の 2 つの待機バリエーション wait_for と wait_until は、新しいオーバーロードを取得します。 std::interrupt_token を取ります。

template <class Predicate>
bool wait_until(unique_lock<mutex>& lock, 
 Predicate pred, 
 interrupt_token itoken);

template <class Rep, class Period, class Predicate>
bool wait_for(unique_lock<mutex>& lock, 
 const chrono::duration<Rep, Period>& rel_time, 
 Predicate pred, 
 interrupt_token itoken);

template <class Clock, class Duration, class Predicate>
bool wait_until(unique_lock<mutex>& lock, 
 const chrono::time_point<Clock, Duration>& abs_time, 
 Predicate pred, 
 interrupt_token itoken);

これらの新しいオーバーロードには、述語が必要です。渡された std::interrupt_token itoken に対して割り込みが通知された場合、バージョンは確実に通知を受け取ります。待機呼び出しの後、割り込みが発生したかどうかを確認できます。

cv.wait_until(lock, predicate, itoken);
if (itoken.is_interrupted()){
 // interrupt occurred
}

次は?

前回の投稿で約束したように、次の投稿は概念を定義するための残りのルールについてです。