Networking TS の Boost.Asio からの変更点 - その 2: async_result
はじめに
前回は Boost.Asio から Networking TS への変更点として Associated allocator について説明しました.
今回は, async_result について説明します.
Boost.Asio における非同期処理スタイル
Boost.Asio の非同期関数はすべて継続渡しスタイルをベースに設計, 実装されており, 継続となる関数オブジェクトを最終引数として受け取ります.
socket.async_read_some( asio::buffer(buf) // 継続渡し: 読み込み完了時に実行される , [&](auto ec, std::size_t nread) { asio::async_write( socket, asio::buffer(buf, nread) // 継続渡し: 書き込み完了時に実行される , [&](auto ec, std::size_t) { ... }); });
しかしながら, 非同期処理のスタイルは継続渡し以外にもコルーチンを使用したものなどあり, それぞれの利点, 欠点があります.
そのため, Boost.Asio では継続渡しスタイルをベースにしながらも, 他のスタイルもサポートしています.
そのために使用されるのが async_result
と handler_type
の二つのクラステンプレートです.
これらの説明をするためにコルーチンを使用した例を見てみます.
// コルーチンの例 asio::spawn(io_service, [&](asio::yield_context yield) { unsigned char buf[255]; auto nread = socket.async_read_some(asio::buffer(buf), yield); asio::async_write(socket, asio::buffer(buf, nread), yield); });
この例では非同期関数に yield_context
型のオブジェクト yield
を最終引数に渡していますが,
このオブジェクトは関数オブジェクトではありません.
継続渡しスタイルベースで実装されている非同期関数に非関数オブジェクトを渡すことができるのは,
非同期関数が内部でこれを関数オブジェクトに変換しているからです.
この変換に使用されるのが handler_type
です.
handler_type
はテンプレート引数に非同期関数の最終引数の型を受け取ります.
この型は CompletionToken と呼ばれ, handler_type
は CompletionToken によって特殊化されます.
特殊化された handler_type
は関数オブジェクトの型を依存型として返すので,
非同期関数はこれを継続として使用します.
ちなみに, handler_type
はデフォルトでは CompletionToken をそのまま返すので,
継続渡しスタイルの場合, 継続として渡した関数オブジェクトがそのまま使用されることになります.
async_result
コルーチンの例では非同期関数 async_read_some
に戻り値があることがわかります
(使用していませんが async_write
も戻り値はあります).
一番初めの継続渡しの例では戻り値はありません (コード上からはわかりませんが).
このように非同期関数の戻り値も CompletionToken によって変わります.
戻り値を決めるのが async_result
の役目です.
async_result
は戻り値の型, 及び戻り値の生成を責務としており,
handler_type で変換された関数オブジェクトの型で特殊化されます.
Boost.Asio では各非同期スタイルに対して handler_type
と async_result
を特殊化するだけでよいので,
非同期関数はただ一つ定義すればよいことになります.
Networking TS における async_result
Networking TS でも Boost.Asio 同様 CompletionToken の仕組みを使用します.
しかし, Networking TS では handler_type
は存在しません.
CompletionToken から関数オブジェクトへの変換も async_result
が受け持ちます.
このため, async_result は変換後の関数オブジェクトの型ではなく CompletionToken で特殊化されます.
この変更がどのように影響するかを次節で見ていきます.
async_result と handler_type 統合による影響
Boost.Asio の場合
async_result
への統合への影響を説明するためにまた例を出します.
今度の例は, ある strand 内で非同期関数を呼び出す, という非同期関数です. マルチスレッドを使用している場合, このような処理をしたくなる場合があると思います.
template <class Stream, class ConstBufferSequence, class CompletionToken> auto async_write_in_some_strand( Stream& stream, ConstBufferSequence const& buffers , asio::io_service::strand strand , CompletionToken&& token) { using handler = typename asio::handler_type< CompletionToken, void(system::error_code, std::size_t)>::type; handler h{std::forward<CompletionToken>(token)}; asio::async_result<handler> result{h}; // strand の中で非同期関数を呼び出す. strand.post([&stream, buffers, h]{ asio::async_write(stream, buffers, h); }); return resutl.get(); }
この関数は一見問題なさそうに見えますが, 実は致命的なバグがあります.
コルーチンを使用してこの関数を呼び出すと未定義の動作を招きます. 大体の場合, この関数を呼び出したコンテキストにコルーチンが戻ってきません.
これは特殊化された async_result
が, async_write_in_some_strand
と asio::async_write
の両方で使用されるためです
(Boost.Asio の async_result
は handler_type
で変換した型で特殊化されることを思い出してください).
handler
で特殊化された async_result
のメンバ関数 get
(上記の result.get()
) は,
コルーチンから一度抜けるように実装されているので, コルーチンから二回抜けようとするわけです.
上記の関数を正しく動作させるためには, asio::async_write
で特殊化された async_result
が使用されないように,
handler_type
で取得した関数オブジェクトをラップしてあげる必要があります.
template <class Stream, class ConstBufferSequence, class CompletionToken> auto async_write_in_some_strand( Stream& stream, ConstBufferSequence const& buffers , asio::io_service::strand strand , CompletionToken&& token) { using handler = typename asio::handler_type< CompletionToken, void(system::error_code, std::size_t)>::type; handler h{std::forward<CompletionToken>(token)}; asio::async_result<handler> result{h}; strand.post([&stream, buffers, h]{ asio::async_write( stream, buffers // 特殊化した async_result が使われないようにラップする , [h](auto&&... args) { h(std::forward<decletype(args)>(args)...); }); }); return resutl.get(); }
上記では簡略化のためラムダ式を使用しましたが, asio_handler_allocate
等の他のカスタマイゼーションポイントを有効にするには
きちんとそれらの関数を転送するラッパを書く必要があります.
Networking TS の場合
Networking TS ではこのような誤りは起こりにくくなっています.
template <class Stream, class ConstBufferSequence, class CompletionToken> auto async_write_in_some_strand( Stream& stream, ConstBufferSequence const& buffers , asio::io_service::strand strand , CompletionToken&& token) { using aresult = asio::async_result< CompletionToken, void(system::error_code, std::size_t)>; typename aresult::handler_type h{std::forward<CompletionToken>(token)}; aresult result{h}; strand.post([&stream, buffers, h]{ // async_result は CompletionToken に対して特殊化されており, // aresult::handler_type に対しては特殊化されていないのでラップする必要はない. asio::async_write(stream, buffers, h); }); return result.get(); }
async_result
は CompletionToken で特殊化されており, 内部で呼び出す非同期関数には変換後の関数オブジェクトが渡されるため,
特殊化された async_result
が再び使用されることはありません.
まとめ
Boost.Asio では継続渡し以外の非同期処理スタイルをサポートするため,
async_result
と handler_type
の二つの関数テンプレートをカスタマイゼーションポイントとして使用しています.
しかし, これらを使用して複雑な非同期処理関数を書く際, 陥りやすい問題があることを説明しました.
Networking TS ではこれらを一つに統合したことで, 複雑な非同期処理関数でも謝りなく記述できるようになりました.
次は Executor 周りについて説明しようと思います.