🔌

非同期関数の実装内部でco_awaitを使う

2024/02/10に公開

非同期関数の実装内部でco_awaitを使う

CompletionToken対応の非同期関数

CompletionToken対応の非同期関数は、CompetionTokenに、deferred, use_awaitable, experimental::use_futureを渡すことで、co_awaitが可能となります。またコールバック関数を渡すこともできます。
このようなCompletionToken対応の非同期関数の実装にco_awaitを使う方法を示します。

通常、co_awaitを使うためには、co_awaitを使う関数の戻り値の型が、awaitable<T>である必要があります。こうなると、関数が、coroutine前提となり、コールバック関数対応など柔軟なCompletionToken対応ができません。
これを解決するために、boost::asio::experimental::co_composedを利用します。

Code

#include <iostream>
#include <chrono>

#include <boost/asio.hpp>
#include <boost/asio/experimental/co_composed.hpp>

namespace as = boost::asio;

template <typename CompletionToken>
auto co_composed_func(
    as::any_io_executor exe,
    CompletionToken&& token
) {
    return as::async_initiate<
        CompletionToken,
        void()
    >(
        as::experimental::co_composed<
            void()
        >(
            [](auto /*state*/, auto exe) -> void {
                as::steady_timer tim{exe, std::chrono::seconds(1)};
                auto [ec] = co_await tim.async_wait(as::as_tuple(as::deferred));
                std::cout << ec.message() << std::endl;
                co_return {};
            },
            exe
        ),
        token,
        exe
    );
}

int main() {
    as::io_context ioc;

    std::cout << "start" << std::endl;
    auto start = std::chrono::system_clock::now();

    co_composed_func(
        ioc.get_executor(),
        [&] {
            auto end = std::chrono::system_clock::now();
            double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
            std::cout << elapsed << std::endl;
        }
    );
    ioc.run();
}

godboltでの実行:
https://godbolt.org/z/fheq4xc8v

今回実装する非同期関数は、co_composed_func()です。int main()からコールバック関数をCompletionTokenとして呼び出しています。ここまで、Coroutine的な要素が混入していないことに注意してください。

async_initiateの第1引数として、experimental::co_composedを渡します。template引数に、signatureを渡しますが、これは、async_initiateの第2 template引数と同じsignatureを渡します。async_initiateの、第1引数には、tokenを、第2引数以降は任意の引数を渡します。co_composedの第1引数statetokenに対応します。第2引数以降はasync_initiateの引数が対応します。

さて、ここで1秒タイマを設定しています。そして、タイマの、async_waitのCompletionTokenとして、deferredを渡しています。そして、co_awaitで完了を待っています。このように、co_composed_func()の戻り値がawaitable<T>ではないにも関わらず、関数の実装で、co_awaitを使うことができます。実装の戻り値は、co_returnで指定します。また、戻り値がvoidであっても、{}と空のinitialize listを返す必要がある点に注意してください。
なお、実装内部のCompletionTokenとして、deferredを用いていますが、ここで、use_awaitableを使うことはできません。これが、co_composedの現在の制約といえます。deferredでは型情報が維持されるため、variantなどとの組み合わせで、戻り値の型が一致しないエラーに遭遇することがあります。このような場合、型消去が必要となりますが、use_awaitableが使えないため、experimental::use_promiseを使うことになるでしょう。

備考

本記事は、experimentalな要素に関しての内容であり、Boost.1.84.0で動作確認しています。将来、仕様が変更される可能性がある点に注意してください。

GitHubで編集を提案

Discussion