ちょっと回り道:Executors

数週間前、C++ の先物への提案の著者の 1 人である Felix Petriconi が私に電子メールを書きました。彼は、std::future Extensions に関する私の記事はかなり古いと言いました。正直なところ、彼は正しいです。フューチャーの未来は、主にエグゼキュータのために変更されました。

フューチャーの未来について書く前に、エグゼキューターの概念を紹介しなければなりません。 Executor には、C++ でかなりの歴史があります。議論は少なくとも8年前に始まりました。詳細については、Detlef Vollmanns がプレゼンテーション「Finally Executors for C++」で優れた概要を示しています。

この投稿は、主にエグゼキュータ P0761 の設計に対する提案と、それらの正式な説明 P0443 に基づいています。この投稿は、比較的新しい「Modest Executor Proposal」P1055 にも言及しています。

初めに。エグゼキュータとは?

エグゼキュータ

Executor は、C++ で実行するための基本的な構成要素であり、C++ でのコンテナーのアロケーターなど、実行に関して同様の役割を果たします。 2018 年 6 月に、エグゼキューター向けに多くの提案が書かれましたが、多くの設計上の決定はまだ未解決です。それらは C++23 の一部であることが期待されますが、C++ 標準の拡張機能としてずっと以前から使用できます。

executor は、where に関する一連のルールで構成されます 、いつ 、および方法 callable を実行します。 callable は、関数、関数オブジェクト、またはラムダ関数にすることができます。

  • どこ :callable は内部または外部プロセッサで実行でき、結果は内部または外部プロセッサから読み戻されます。
  • いつ :callable はすぐに実行されるか、単にスケジュールされます。
  • 方法 :callable は CPU または GPU 上で実行されるか、ベクトル化された方法で実行される場合もあります。

エグゼキューターは実行のビルディング ブロックであるため、C++ の同時実行性と並列処理機能はエグゼキューターに大きく依存しています。これは、拡張フューチャー、ラッチとバリア、コルーチン、トランザクション メモリ、タスク ブロックなど、C++20/23 の新しい同時実行機能にも当てはまります。これはネットワークの拡張だけでなく、STL の並列アルゴリズムにも当てはまります。

最初の例

Executor の使用

エグゼキュータ my_excutor の使用法を示すいくつかのコード スニペットを次に示します。

  • 約束 std::async
// get an executor through some means
my_executor_type my_executor = ...

// launch an async using my executor
auto future = std::async(my_executor, [] {
 std::cout << "Hello world, from a new execution agent!" << std::endl;
});

  • STL アルゴリズム std::for_each
// get an executor through some means
my_executor_type my_executor = ...

// execute a parallel for_each "on" my executor
std::for_each(std::execution::par.on(my_executor),
 data.begin(), data.end(), func);

Executor の取得

エグゼキュータを取得するにはさまざまな方法があります。

  • 実行コンテキスト static_thread_pool から
// create a thread pool with 4 threads
static_thread_pool pool(4);

// get an executor from the thread pool
auto exec = pool.executor();

// use the executor on some long-running task
auto task1 = long_running_task(exec);

  • システム実行者から

これは、実行に通常スレッドを使用するデフォルトのエグゼキュータです。別のものが指定されていない場合に使用されます。

  • エグゼキュータ アダプタから
// get an executor from a thread pool
auto exec = pool.executor();

// wrap the thread pool's executor in a logging_executor
logging_executor<decltype(exec)> logging_exec(exec);

// use the logging executor in a parallel sort
std::sort(std::execution::par.on(logging_exec), my_data.begin(), my_data.end());

logging_executor は、プール エグゼキュータのラッパーであるコード スニペットにあります。

Executor コンセプトの目標

提案 P1055 によるエグゼキュータの概念の目標は何ですか?

<オール>
  • バッチ可能 :callable の遷移のコストとそのサイズの間のトレードオフを制御します。
  • 異種 :callable が異種のコンテキストで実行され、結果が返されることを許可します。
  • 注文可能 :callable が呼び出される順序を指定します。目標には、LIFO (L ast n、F 最初の O ut)、FIFO (F) まず n、F 最初の O ut) 実行、優先度または時間の制約、さらには順次実行。
  • 制御可能 :呼び出し可能オブジェクトは、特定のコンピューティング リソースをターゲットにするか、延期するか、キャンセルする必要があります。
  • 継続可能 :非同期呼び出し可能シグナルを制御するには、必要です。これらのシグナルは、結果が利用可能かどうか、エラーが発生したかどうか、callable がいつ終了したか、または呼び出し先が callable をキャンセルしたいかどうかを示す必要があります。 callable の明示的な開始または凝視の停止も可能であるべきです。
  • レイヤー可能 :階層により、単純なユースケースの複雑さを増やさずに機能を追加できます。
  • 使える :実装者とユーザーにとっての使いやすさが主な目標であるべきです。
  • 構成可能 :ユーザーは、標準に含まれていない機能のエグゼキュータを拡張できます。
  • 最小限 :概念の上にあるライブラリに外部から追加できるエグゼキュータの概念には何も存在してはなりません。
  • 実行関数

    executor は、callable から実行エージェントを作成するための 1 つ以上の実行関数を提供します。エグゼキュータは、次の 6 つの機能のうち少なくとも 1 つをサポートする必要があります。

    各実行関数には、カーディナリティと方向の 2 つのプロパティがあります。

    • カーディナリティ :
      • single:1 つの実行エージェントを作成します
      • bulk:実行エージェントのグループを作成します
    • 方向 :
      • oneway:実行エージェントを作成し、結果を返しません
      • twoway:実行エージェントを作成し、実行の完了を待機するために使用できる Future を返します
      • then:実行エージェントを作成し、実行の完了を待機するために使用できる Future を返します。実行エージェントは、特定の Future の準備が整った後に実行を開始します。


    実行関数をもっと非公式に説明しましょう。

    最初に、単一カーディナリティのケースについて説明します。

    • 一方向実行関数は、ファイア アンド フォーゲット ジョブです。これは fire and forget future によく似ていますが、future のデストラクタで自動的にブロックしません。
    • 双方向実行関数は、結果を取得するために使用できる未来を返します。これは、関連する std::future へのハンドルを返す std::promise と同様に動作します。
    • その後の実行は一種の継続です。 Future が返されますが、実行エージェントは、提供された Future の準備ができている場合にのみ実行されます。

    第 2 に、バルク カーディナリティのケースはより複雑です。これらの関数は実行エージェントのグループを作成し、これらの実行エージェントはそれぞれ指定された callable を呼び出します。これらは、実行エージェントによって呼び出された単一の呼び出し可能な f の結果ではなく、ファクトリの結果を返します。ユーザーは、このファクトリを介して正しい結果を明確にする責任があります。

    実行::必須

    エグゼキュータが特定の実行機能をサポートしていることをどのように確認できますか?

    特別なケースでは、あなたはそれを知っています.

    void concrete_context(const my_oneway_single_executor& ex)
    {
     auto task = ...;
     ex.execute(task);
    }
    

    一般的なケースでは、関数 execution::require を使用して要求できます。

    template <typename Executor>
    void generic_context(const Executor& ex)
    {
     auto task = ...;
    
     // ensure .twoway_execute() is available with execution::require()
     execution::require(ex, execution::single, execution::twoway).twoway_execute(task);
    }
    

    この場合、エグゼキュータ ex は単一カーディナリティで双方向のエグゼキュータでなければなりません。

    次は?

    次回の投稿では、C++ コア ガイドラインからの回り道を続けます。先物の未来は、主に執行者のために変化しました。したがって、先物について書きます。