C++における関数バインディングと非同期タスクのラッピング

関数バインディングの基本を確認してみましょう:

int processData(std::string text, int id, int val1, int val2, int val3)
{
    std::cout << id << ":" << text << std::endl;
    return text.length() + val1 + val2 + val3;
}

これは複数の引数を取る関数の例です。次に、この関数の第1、3、4、5引数をバインドする関数を作成します:

void bindFunctionWithParameters(std::string const& message)
{
    int value1, value2, value3;

    std::cout << "三つの整数を入力してください:";
    std::cin >> value1 >> value2 >> value3;

    // バインディングを実行
    using ProcessFunction = std::function<int(std::string, int, int, int)>;
    ProcessFunction boundFunc = std::bind(processData, std::placeholders::_1, value1, value2, value3);

    // 典型的な使用法:バインドした結果を他の関数に渡す
    executeTask(boundFunc);
}

「ポリシー」引数を持たないasync()関数の引数リストは「(関数、引数1、引数2、引数3…)」です。bind()関数の引数リストと見比べてみると、両者は非常によく似ていることがわかります。どちらも即座に処理を実行するのではなく、処理とその処理に必要なデータをパッケージ化します。

主な違いは、bind()は現在のスレッドでパッケージ化し、「ラッパー」オブジェクトを取得します。このオブジェクトは現在のスレッドで実行することも、他のスレッドに渡して実行することもできます。

一方、async()はバインディングの詳細には関心がなく、主に現在のスレッドで他のスレッドが実行した「ラッパー」の結果をどのように取得するかという問題を解決します。これは「スタブ」とも呼ばれます。

async()呼び出し時に「遅延」ポリシーを指定すると、その効果はbind()にさらに近づきます。

もう一つの細かい違いは、bind()では最終的な関数呼び出しコードを自分で記述するため、バインディング時に一部のデータのみをバインドし、残りのデータは「placeholders::_1、placeholders::_2」などの定義済み定数で代用できます。

一方、async()での操作呼び出しは標準ライブラリの作者によって実装されています。もし十分なデータを提供しなければ、標準ライブラリはどのようにそれを補完すればよいのでしょうか?

「射撃、銃、弾丸、敵」という例で続けましょう。bind()の場合は弾丸を銃に装填し、銃を身につけて敵を探し、見つけてから狙って撃つようなものです。

async()の場合は弾丸を銃に装填し、銃を部下に渡して「敵を探し、狙って撃つ」という操作を実行するように指示し、私たちは操作の結果を確認する権利と手段を保持します。

もちろん、bind()の後にasync()を使用して別のスレッドで実行することもできます。いつ自分(現在のスレッド)が、いつ他人(他のスレッド)がこの操作を実行するかを即座に決定できないが、すでにその操作に必要なデータの一部を持っている場合、この2つの機能を組み合わせることが特に有用です。

ここでは、lambda式とautoを加えた「四つの強力な機能の連携」の例を見てみましょう。同期実行の場合は現在のスレッド内で実行され、非同期実行の場合は新しいスレッドで実行されます。

例の「同期」実行を選択する場合を考慮しない場合、全体の処理を次のように簡略化できます:最初にbind()で操作とデータをバインドして「ラッパー」オブジェクトを取得し、必要なときにこの「ラッパー」をasync()に渡して非同期呼び出しを実現します。

これに対応するニーズは、「非同期タスクをパッケージ化しておき、後で必要になったときに実行したい」というものです。

C++標準ライブラリでは、この目的のために非常に分かりやすい名前であるpackaged_task<T>が用意されています。ここでTは何らかの操作の型です。操作は関数、関数オブジェクト、lambda、bind()の結果、function<T>などです。

int calculateSum(int num1, int num2) { return (num1 + num2); }

int main()
{
    packaged_task<int(int, int)> taskWrapper(calculateSum);

    std::future<int> result = taskWrapper.get_future(); 
    taskWrapper(10, 11); 

    std::cout << result.get() << std::endl;
    return 0;
}

5行目で、「packaged_task(パッケージ化されたタスク)」を作成しています。「ラッパー」には実行待ちの操作が含まれており、この例ではcalculateSum()関数です。「ラッパー」には操作に必要な引数は含まれていません。必要な場合は、bind()と組み合わせて使用してください。

7行目で、packaged_task<T>のget_future()メソッドを呼び出し、「ラッパー」から「スタブ」を取得します。

8行目でtaskWrapper()を呼び出します。実際にはtaskWrapperはオブジェクト変数であり、packaged_task<T>はクラステンプレートです。重要なのは、この呼び出しメカニズムが「ポリシー」なしバージョンのasync()関数呼び出しと同等であり、同期でも非同期でも可能性があるということです。

10行目で、前の「スタブ」オブジェクトを使用してタスクの実行結果をクエリ・待機します。

bind()を使用すると、ジェームズ・ボンドは銃に弾丸を装填し、敵に遭遇したときに撃つことができます。packaged_task()を加えると、ボンドは弾丸を部下に渡し、緊急時に部下に撃たせることができます。ではボンドは何をするのでしょうか?『白話C++』の後の章で、テトリスでお会いしましょう。

タグ: C++ concurrency async bind packaged_task

6月12日 19:40 投稿