はじめに
コールバック関数の使用は、Boost.Asioライブラリ(だけでなく)を使用してネットワークアプリケーションを構築する一般的なアプローチです。 このアプローチの問題は、データ交換プロトコルのロジックを複雑にしている間、コードの可読性と保守性の悪化です[1] 。
コールバックの代替として、コルーチンを使用して非同期コードを記述できます。非同期コードの可読性レベルは、同期コードの可読性に近くなります。 Boost.Asioは、Boost.Coroutineライブラリを使用してコールバックを処理する機能を提供することにより、このアプローチをサポートしています。
Boost.Coroutineは、現在のスレッドの実行コンテキストを保存することにより、コルーチンを実装します。 このアプローチは、新しいキーワードco_return、co_yield、co_awaitを導入するMicrosoftの提案により、C ++標準の次のエディションに含めるために競合しました。 Microsoftの提案は、技術仕様(TS) [2]のステータスを取得しており、標準になる可能性が高くなっています。
記事[3]は、Coroutines TSおよびboost :: futureでBoost.Asioを使用する方法を示しています。 私の記事では、ブーストなしで行う方法::将来を示したいと思います。 Boost.Asioの非同期TCPエコーサーバーの例をベースとして、Coroutines TSのコルーチンを使用して変更します。
この記事の執筆時点で、Coroutines TSはVisual C ++ 2017およびclang 5.0コンパイラに実装されています。 clangを使用します。 C ++ 20標準(-std = c ++ 2a)およびCoroutines TS(-fcoroutines-ts)の実験的サポートを有効にするには、コンパイラフラグを設定する必要があります。 また、ヘッダー<experimental / coroutine>を含める必要があります。
ソケットから読み取るためのコルーチン
元の例では、ソケットから読み取るための関数は次のようになります。
void do_read() { auto self(shared_from_this()); socket_.async_read_some( boost::asio::buffer(data_, max_length), [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) { do_write(length); } }); }
ソケットから非同期読み取りを開始し、データを受信してその送信を開始するときに呼び出されるコールバックを設定します。 オリジナルの録音機能は次のようになります。
void do_write(std::size_t length) { auto self(shared_from_this()); boost::asio::async_write( socket_, boost::asio::buffer(data_, length), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { do_read(); } }); }
ソケットへのデータの書き込みが成功すると、非同期読み取りを再び開始します。 本質的に、プログラムロジックはループ(擬似コード)に縮小されます。
while (!ec) { ec = read(buffer); if (!ec) { ec = write(buffer); } }
これを明示的なループの形式でエンコードすると便利ですが、この場合は同期操作で読み取りと書き込みを行う必要があります。 これは、1つの実行スレッドで複数のクライアントセッションを同時に処理するため、適切ではありません。 コルーチンが助けになります。 do_read()関数を次のように書き直します。
void do_read() { auto self(shared_from_this()); const auto[ec, length] = co_await async_read_some( socket_, boost::asio::buffer(data_, max_length)); if (!ec) { do_write(length); } }
co_awaitキーワード(およびco_yieldおよびco_return)を使用すると、関数がコルーチンに変わります。 このような関数には、状態(ローカル変数の値)を維持しながら実行が中断(中断)するポイントがいくつかあります(中断ポイント)。 最後の停止から開始して、後でコルーチンの実行を再開(再開)できます。 この関数のco_awaitキーワードは一時停止ポイントを作成します。非同期読み取りが開始された後、do_read()コルーチンの実行は読み取りが完了するまで一時停止されます。 関数からの戻りはありませんが、プログラムの実行はコルーチンを呼び出した時点から継続されます。 クライアントが接続すると、セッション:: start()が呼び出されます。このセッションでは、do_read()が初めて呼び出されます。 非同期読み取りの開始後、start()関数は引き続き実行され、そこから戻り、次の接続が開始されます。 次に、async_accept()引数ハンドラーを呼び出したAsioのコードの実行が継続されます。
co_awaitマジックが機能するためには、その式(この例ではasync_read_some()関数)が特定のコントラクトに一致するクラスのオブジェクトを返す必要があります。 async_read_some()の実装は、記事[3]の解説から取られています。
template <typename SyncReadStream, typename DynamicBuffer> auto async_read_some(SyncReadStream &s, DynamicBuffer &&buffers) { struct Awaiter { SyncReadStream &s; DynamicBuffer buffers; std::error_code ec; size_t sz; bool await_ready() { return false; } void await_suspend(std::experimental::coroutine_handle<> coro) { s.async_read_some(std::move(buffers), [this, coro](auto ec, auto sz) mutable { this->ec = ec; this->sz = sz; coro.resume(); }); } auto await_resume() { return std::make_pair(ec, sz); } }; return Awaiter{s, std::forward<DynamicBuffer>(buffers)}; }
async_read_some()は、co_awaitが必要とするコントラクトを実装するAwaiterクラスのオブジェクトを返します。
- await_ready()は、非同期操作の結果が既に準備ができているかどうかを確認するために、待機の開始時に呼び出されます。 結果を取得するには、データが読み取られるまで常に待機する必要があるため、falseを返します。
- await_suspend()は、呼び出し元のコルーチンが一時停止される前に呼び出されます。 ここで非同期読み取りを開始し、非同期操作の結果をAwaiterクラスのメンバー変数に保存してコルーチンを再開するハンドラーを渡します。
- await_resume()-この関数の戻り値は、co_awaitを実行した結果になります。 以前に保存した非同期操作の結果を返すだけです。
プログラムをビルドしようとすると、コンパイルエラーが発生します。
error: this function cannot be a coroutine: 'std::experimental::coroutines_v1::coroutine_traits<void, session &>' has no member named 'promise_type' void do_read() { ^
理由は、コルーチンに対しても特定のコントラクトを実装する必要があるためです。 これは、std :: Experimental :: coroutine_traitsテンプレートの特殊化を使用して行われます。
template <typename... Args> struct std::experimental::coroutine_traits<void, Args...> { struct promise_type { void get_return_object() {} std::experimental::suspend_never initial_suspend() { return {}; } std::experimental::suspend_never final_suspend() { return {}; } void return_void() {} void unhandled_exception() { std::terminate(); } }; };
void型の戻り値と任意の数と型のパラメーターを持つコルーチン用にcoroutine_traitsを特殊化しました。 do_read()コルーチンはこの説明に適合します。 テンプレートの特殊化には、次の機能を持つpromise_typeタイプが含まれます。
- get_return_object()が呼び出されて、コルーチンが後で埋めて返すオブジェクトを作成します。 この場合、do_read()は何も返さないため、何も作成する必要はありません。
- initial_suspend()は、最初の呼び出しの前にコルーチンが中断されるかどうかを決定します。 類推は、Windowsで中断されたスレッドを開始することです。 最初に停止することなく実行するにはdo_read()が必要なので、suspend_neverを返します。
- final_suspend()は、値を返して終了する前にコルーチンを中断するかどうかを決定します。 suspend_neverを返します。
- return_void()は、コルーチンが何も返さないことをコンパイラーに伝えます。
- コルーチン内で例外がスローされ、コルーチン内で処理されなかった場合、unhandled_exception()がスローされます。 この場合、プログラムはクラッシュします。
これで、telnetを使用して複数の接続を開くことにより、サーバーを起動してそのパフォーマンスを確認できます。
ソケットに書き込むためのコルーチン
do_write()書き込み関数は、コールバックの使用に基づいています。 修正してください。 do_write()を次のように書き換えます。
auto do_write(std::size_t length) { auto self(shared_from_this()); struct Awaiter { std::shared_ptr<session> ssn; std::size_t length; std::error_code ec; bool await_ready() { return false; } auto await_resume() { return ec; } void await_suspend(std::experimental::coroutine_handle<> coro) { const auto[ec, sz] = co_await async_write( ssn->socket_, boost::asio::buffer(ssn->data_, length)); this->ec = ec; coro.resume(); } }; return Awaiter{self, length}; }
ソケットに書き込むための待機可能なラッパーを作成しましょう。
template <typename SyncReadStream, typename DynamicBuffer> auto async_write(SyncReadStream &s, DynamicBuffer &&buffers) { struct Awaiter { SyncReadStream &s; DynamicBuffer buffers; std::error_code ec; size_t sz; bool await_ready() { return false; } auto await_resume() { return std::make_pair(ec, sz); } void await_suspend(std::experimental::coroutine_handle<> coro) { boost::asio::async_write( s, std::move(buffers), [this, coro](auto ec, auto sz) mutable { this->ec = ec; this->sz = sz; coro.resume(); }); } }; return Awaiter{s, std::forward<DynamicBuffer>(buffers)}; }
最後のステップは、do_read()を明示的なループとして書き直すことです。
void do_read() { auto self(shared_from_this()); while (true) { const auto[ec, sz] = co_await async_read_some( socket_, boost::asio::buffer(data_, max_length)); if (!ec) { auto ec = co_await do_write(sz); if (ec) { std::cout << "Error writing to socket: " << ec << std::endl; break; } } else { std::cout << "Error reading from socket: " << ec << std::endl; break; } } }
プログラムロジックは、同期コードに近い形式で記述されるようになりましたが、非同期で実行されます。 軟膏のフライは、do_write()の戻り値用に追加の待機可能クラスを作成する必要があったことです。 これは、コルーチンTSの欠点の1つ-co_awaitの非同期呼び出しのスタックへの広がり[4]を示しています。
サーバーの再作成::コルーチン内のdo_accept()関数は演習として残されています。 プログラムの全文はGitHubにあります 。
おわりに
Boost.AsioとCoroutines TSを使用して、非同期ネットワークアプリケーションのプログラミングを検討しました。 このアプローチの利点は、同期に近い形になるため、コードの可読性が向上することです。 欠点は、Coroutines TSに実装されたコルーチンモデルをサポートするために追加のラッパーを作成する必要があることです。