非同期関数の実装内部でco_awaitを使う
非同期関数の実装内部で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での実行:
今回実装する非同期関数は、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引数stateはtokenに対応します。第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で動作確認しています。将来、仕様が変更される可能性がある点に注意してください。
Discussion