yohhoyの日記

技術的メモをしていきたい日記

C++26 Executionライブラリ:Sender完了シグネチャ集合

C++2c(C++26)実行制御ライブラリ(execution control library)(→id:yohhoy:20250609)において、Sender型の完了シグネチャ集合(set of completion signatures)の宣言方法。[P3557R3採択後WD N5014*1準拠]

自作SenderクラスMySenderの完了シグネチャ集合は、コンパイル時評価される静的メンバ関数テンプレートMySender::get_completion_signaturesの戻り値型execution::completion_signatures<Sigs...>によって表現する。実行制御ライブラリからはexecution::get_completion_signaturesを経由したexecution::completion_signatures_of_tエイリアステンプレートによりSender完了シグネチャ集合にアクセスされる。

// C++2c
#include <execution>
namespace exec = std::execution;

// 非依存Senderクラス(詳細後述)
struct MySender {
  using sender_concept = exec::sender_t;

  // MySenderの完了シグネチャ集合を宣言
  // 値完了     set_value_t(int)
  // エラー完了 set_error_t(exception_ptr)
  // 停止完了   set_stopped_t()
  template <class Self>
  static consteval auto get_completion_signatures()
  {
    return exec::completion_signatures<
      exec::set_value_t(int),
      exec::set_error_t(std::exception_ptr),
      exec::set_stopped_t()
    >{};
  }

  // ...
};

非依存Sender/依存Sender

Senderはその完了シグネチャ集合が確定するタイミングに応じて、下記2種類に区分される。

非依存Sender(non-dependent sender)
オブジェクト構築時点で完了シグネチャ集合が確定しているSender*2。例:justファクトリが生成するSender。非依存Senderを子Senderにもつ標準Senderアダプタが生成するSenderなど。
依存Sender(dependent sender)
Senderと後続Receiverとの接続時(connect)に、Receiver環境(environment)に依存して完了シグネチャ集合が型計算されるSender。execution::dependent_senderコンセプトを満たす。例:Receiver環境を読み取るread_envアルゴリズム(→id:yohhoy:20250612)が生成するSender。

Sender型Sndrの非依存Sender/依存Sender区分は、コンパイル時に静的メンバ関数テンプレートをSndr::get_completion_signatures<Sndr>()形式で呼び出したときにexecution::dependent_sender_error例外を送出*3する(依存Sender)か否か(非依存Sender)で判定される。

Senderクラスでは下記いずれかの静的メンバ関数テンプレートget_completion_signaturesを定義すればよい。いずれも空の引数リストをもち、戻り値型はクラステンプレートexecution::completion_signatures<Sigs...>とする*4。妥当な完了シグネチャ集合を定義できない場合は、任意の例外クラスを送出する*5ことでコンパイルエラーとできる。

// 非依存Senderとして定義
template <class Sndr>
static consteval auto get_completion_signatures();

// 依存Senderとして定義
template <class Sndr, class Env>
static consteval auto get_completion_signatures();

// 型情報に応じて非依存Sender/依存Senderを切替
template <class Sndr, class... Env>
static consteval auto get_completion_signatures();
// 依存Senderとして定義する場合は、Envが空の
// 呼び出し式 get_completion_signatures<Sndr>() に対して
// 例外 execution::dependent_sender_error を送出する

旧P2300R10仕様

提案文書P3557R3採択以前の実行制御ライブラリベース提案P2300R10時点では、Sender型Sndrの完了シグネチャ集合宣言には下記方式がとられていた。

  • 非依存Sender(相当)*6:メンバ型Sndr::completion_signaturesとして完了シグネチャ集合execution::completion_signatures<Sigs...>を定義する。
  • 依存Sender:戻り値型execution::completion_signatures<Sigs...>をもつメンバ関数テンプレートget_completion_signatures(Env)を定義する。SenderオブジェクトsndrとReceiver環境envに対して、decltype(sndr.get_completion_signatures(env))で完了シグネチャ集合をコンパイル時計算する。

P3557R1まではメンバ型completion_signatures定義方式も有効であったが、P3164R4を統合したP3557R2時点で冗長な仕様として削除された経緯がある。

Drop support for specifying a sender's completion signatures with a nested type alias.

P3557R3, Revision History, R2

Finally, since a sender can now use its get_completion_signatures() member function to provide its non-dependent senders, the nested completion_signatures type alias becomes redundant. This paper suggests dropping support for that.

P3164R4 Early Diagnostics for Sender Expressions

関連URL

*1:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5014.pdf

*2:型 Sndr が非依存Senderであることは、式 sender<Sndr> && !dependent_sender<Sndr> で判定可能。

*3:C++26からコンパイル時の例外送出+例外ハンドリング(try-catch)がサポートされる。P3068R6 参照。

*4:仮に適当な型を戻り値型としても execution::get_completion_signatures 仕様により未規定の例外送出動作へと変換される。これは同関数テンプレートの戻り値型制約を満たすための措置。

*5:依存Sender判定に用いられる execution::dependent_sender_error 以外を送出すること。

*6:P2300R10時点では “非依存Sender” 概念は定義されておらず、依存Senderと同様にReceiver接続時まで完了シグネチャ集合の計算および検査が遅延されていた。

クラステンプレート特殊化型判定 @ C++26

プログラミング言語C++において、与えられた型があるクラステンプレートの特殊化(specialization)か否かを判定する方法。id:yohhoy:20220122C++2c(C++26) Reflection(→id:yohhoy:20250305)バージョン。自明な実装で面白みはない...

// C++2c(C++26)
#include <atomic>
#include <array>
#include <complex>
#include <meta>

// rはプライマリテンプレートprimaryの特殊化?
consteval bool is_specialization_of(std::meta::info r, std::meta::info primary)
{
  return std::meta::has_template_arguments(r)
      && std::meta::template_of(r) == primary;
}

consteval bool is_complex(std::meta::info r)
  { return is_specialization_of(r, ^^std::complex); }

consteval bool is_stdarray(std::meta::info r)
  { return is_specialization_of(r, ^^std::array);}

consteval bool is_atomic(std::meta::info r)
  { return is_specialization_of(r, ^^std::atomic); }

static_assert( is_complex(^^std::complex<float>) );
static_assert( !is_complex(^^double) );

static_assert( is_stdarray(^^std::array<int, 5>) );
static_assert( !is_stdarray(^^int[5]) );

static_assert( is_atomic(^^std::atomic<int>) );
static_assert( !is_atomic(^^int) );

ノート:ADLを利用して単にhas_template_arguments(r), template_of(r)とも記述できる。言語組込み配列型(T[N])を判定したい場合は、標準ヘッダ<meta>にてリフレクション・メタ関数std::meta::is_array_type(r)が提供される。

関連URL

constexpr Two-Step

C++20からサポートされたコンパイル時動的確保データを、コンパイル時定数としてプログラム実行時へと効率的に持ち越すテクニック。

C++23現在の言語仕様では、コンパイルフェーズで動的確保(new)されたデータ領域はコンパイルフェーズにおいて解放(delete)されなければならない(“transient constexpr allocation”)。この制約によりstd::string*1std::vector型の定数初期化はコンパイルエラー(ill-formed)となる。データ領域の動的確保/解放を伴わないstd::array型ではプログラマの期待通りに定数初期化が行われる。

#include <array>
#include <string>
#include <vector>

constexpr std::string get_str()
{ return std::string(42, 'X'); }

constexpr std::vector<int> get_vec()
{ return {1, 2, 3}; }

constexpr std::array<int, 3> get_arr()
{ return {1, 2, 3}; }

static_assert(get_str() != "C++");  // OK
static_assert(get_vec()[0] == 1);   // OK
static_assert(get_arr()[0] == 1);   // OK

constexpr std::string str = get_str();  // NG: ill-formed
constexpr std::vector vec = get_vec();  // NG: ill-formed
constexpr std::array arr = get_arr();   // OK

前掲コードのうちstd::arrayの例が示す通り、コンパイル時に静的確保された領域ならば実行時へと値を持ち越せる。つまりコンパイル時に動的確保したデータを十分な要素サイズもつ静的領域へコピーしておき、後者を実行時に持ち越す実装方式が考えられる。同方式のデメリットとして必要な静的領域サイズを事前計算はできず、大きめの領域を確保する必要があるためにメモリ使用効率が犠牲になってしまう。

C++20/23: constexpr Two-Step技法

コンパイル時から実行時に持ち越す静的領域サイズを必要最小限とするため、0) コンパイル時に動的確保データを構築し、1) まず十分に大きい静的領域へとコピーしてから、2) 必要十分サイズの静的領域へとコピーする(“constexpr Two-Step”)。最後にconsteval関数テンプレートの非型テンプレートパラメータ*2に指定することで、静的領域を実行時にも参照可能とする。

#include <algorithm>
#include <array>
#include <string>
#include <string_view>

// コンパイル時にstd::stringを構築する関数
constexpr std::string my_data();

template <size_t N>
struct Storage {
  std::array<char, N> contents = {};
  size_t length = 0;

  template <class R>
  constexpr Storage(R&& r)
    : length(std::ranges::size(r))
  {
    std::ranges::copy(r, contents.data());
  }

  constexpr auto begin() const -> char const*
    { return contents.data(); }
  constexpr auto end() const -> char const*
    { return contents.data() + length; }
};

template <Storage V>
consteval auto promoted_value() {
  return std::string_view(V);  // C++23
  // C++20: std::string_view(V.begin(), V.end());
}

template <auto F>
constexpr std::string_view promote_to_static() {
  constexpr auto oversized_storage = Storage<255>(F());  // ★
  constexpr auto correctly_sized_storage =
    Storage<oversized_storage.length>(oversized_storage);
  return promoted_value<correctly_sized_storage>();
}

constexpr std::string_view s = promote_to_static<[]{ return my_data(); }>();

上記コード★箇所のバッファサイズ255は、コンパイル時に計算される文字列長よりも十分大きな値であればよい。バッファサイズ不足の場合はコンパイルエラーとして検出される。

C++2c Reflection

C++ Reflection(→id:yohhoy:20250305)の一環としてC++2c(C++26)へ採択*3された提案文書P3491R3では、コンパイル時の確保領域をプログラム実行時に参照可能な領域へと昇格させる関数群を追加する。これによりC++2c以降ではconstexpr Two-Step技法は不要となる。

// C++2c(C++26)
#include <meta>  // Reflection(P2996,P3491,...)

constexpr std::string my_data();

constexpr std::string_view s = std::define_static_string(my_data());  // OK

新しい標準ヘッダ<meta>に追加される関数*4

std::define_static_string
コンパイル時の文字型(CharT)Rangeから生成した文字列リテラルへのポインタ(const CharT*)を返すconsteval関数*5
std::define_static_array
コンパイル時の要素型(T)Rangeから生成した静的配列への参照(std::span<const T>)を返すconsteval関数
std::define_static_object
コンパイル時の構造的型(structural type)Tオブジェクトから生成した静的オブジェクトへのポインタ(const T*)を返すconsteval関数

提案文書P3491によれば、将来的にコンパイル時動的確保データを実行時へ直接持ち越し可能とする(“Non-transient constexpr allocation”)仕様拡張や、std::stringstd::vectorを非型テンプレートパラメータ(non-type template parameter)指定可能に制約緩和されるまでの過渡的ソリューションと位置付けている*6。下記提案がC++2d(C++29)向けて検討中ステータスにある。

関連URL

*1:例示コードのうち get_str 関数で構築する std::string 文字列長が十分短いときはSSO(→id:yohhoy:20120428)が行われ、副次効果としてコンパイル時に構築した文字列を実行時に持ち越せるケースがある。SSO判定閾値C++標準ライブラリの実装依存となるため、この動作仕様に依存するプログラムは可搬性が損なわれる。

*2:採択済み提案文書(PDF)P2841R7により、C++2c仕様では旧来の "non-type template parameter" は "constant template parameter" へと名称変更される。https://github.com/cplusplus/draft/pull/7587

*3:https://github.com/cplusplus/papers/issues/2158

*4:P3491は文字型ポインタが文字列リテラルを指す否かを判定する std::is_string_literal 関数も追加するが、本文趣旨と無関係なためここでは省略。

*5:std::define_static_string 関数が生成する文字列リテラルはNUL文字終端されることを示すため、戻り値型が std::string_view ではなく const CharT* と定義されている。

*6:P3491: §1 "So having facilities to solve these problems until the general language solution arises is very valuable.", §3.5 "Given non-transient allocation and a std::string and std::vector that are usable as non-type template parameters, this paper likely becomes unnecessary."

*7:https://github.com/cplusplus/papers/issues/867

*8:https://github.com/cplusplus/papers/issues/1336

*9:https://github.com/cplusplus/papers/issues/2037

C++26 Executionライブラリ:Cancellable Sender

C++2c(C++26)実行制御ライブラリ(execution control library)(→id:yohhoy:20250609)において、キャンセル可能なSenderの実装方法。

実行制御ライブラリにおける非同期操作のキャンセルは、Receiver側で発行された停止要求(stop request)をSender側が明示的に確認する協調的キャンセル(cooperative cancellation)によって実現する。Senderにおいて明示的に対応しない限りは、同Senderによる非同期操作は停止要求を無視して継続処理される。停止要求の発行/確認を行うクラス群は<stop_token>ヘッダで提供される。*1

関数オブジェクトfを引数にとり、下記動作を行うキャンセル可能なSenderMySender{f}の実装例:

  • 下流側で停止要求が行われていた場合、何もせず停止完了を送信する。
  • 関数呼び出し式f()が正常終了(return)したとき、戻り値を値送信する。
  • 関数呼び出し式f()が例外終了(throw)したとき、例外をエラー送信する。
// C++2c
#include <exception>
#include <stop_token>
#include <type_traits>
#include <execution>
namespace exec = std::execution;

// MySender接続結果Operation State
template<typename Rcvr, typename F>
struct OpState {
  using operation_state_concept = exec::operation_state_t;

  // exec::start(op)カスタマイズ
  void start() & noexcept {
    // 停止トークンをReceiver環境から取得
    auto st = std::get_stop_token(exec::get_env(rcvr_));
    if (st. stop_requested()) {
      // 停止要求が存在する場合は停止完了を送信
      exec::set_stopped(std::move(rcvr_));
      return;
    }
    try {
      // 関数呼び出しf()の戻り値で値完了
      exec::set_value(std::move(rcvr_), f_());
    } catch (...) {
      // 送出された例外でエラー完了
      exec::set_error(std::move(rcvr_), std::current_exception());
    }
  }

  Rcvr rcvr_;
  F f_;
};

template<typename F>
struct MySender {
  using sender_concept = exec::sender_t;
  using result_type = std::invoke_result_t<F>;

  // MySenderの完了シグネチャ集合を宣言
  using completion_signatures = exec::completion_signatures<
    exec::set_value_t(result_type),
    exec::set_error_t(std::exception_ptr),
    exec::set_stopped_t()
  >;

  template <class Self>  // P3557R3
  static consteval auto get_completion_signatures()
    { return completion_signatures{}; }

  // exec::connect(sndr, rcvr)カスタマイズ
  template<typename Rcvr>
  auto connect(Rcvr rcvr) {
    return OpState{std::move(rcvr), std::move(f_)};
  }

  F f_;
};

2025-08-20追記:P3557R3採択によってSender完了シグネチャ集合のカスタマイズ方式が当初提案P2300R10から変更され、メンバ型completion_signaturesではなく静的メンバ関数テンプレートget_completion_signaturesを定義する必要がある。→ id:yohhoy:20250823

C++標準ライブラリ提供のSenderアダプタ(→id:yohhoy:20250612)から生成されるSenderは、Receiver側での停止要求を無視するため協調的キャンセルを行わない。ただし、上流側Senderからの停止トークン取得問い合わせstd::get_stop_token(→id:yohhoy:20250609)を下流側Receiverへと転送する動作仕様となっており、タスクチェイン全体での非同期操作キャンセルを阻害しないよう設計されている。*2

例外として、全入力Sender完了を待機するexecution::when_allSenderアダプタでは、下流側Receiverでの停止要求検知もしくは入力Senderからのエラー完了受信をトリガとして停止要求を発行し、他の入力Senderに対して協調的キャンセルを促す。*3

// タスクチェインを構築
exec::sender auto snd1 = MySender{[] -> int { return 1; }};
exec::sender auto snd2 = MySender{[] -> int { throw 42; }};
exec::sender auto snd3 = MySender{[] -> int { return 3; }};
exec::sender auto sndr = exec::when_all(snd1, snd2, snd3);

try {
  auto tup = std::this_thread::sync_wait(sndr).value();
  // 1) snd1は戻り値1を値送信
  // 2) snd2は例外42をエラー送信
  // 3) when_all内部で停止要求を発行
  // 4) snd3で停止要求を検知し停止完了を送信
  // 5) sndr自体は例外42でエラー完了
  std::println("val={}", tup);
} catch (int e) {
  std::println("catch {}", e);
}
// "catch 42"

2025-07-22追記:P3284R4にてSenderアダプタexecution::unstoppableが追加される。上流側Senderからのstd::get_stop_token問い合わせに対してstd::never_stop_tokenを返すようReceiver環境を書き換えることで、タスクチェイン下流側からの非同期操作キャンセル伝搬を遮断する。上流側からの停止完了送信には影響しない。

自作Senderアダプタにおいて独自基準で非同期操作キャンセルを追加サポートする場合、下流側Receiver環境から取得した停止トークン(stop token)を確認する以外に下記対応が必要となる。

  • Operation Stateがstd::inplace_stop_source型の停止ソース(stop source)を内包する。*4
  • 下流側Receiver環境から取得した停止トークンに対して、自身の停止ソースへ停止要求を発行(request_stop)する停止コールバック(stop callback)を登録する。
  • 上流側Senderと接続されるReceiverの環境は、std::get_stop_token問い合わせに対して停止ソースから取得した停止トークンを返す。

複雑なキャンセルサポートの実装例として、提案文書P2300R10, §4.1 Asynchronous Windows socket recvなども参照のこと。

関連URL

*1:<stop_token>ヘッダはC++20標準ライブラリで協調的な中断処理をサポートする std::jthread クラスと同時に導入された。実行制御ライブラリの提案文書 P2300R10 では、同ヘッダに std::inplace_stop_{token,source} クラスが追加される。

*2:Senderアダプタ間で対象クエリオブジェクトによる問い合わせを転送するか否かは、別のクエリオブジェクト std::forwarding_query への問い合わせ結果(bool値)によって制御される。

*3:厳密には、下流側Receiverでの停止要求発行もしくは上流側Senderからのエラー完了/停止完了いずれかの初回受信タイミングで停止要求を発行する。いずれかの入力Senderがエラー完了した場合、初回受信したエラー完了値が when_all のエラー完了値となる。入力Senderが値完了/停止完了のいずれかであれば、when_all は停止完了を送信する。

*4:C++20から存在する std::stop_source でも問題ないが、動的メモリ確保が発生しない std::inplace_stop_source の方が好ましい。C++標準ライブラリ execution::when_all Senderアダプタの仕様定義でも std::inplace_stop_source が利用されている。

C++26 Executionライブラリ(コルーチン相互運用編)

C++2c(C++26)標準ライブラリに追加される実行制御ライブラリ(execution control library)についてメモ。別名:std::execution, Senders/Receivers(S/R)

2025年5月現在、ベースライン提案文書P2300R10までを採択済み。
2025-07-20追記:2025年6月会合にて関連する一連の提案文書P2079R10,
P3149R11, P3284R4, (PDF)P3433R1, P3481R5, P3552R3, P3557R3, P3570R2, (PDF)P3682R0が採択された。

コルーチンAwaitable==Sender

C++コルーチンにおいてco_await演算子オペランドに指定可能なAwaitableオブジェクトは、実行制御ライブラリのSenderとしてReceiverと接続(connect)できる。2025年5月現在はそのような標準Awaitable型は提案文書P3552にて検討段階にあり、他プログラミング言語における非同期処理の結果型(例:Task, Promise, Futureなど)に相当する。
2025-07-20追記:P3552R3にてSender互換コルーチン戻り値型として利用する非同期タスクexecution::task<T>クラスが追加された。同クラスはコルーチンAwaitableとして定義されるものの、直接的にSenderへアダプト(メンバ型sender_concept定義)するため下記機構は利用されない。

Awaitableオブジェクトの取り扱いは接続操作execution::connectCPOにて仕様規定されるため、Awaitable型を定義するコルーチンライブラリ実装側での対処は必要ない。(コルーチン側でSenderを利用したい場合は、コルーチンライブラリ実装側にも後述対応が必要。)

co_await Awaitableの戻り値型Tのとき、コルーチンAwaitableと接続するReceiverは下記の完了シグネチャを持つ必要がある。

  • 値完了:set_value_t(T)
  • エラー完了:set_error_t(std::exception_ptr)
  • 停止完了:set_stopped_t()
// C++2c
#include <coroutines>
#include <execution>
namespace exec = std::execution;

// Awaitableコルーチン戻り値型
template<typename T>
struct Lazy {
  struct promise_type;
  //...
  // co_await演算子オーバーロードを定義
  auto operator co_await() noexcept;
  // 式co_await Lazy<T>はT型の値を返す
}

// 自作Receiver型
struct DumpReceiver {
  using receiver_concept = exec::receiver_t;

  // 値完了     set_value_t(int)
  void set_value(int) noexcept;
  // エラー完了 set_error_t(exception_ptr)
  void set_error(std::exception_ptr) noexcept;
  // 停止完了   set_stopped_t()
  void set_stopped() noexcept;
};

// Awaitableオブジェクトを返すコルーチン(関数)
Lasy<int> async_work();

// Awaitableオブジェクトはsenderコンセプトを満たす
exec::sender auto task = async_work();  // OK

// Awaitable(Sender)とRecevierを接続
exec::receiver auto rcvr = DumpReceiver{};
auto op = exec::connect(task, rcvr);  // OK
exec::start(op);

Sender Awaitableコルーチン

C++コルーチンのPromise型が特定インタフェース要件を満たすとき、コルーチン内部においてSenderオブジェクトに対して式co_await sndrでAwait可能となる。実行制御ライブラリと相互運用可能なコルーチンライブラリ実装者向けに、Promise型Pの実装ヘルパとして基底クラスexecution::with_awaitable_senders<P>が提供される。
2025-07-20追記:execution::task<T>を戻り値型とするコールチンはSender Awaitableコルーチンとなる。[P3552R3]

Sender Awaitableコルーチン内部でSenderオブジェクトをco_await演算子に指定するとexecution::as_awaitableCPOが呼び出され、Senderとコルーチン接続用のAwaitableオブジェクトに自動変換される。同Awaitableオブジェクトはコルーチンを中断(suspend)してSenderを開始(start)し、非同期操作が完了すると送信値をco_await式の値としてコルーチンを再開(resume)する。非同期操作がエラー完了したときは例外送出が行われ、停止完了の既定動作としてプログラム異常終了(std::terminate())する。

co_await演算子オペランドに指定可能なSenderの制約として、値完了シグネチャを1種類だけ持つことが要請される。複数の値完了シグネチャを持つSenderの場合は、Senderアダプタexecution::into_variant等を適用する必要がある。

  • 2025-07-20追記:Senderがクエリオブジェクトexecution::get_await_completion_adaptorに対応していれば、co_await演算子の適用時に単一の値完了シグネチャへと自動変換される。この仕組みにより、Receiver接続時およびコルーチン利用時それぞれで最適なインタフェースを提供できる。[P3570R2]
// C++2c
#include <coroutines>
#include <execution>
namespace exec = std::execution;

// Sender Awaitableコルーチン戻り値型
template<typename T>
struct Lazy {
  struct promise_type : exec::with_awaitable_senders<promise_type> {
    void return_value(T);
    //...
  };
  // コルーチンco_return戻り値を取得するメンバ関数
  T get();
};

// Sender Awaitableコルーチン
Lazy<int> coro(int n)
{
  // n*3を計算するSenderチェイン構築
  exec::sender auto sndr =
    exec::just(n)
    | exec::then([](int m){ return m * 3; });

  // Senderを開始して値取得を待機
  int val = co_await sndr;  // OK

  co_return val * 7;
}

auto task = coro(2);
int value = task.get();  // 42==2*3*7

Sender Awaitableコルーチン動作のカスタマイゼーションポイントとして下記が定義される。

  • co_await sndr
    • execution::as_awaitableCPO経由のas_awaitableメンバ関数
    • co_awaitオペランド指定専用のSender型変換として、Sender属性へのexecution::get_await_completion_adaptor問い合わせ[P3570R2]
  • 停止完了ハンドラ:コルーチンPromsie型unhandled_stoppedメンバ関数
  • 停止完了の委譲:execution::with_awaitable_senders<P>::set_continuationメンバ関数によるコルーチンハンドラ設定

メモ:難解なC++20コルーチン仕様 × 複雑なC++2c実行制御ライブラリの相乗効果で気分は宇宙猫 (゚ω゚)


関連URL

C++26 Executionライブラリ(Senderアルゴリズム編)

C++2c(C++26)標準ライブラリに追加される実行制御ライブラリ(execution control library)についてメモ。別名:std::execution, Senders/Receivers(S/R)

2025年5月現在、ベースライン提案文書P2300R10までを採択済み。
2025-07-20追記:2025年6月会合にて関連する一連の提案文書P2079R10,
P3149R11, P3284R4, (PDF)P3433R1, P3481R5, P3552R3, P3557R3, P3570R2, (PDF)P3682R0が採択された。

Senderアルゴリズム

Senderアルゴリズム(sender algorithm)はSenderを引数/戻り値とする関数であり、 下記3つのカテゴリに区分される。C++2c標準ライブラリでは、Senderアルゴリズムは全てCPO(Customization Point Object)として定義される。

区分 引数 戻り値
Senderファクトリ
(sender factory)
任意個の引数
(非Sender)
Sender execution::
just
Senderアダプタ
(sender adaptor)
1個以上のSender
+任意個の引数
Sender execution::
then
Senderコンシューマ
(sender consumer)
1個以上のSender
+任意個の引数
任意戻り値
(非Sender)
this_thread::
sync_wait
パイプライン記法

一部のSenderアダプタは|演算子によるパイプライン記法をサポートする。(標準RangesライブラリのRangeアダプタと同様)

#include <execution>
namespace exec = std::execution;

auto calc(int, int) -> int { /*...*/ }

// 関数呼び出し
exec::sender auto snd0 = exec::just(2, 3);
exec::sender auto snd1 = exec::then(snd0, calc);
// パイプライン記法
exec::sender auto sndr = exec::just(2, 3) | exec::then(calc);

パイプライン記法をサポートするSenderアダプタ(例:then)はパイプ可能Senderアダプタオブジェクト(pipeable sender adaptor object)と定義され、第1引数(Sender)以外を束縛したオブジェクト(例:then(calc))はパイプ可能Senderアダプタクロージャオブジェクト(pipeable sender adaptor closure object)となる。後者は|演算子の右オペランドや、Senderアダプタexecution::onの引数*1としてSenderチェイン構築時に利用される。

非同期処理フレームワークを拡張するライブラリ実装者向けに、パイプ可能Senderアダプタクロージャオブジェクト型Dの実装ヘルパとして基底クラスexecution::sender_adaptor_closure<D>が提供され、独自Senderアダプタのパイプライン記法対応がサポートされる。*2

Senderチェイン

SenderアダプタやSenderコンシューマに指定した入力Senderは生成Senderオブジェクトのメンバ変数として保持されるため、ライブラリ仕様では子(child)Senderと呼ばれる。同様に、Senderアルゴリズムが生成するSenderオブジェクトは親(parent)Senderと呼ばれる。

  • 子Sender = Senderチェインの上流側
  • 親Sender = Senderチェインの下流
snd1() | snd2() | snd3()  // パイプライン記法
snd3(snd2(snd1()))        // 関数呼び出し
// 生成Senderオブジェクトは下記の入れ子構造
snd3{ snd2 { snd1{} } }

Senderアルゴリズムが生成する親Senderは子Senderの接続先となるため、Senderオブジェクト内部ではReceiverインタフェースを定義する。Sender-Receiver間の接続(connect)はSenderコンシューマ適用時にSenderチェイン上で親→子の順序で実行され、Sender同様に入れ子構造をとるOperation Stateを生成する。親Operation Stateに対して開始(start)操作を行うと子Operation State=Senderチェイン上流側の処理が開始(start)され、以降は子Operation Stateが親SenderのReceiver完了操作を呼び出すことで上流→下流の順に非同期操作全体が実行される。

std::this_thread::sync_wait( exec::just(2, 3) | exec::then(calc) );
// Sender構造: sync_wait{ then{ just{ {}, {2, 3} }, calc } }
  • 1) sync_waitコンシューマ内部で、子Sender-親SenderのReceiver間を接続(connect)する。
  • 2) sync_waitコンシューマ内部で、Operation Stateを開始(start)する。
  • 3) justのOperation Stateが、送信値2, 3thenのReceiverハンドラを呼び出す。
  • 4) thenのReceiverハンドラ受信値2, 3calcに適用し、結果値でsync_waitのReceiverハンドラを呼び出す。
  • 5) sync_waitのReceiver受信値を戻り値として返す。

Senderファクトリ

名前空間std::executionにて下記Senderファクトリが提供される。

ファクトリ 呼び出し形式 概要
just (values...) 値完了を送信
just_error (err) エラー完了を送信
just_stopped () 停止完了を送信
schedule (sch) Scheduler上で非同期処理を開始
read_env (q) 接続先Receiver環境への問合せ

ノート:

  • schedule:指定Schedulerschを値完了Scheduler(value completion scheduler)として、空の値を送信する。
  • read_env:指定クエリオブジェクトqを用いて、非同期操作の開始(start)時に後続Receiver環境へ問い合わせて得られた結果を値送信する。Senderオブジェクト構築時やReceiver接続(connect)時には何もしない。

Senderアダプタ

名前空間std::executionにて下記Senderアダプタが提供される。※列はパイプライン記法の対応可否を表す。

アダプタ 呼び出し形式 概要
write_env (sndr, env) Receiver環境を上書き[P3284R4]
unstoppable (sndr) 下流側の停止要求を無視[P3284R4]
start_on (sch, sndr) Scheduler上でSender開始
continues_on (sndr, sch) Scheduler上で後続継続
on (sch, sndr) 指定Senderのみ別Scheduler上で実行
on (sndr, sch, closure) 指定非同期操作のみ別Scheduler上で実行
schedule_from (sch, sndr) continues_on実装補助*3
then (sndr, f) 値完了の後続処理アタッチ
upon_error (sndr, f) エラー完了の後続処理アタッチ
upon_stopped (sndr, f) 正常完了の後続処理アタッチ
let_value (sndr, fn) 値完了をネスト非同期操作へ変換
let_error (sndr, fn) エラー完了をネスト非同期操作へ変換
let_stopped (sndr, fn) 停止完了をネスト非同期操作へ変換
bulk (sndr, policy, n, f) 指定関数を複数回実行*4
bulk
_chunked
(sndr, policy, n, f) 指定関数をグループ単位で複数回実行[P3481R5]
bulk
_unchunked
(sndr, policy, n, f) 指定関数を個別に複数回実行[P3481R5]
split (sndr) P3682R0で削除*5
when_all (sndrs...) 全Sender完了待機+値結合
when_all
_with_variant
(sndrs...) 全Sender完了待機+値結合
複数値完了シグネチャ対応版
into_variant (sndr) 複数値完了シグネチャvariant型に統合
stopped_as
_optional
(sndr) 停止完了をoptional値完了に変換
stopped_as
_error
(sndr, err) 停止完了をエラー完了に変換
associate (sndr, token) 非同期スコープに関連付け[P3149R11]
spawn_future (sndr, token, env?) 非同期スコープでSender早期開始[P3149R11]*6
affine_on (sndr, sch) コルーチンでのSchedulerアフィニティ指定[P3552R3]

ノート:

  • then/upon_{error,stopped}:受信値args...に対して、f(args...)戻り値を値完了で送信する。
  • let_{value,error,stopped}:受信値args...に対して、fn(args...)戻り値Senderに後続Receiverを接続する。
  • bulk:受信値values...に対して、範囲[0, n)の各idxに対してf(idx, values...)が逐次的に呼び出される。
    • 2025-07-20追記:bulk動作はbulk_chunkedに移譲される。既定動作はP2300R10定義と同様に逐次実行。[P3481R5]
  • bulk_chunked:既定動作はbulkと同様に逐次実行。並列Scheduler上かつ実行ポリシーpolicyで並列処理を許可(execution::par)した場合、範囲[0, n)を分割した重複しない部分区間[begin, end)に対して複数スレッド上でf(begin, end, values...)が並列実行される。[P2079R10, P3481R5]
  • bulk_unchunked:既定動作はbulkと同様に逐次実行。並列Scheduler上かつ実行ポリシーpolicyで並列処理を許可(execution::par)した場合、範囲[0, n)の各idxに対して複数スレッド上でf(idx, values...)が並列実行される。[P2079R10, P3481R5]
  • when_all:入力Sender#nからの受信値vs_n...に対して、tuple<Vs_0..., ..., Vs_k...>型に統合して送信する。いずれかの入力Senderからエラー完了/停止完了を受信した場合、他の入力Senderに対して停止要求を行う。*7
  • when_all_with_variantwhen_all(into_variant(sndrs)...)相当。
  • into_variant:入力Senderから受信可能性のある値vs_m...に対して、variant<tuple<Vs_0...>, ..., tuple<Vs_j...>>型に統合して送信する。
  • spawn_future:任意の値完了シグネチャset_value_t(Vs...)をもつ入力Senderを非同期スコープトークtokenに関連付けて、非同期操作を即座に開始(start)する。[P3149R11] → id:yohhoy:20251002

Senderコンシューマ

名前空間std::this_threadにて下記Senderコンシューマが提供される。

コンシューマ 呼び出し形式 概要
sync_wait (sndr) Sender完了待機し結果取得
sync_wait
_with_variant
(sndr) Sender完了待機し結果取得
複数値完了シグネチャ対応版

名前空間std::executionにて下記Senderコンシューマが提供される。

コンシューマ 呼び出し形式 概要
spawn (sndr, token, env?) 非同期スコープでSender早期開始[P3149R11]

ノート:

  • sync_wait:値完了のときoptional<tuple<Vs...>>値を、停止完了のとき無効値(nullopt)を返す。エラー完了のときは例外送出する。
  • sync_wait_with_variant:ほぼsync_wait(into_variant(sndr))相当。*8
  • spawn:空の値完了シグネチャset_value_t()をもつ入力Senderを非同期スコープトークtokenに紐づけて、非同期操作を即座に開始(start)する。[P3149R11] → id:yohhoy:20251002
#include <execution>
namespace exec = std::execution;

// 値(123, 'X')を送信
exec::sender auto sndr = exec::just(123, 'X');
try {
  auto result = std::this_thread::sync_wait(sndr);
  if (result) {
    // 値完了
    auto [n, c] = *result;
    // n := int型/123, c := char型/'X'
  } else {
    // 停止完了
  }
} catch (...) {
  // エラー完了
}

実行ドメインとカスタマイゼーションポイント

サードパーティ・ライブラリによる柔軟な機能拡張を可能とするため、実行ドメイン(execution domain)を介したSenderアルゴリズムのカスタマイゼーションポイントを定義する。独自実行ドメインに関連付けた独自Senderと標準Senderアルゴリズムを組み合わせるとき、標準Senderアルゴリズムの動作セマンティクスを実現する独自のSender型へと変換することができる。標準ライブラリではデフォルト実行ドメイン(execution::default_domain)のみ定義し、標準Senderアルゴリズムは暗黙に同ドメインと関連付けられる。

カスタマイゼーションポイント実装側でSenderアルゴリズムを識別するため、大半の標準Senderアルゴリズムは同名タグ型(例:justアルゴリズムはSenderタグ型just_t)を持つ*9。また標準Senderアルゴリズムで作成されるSenderオブジェクトは構造化束縛(structured binding)に対応しており、カスタマイゼーションポイント実装側でSenderタグ型、オブジェクト構築時引数、子Senderを容易に取り出せる仕様となっている。

標準Senderアルゴリズムは下記CPO呼び出しを介したカスタマイゼーションポイントを規定する。一部の標準Senderアルゴリズム仕様では、これらのカスタマイゼーションポイントとデフォルト実行ドメイン動作に依存した仕様記述を行う。(動作を追うのが、とてもつらい。)

  • execution::transform_sender:Senderアルゴリズム呼び出しによるSenderオブジェクト構築時、および後続Receiverへの接続(connect)時のSender型変換*10。既定動作は無変換、一部SenderアルゴリズムではSenderタグ型へ処理ディスパッチ。
  • execution::transform_env:後続Receiver接続(connect)時の環境(environment)変換。既定動作は無変換。
  • execution::apply_sender:Senderコンシューマ適用時の動作。既定動作はSenderタグ型へ処理ディスパッチ。

例:starts_onアルゴリズムは後続Receiver接続(connect)時にschedulelet_valueの組合せへと変換される。以下は簡略化したstarts_on.transform_senderカスタマイゼーションポイント実装。

// 呼び出し式 execution::starts_on(sch, sndr)

// out_sndr := starts_onアルゴリズムで生成されるSenderオブジェクト
// OutSndr  := 上記Senderの型
auto&& [_, sch, sndr] = out_sndr;
// sch  := 非同期処理を実行するScheduler
// sndr := 実行させる非同期処理(Sender)
return let_value(
  schedule(sch),
  [sndr = std::forward_like<OutSndr>(sndr)]() mutable {
    return std::move(sndr);
  });


関連URL

*1:Senderアダプタ execution::on は2種類の呼び出し形式をサポートしており、パイプ可能Senderアダプタクロージャオブジェクトは on(sndr, sch, closure) 形式に対して指定する。

*2:C++23標準Rangesライブラリでも類似の ranges::range_adaptor_closure<D> が提供され、独自Rangeアダプタ実装者向けのパイプライン記法サポートが存在する。

*3:P2300R10: "schedule_from is not meant to be used in user code; it is used in the implementation of continues_on."

*4:2025-07-20追記:P3481R5にてbulkアルゴリズムCPO引数リストが (sndr, n, f) から (sndr, policy, n, f) へと変更された。P2079R10で導入される並列Schedulerと組み合わせてスレッド並列を実現する。

*5:P3682R0: "Deficiencies of std::execution::split, Dynamic Allocation, Shared Ownership, Eagerness, Naming"

*6:P3149R11, §6.4: "To keep consistency with spawn() this paper doesn’t support pipe operator for spawn_future()."

*7:各Senderが停止要求を認識し、早期に停止完了を送信するか否かはSenderの実装に依存する。

*8:sync_wait_with_variantアルゴリズムの戻り値型は optional<variant<tuple<...>...>> となる。単純に sync_wait( into_variant(sndr) ) とすると、冗長なtuple層が追加された optional<tuple<variant<tuple<...>...>>> 型となってしまう。

*9:read_envアルゴリズムのみ例外的にSenderタグ型を規定しない(unspecified)。2025-07-20追記:write_env, unstoppableも同様にSenderタグ型を規定しない[P3284R4]。

*10:同一関数 transform_sender の呼び出し引数リストの差異により、2つのタイミングのカスタマイゼーションポイントを定義する。Senderオブジェクト構築時はSenderのみが、Receiver接続時はSenderと環境が実引数に渡される。

C++26 Executionライブラリ(基礎編)

C++2c(C++26)標準ライブラリに追加される実行制御ライブラリ(execution control library)についてメモ。別名:std::execution, Senders/Receivers(S/R)

2025年5月現在、ベースライン提案文書P2300R10までを採択済み。
2025-07-20追記:2025年6月会合にて関連する一連の提案文書P2079R10,
P3149R11, P3284R4, (PDF)P3433R1, P3481R5, P3552R3, P3557R3, P3570R2, (PDF)P3682R0が採択された。

// C++2c
#include <execution>
#include <print>
namespace exec = std::execution;

// タスクチェインを構成
exec::sender auto work =
  exec::just(2, 3)
  | exec::then([](int a, int b) {
      return a * b * 7;
    });
// 呼び出しスレッド上で同期的にタスク実行
auto result = std::this_thread::sync_wait(work);
// 結果値を取り出す
auto [value] = result.value();
// value == int型の値42

CPU並列処理を実現するスレッドプール機構や、コルーチン非同期タスク型といった応用機能は検討進行中GPU/CUDA並列処理のようなベンダ拡張機能では、非同期処理フレームワークに則ったサードパーティ実装が提供される。*2

  • 2025-07-20追記:システムスレッドプールのSchedulerを返すexecution::get_parallel_scheduler関数[P2079R10]と連携してCPU並列処理を行うSenderアダプタexecution::bulkファミリ[P3481R5]、Sender互換コルーチン戻り値型として利用する非同期タスクexecution::task<T>クラス[P3552R3]が採択済み。

実行制御ライブラリ

C++2c実行制御ライブラリでは、非同期(asynchronous)・並行(concurrent)/並列(parallel)処理を統合的に扱うフレームワークを規定する。ライブラリ仕様は非同期処理を実現する枠組みとして記述され、並行/並列処理は具体的なScheduler実装によって実現される。(例:CPUスレッドプール、GPGPU/CUDA処理をサポートするScheduler)

  • <execution>ヘッダ(C++17追加ヘッダを拡張)
  • 基本コンセプト
    • execution::sender
    • execution::receiver
    • execution::operation_state
    • execution::scheduler
  • 基本操作(CPOとして定義)
    • execution::connect:(sender, receiver) -> operation_state
    • execution::start:(operation_state) -> void
    • execution::schedule:(scheduler) -> sender
    • execution::set_value:(receiver, values...) -> void
    • execution::set_error:(receiver, err) -> void
    • execution::set_stopped:(receiver) -> void
  • プロパティ問い合わせ(queries)
    • 標準クエリオブジェクト(CPOとして定義)
    • クエリ可能オブジェクト == Sender属性, Receiver環境, Scheduler
    • クエリ可能オブジェクトの実装ヘルパ型execution::env, execution::prop
  • Senderアルゴリズム(CPOとして定義)→ id:yohhoy:20250612
    • Senderファクトリ:(args...) -> sender
    • Senderアダプタ:(sender, args...) -> sender
    • Senderコンシューマ:(sender) -> result
    • 実行ドメイン == Senderアルゴリズムのカスタマイズ機構
    • Senderアルゴリズムの実装ヘルパ型execution::sender_adaptor_closure
  • C++コルーチンサポート → id:yohhoy:20250613
    • 任意のコルーチンAwaitable型をSenderとして扱える(execution::connectCPO内部動作)
    • Senderをコルーチン内で利用可能にする実装ヘルパ型execution::with_awaitable_senders

基本コンセプト: Sender, Receiver, Operation State

  • 非同期操作(asynchronous operation)
    • 明示的に作成され、1回だけ明示的に開始(start)でき、最終的に3種類いずれかの完了(completion)に到達する。
    • 値完了(value completion):非同期操作の “成功”。0個以上の任意型の値。
    • エラー完了(error completion):非同期操作の “失敗”。1個の任意型のエラー値。
    • 停止完了(stopped completion):非同期操作の “キャンセル”。付帯情報なし。
  • Operation State:execution::operation_state concept
    • 非同期操作に関連付けられる状態オブジェクト。
    • Operation Stateはコピー/ムーブともに不可。*3
    • メンバ型operation_state_conceptでタグ型operation_state_tを宣言。
    • 開始操作execution::startCPOから呼ばれるstartメンバ関数を定義。
  • Sender:execution::sender concept
    • 1つ以上の非同期操作のファクトリ。
    • メンバ型sender_conceptでタグ型sender_tを宣言。
      • 2025-07-20追記:LWG4202にてenable_sender変数テンプレート特殊化を用いて非侵襲的に任意の型をSenderにアダプト可能となる。
    • SenderとReceiverとを接続(connect)してOperation Stateを生成。
    • 接続操作execution::connectCPOから呼ばれるconnectメンバ関数を定義。
  • Receiver:execution::sender concept
    • 非同期操作の値完了/エラー完了/停止完了を受け取るハンドラの集合体。
    • メンバ型receiver_conceptでタグ型receiver_tを宣言。
    • 完了操作execution::set_{value,error,stopped}CPOから呼ばれる完了ハンドラset_{value,error,stopped}メンバ関数を定義。
  • 完了シグネチャ(completion signature) == 完了ハンドラの型情報*4
    • 値完了:execution::set_value_t(Values...)
    • エラー完了:execution::set_error_t(Err)
    • 停止完了:execution::set_stopped_t()
    • 完了シグネチャ集合をexecution::completion_signatures<Sigs...>で表現。
    • Sender/Receiverは少なくとも1個の完了シグネチャに対応する。Sender完了シグネチャ集合 ⊆ Receiver完了シグネチャ集合ならば、両者は接続(connect)可能。*5
    • 2025-08-23追記:P3557R3採択後のSender完了シグネチャ集合の宣言方法は id:yohhoy:20250823 を参照。
exec::sender auto sndr = /* Senderオブジェクト */
exec::receiver auto rcvr = /* Receiverオブジェクト */
// SenderとReceiverを接続(connect)
exec::operation_state auto op = exec::connect(sndr, rcvr);
// 非同期操作の開始(start)
exec::start(op);
// 非同期操作完了時にexec::set_{value,error,stopped}を呼び出し、
// rcvrで定義する完了ハンドラのいずれか1つが呼び出される。
// (Receiverが受け取った結果値の取り出しは別機構で実現する。)

基本コンセプト: Scheduler

  • 実行エージェント(execution agent):タスクを並列実行する機構(例:CPUスレッドstd::threadGPU/CUDAスレッド)。
  • 実行リソース(execution resource):実行エージェントの集合を管理するエンティティ(例:CPUスレッドプール、CUDAスレッド管理)
  • Scheduler:execution::scheduler concept
    • 実行リソース上でタスク実行をスケジュールするためのインタフェースかつ軽量ハンドル。*6
    • メンバ型sender_conceptでタグ型sender_tを宣言。
    • スケジュール操作execution::scheduleCPOから呼ばれるscheduleメンバ関数を定義。
  • 完了Scheduler(completion scheduler)
    • 非同期操作の値完了/エラー完了/停止完了を実行するScheduler。完了種別毎に異なる可能性あり。
  • スケジュールSender(schedule sender)
    • execution::scheduleCPO呼び出しで得られるSender。Scheduler上で空の値を用いて値完了を実行する。
  • execution::run_loop
    • シングルCPUスレッドでのタスクFIFO処理を行う実行リソース。
exec::scheduler auto sch = /* Schedulerオブジェクト */
// スケジュールSenderを作成
exec::sender auto sndr = exec::schedule(sch);

exec::receiver auto rcvr = /* Receiverオブジェクト */
exec::operation_state auto op = exec::connect(sndr, rcvr);
exec::start(op);
// sch上でrcvrに対して値完了rcvr.set_value()が呼ばれる

クエリ可能オブジェクト/クエリオブジェクト

  • クエリ可能オブジェクト(queryable object)
    • 読み取り専用のKey−Valueデータ集合。Key==クエリオブジェクト、Valueは任意型。
    • Senderに関連付けられた属性(attributes)、Receiverに関連付けれた環境(environment)(execution::get_envCPO)
    • Schedulerオブジェクト自身
  • クエリオブジェクト(query object)
    • 問い合わせ(query)操作を行うCPO 兼 クエリ可能オブジェクトの Key。
    • Sender/Receiver/Schedulerに関連付けられたプロパティ(property)値を取得。
  • 標準クエリオブジェクト
    • forwarding_query:Senderアルゴリズム間のクエリ転送可否を問い合わせ。
    • get_allocator:メモリアロケータを問い合わせ。
    • get_stop_token:非同期キャンセル(→id:yohhoy:20250625)のための停止トークンを問い合わせ。
    • execution::get_domain:Sender動作カスタマイズのための実行ドメインを問い合わせ。
    • execution::get_(delegation_)scheduler:Receiverへ関連付けられたSchedulerを問い合わせ。
    • execution::get_forward_progress_guarantee:Schedulerへ前方進行保証を問い合わせ。
    • execution::get_completion_scheduler<completion-tag>:Senderへ完了Schedulerを問い合わせ。
    • execution::get_await_completion_adaptor:Senderへco_awaitオペランド指定時の型変換を問い合わせ。[P3570R2]
  • execution::env:複数のクエリ可能オブジェクトを合成。[P3325R5]
  • execution::prop:単一Key−Valueに対応した最小のクエリ可能オブジェクト。[P3325R5]
exec::receiver auto rcvr = /* Receiverオブジェクト */
// Receiver環境を取得
auto env = exec::get_env(rcvr);
// メモリアロケータを問い合わせ
auto alloc = std::get_allocator(env);


関連URL

*1:サードパーティ・ライブラリの拡張自由度を高めるためにCPOが多用されており、多数のカスタマイゼーションポイントを提供する。代償としてライブラリ仕様記述が複雑化し、難解な仕様となっているのは否めない。

*2:https://github.com/NVIDIA/stdexec

*3:P2300R10, §5.2: "An operation state is neither movable nor copyable"

*4:Receiverメンバ関数として定義する完了ハンドラの関数シグネチャとは異なり、戻り値型を “完了操作CPOの型” に置換した関数型として表現される。例:値完了シグネチャ execution::set_value_t(int) に対応するRcvrクラスメンバ関数は void Rcvr::set_value(int) となる。

*5:ある種類の完了シグネチャには対応しないケースや、同種の完了シグネチャで異なる引数リストに複数対応するケースもある。停止完了シグネチャは引数0個のため、実質 execution::set_stopped_t() の1種類のみ。

*6:P2300R10, §4.2: "A scheduler is a lightweight handle that represents a strategy for scheduling work onto an execution resource."