C++ futures part 1: メンタルモデル
C++ futures part 1: The mental model (devblogs.microsoft.com)
要約
この記事は、C++標準ライブラリにおける`std::promise`、`std::packaged_task`、`std::async`、`std::future`、`std::sharedfuture`といった関連クラス群の基盤となる「状態」オブジェクトのメンタルモデルを解説します。この「状態」オブジェクトは、値の設定、例外の設定、値または例外の待機、値または例外の取得といった操作をサポートします。
全文翻訳
C++ futures part 1: The mental model
C++標準ライブラリには、std::promise、std::packaged_task、std::async、std::future、std::sharedfutureといった関連する関数とクラスのグループがあります。
これらのクラスはすべて、C++標準ライブラリが「状態」と呼ぶ単一の無名クラスで構築されているという考え方です。しかし、議論の目的のため、私はこれをsourceと呼びます。なぜなら、それは値を生成するものであり、C#のタスク完了ソースに類似した動作をするからです。
sourceは概念的に以下の操作をサポートします。
- 値を設定する。
- 例外を設定する。
- 値または例外が設定されるのを待つ。
- 値を取得するか、例外を再スローする。
ここでは、参照をサポートしない簡略化されたバージョンを示します。
template<typename T>
struct source
{
private:
enum what {
nothing, // 何も設定されていない
value, // 値が設定されている
exception, // 例外が設定されている
consumed, // 値が消費された
} d; // 判別子
union {
T value; // 値を格納
std::exception_ptr ex; // 例外ポインタを格納
} r; // 結果
std::mutex m; // ミューテックス
std::condition_variable cv; // 条件変数
判別子は、ユニオンに何が入っているかを示します:何も(誰も何も設定していないため)、値、例外、または何も(値が消費されたため)。
ミューテックスは他のメンバーへのスレッドセーフなアクセスを保証し、条件変数は値または例外が設定されるのを待つことを可能にします。
void must_be_empty() {
if (d != what::nothing) { // what::empty は nothing と同じ意味で使われていると仮定
throw std::future_error(future_errc::promise_already_satisfied);
}
}
setメソッドはソースが空の場合にのみ呼び出すことができるため、空のチェックを分離します。
public:
void set_value(T& v) {
std::lock_guard g(m);
must_be_empty();
new(&r.value) T(v); // "r.value = v" と同等だが、ユニオンのためplacement newを使用
d = what::value;
cv.notify_all();
}
値または例外は一度しか設定できません。したがって、既に値または例外が存在する場合はエラーとなります。そうでなければ、値を保存し、値を待っていたすべてのスレッドに通知します。
T&&を受け取るオーバーロードは省略します。それはstd::move()を使用する点を除けば同じです。
また、set_value_at_thread_exitというものもありますが、これは別の機会に説明します。
値の設定の代わりに、例外を設定することもできます。
void set_exception(std::exception_ptr ex) {
std::lock_guard g(m);
must_be_empty();
new(&r.ex) std::exception_ptr(ex); // "r.ex = ex" と同等だが、ユニオンのためplacement newを使用
d = what::exception;
cv.notify_all();
}
これは基本的にset_valueと同じですが、例外を格納します。
これが値と例外がsourceに入る方法です。次は、sourceが空でなくなるのを待つ部分です。
void wait() {
std::lock_guard g(m);
while (d == what::nothing) { // what::empty は nothing と同じ意味で使われていると仮定
cv.wait(g);
}
}
void wait_for(duration dur) {
std::lock_guard g(m);
cv.wait_for(g, dur, [&] { return d != what::nothing; }); // what::empty は nothing と同じ意味で使われていると仮定
}
無期限に待つか、特定の時間だけ待つことができます。wait_forメソッドは、条件変数を使用する理由そのものです。もしこれがなければ、std::atomicのwaitとnotify_allメソッドを使用できたかもしれません。
最後の部分は値を取得することです。
void can_get() {
if (d == what::consumed) {
throw std::future_error(future_errc::future_already_retrieved);
}
if (d == what::exception) {
std::rethrow_exception(r.ex);
}
}
値を取得する方法に関わらず、既に消費された値を取得することはできません。また、結果が例外の場合は、例外を再スローします。
T consume() {
wait();
std::lock_guard g(m);
can_get();
d = what::consumed;
return std::move(r.value); // ユニオンのメンバ名を r.v から r.value に修正
}
const T& get() {
wait();
std::lock_guard g(m);
can_get();
return r.value; // ユニオンのメンバ名を r.v から r.value に修正
}
};
値を取得(get)すると、基盤となるストレージへのconst参照が返されます。または、値を消費(consume)すると、基盤となるストレージから値がムーブされ、値が消費済みとしてマークされるため、誰もそれを消費または取得できなくなります(なぜなら、そこにはもう存在しないからです)。
これで、このオブジェクトから他のオブジェクトや関数を構築できます。それは次回行います。
ボーナスチャッター: デストラクタの記述は読者への演習課題とします。ユニオンのため、難しくはありませんが、煩雑です。
ダブルボーナスチャッター: このシリーズは、私が常にその仕組みを忘れるため書いていますが、書き出すことで記憶するのに役立ちます。
Author
[画像: Raymond Chen]
Raymondは30年以上にわたりWindowsの進化に関わってきました。2003年に「The Old New Thing」というウェブサイトを開始し、彼の最もワイルドな想像を超えて人気を博しました。このウェブサイトは、偶然にも同じタイトルの本「The Old New Thing」(Addison Wesley 2007)を生み出しました。彼は時折、Windows Dev DocsのTwitterアカウントに登場し、有用な情報を含まない話をします。