web-dev-qa-db-ja.com

Boost.Asioカスタムサービスの実装を理解しようとしています

現在使用している既存の独自のサードパーティネットワークプロトコルの上にカスタムAsioサービスを作成することを考えています。

Highscore Asioガイド によると、カスタムAsioサービスを作成するには3つのクラスを実装する必要があります。

  • 新しいI/Oオブジェクトを表すboost::asio::basic_io_objectから派生したクラス。
  • boost::asio::io_service::serviceから派生したクラスで、I/Oサービスに登録され、I/Oオブジェクトからアクセスできるサービスを表します。
  • サービスの実装を表す他のクラスから派生していないクラス。

ネットワークプロトコルの実装はすでに非同期操作を提供しており、(ブロッキング)イベントループがあります。そこで、それをサービス実装クラスに入れて、内部ワーカースレッドでイベントループを実行すると思いました。ここまでは順調ですね。

カスタムサービスのいくつかの例を見ると、サービスクラスが独自の内部スレッドを生成していることに気付きました(実際、それらは独自の内部io_serviceインスタンスをインスタンス化します)。例えば:

  1. ハイスコ​​アページは ディレクトリモニターの例 を提供します。これは本質的にinotifyのラッパーです。興味深いクラスはinotify/basic_dir_monitor_service.hppinotify/dir_monitor_impl.hppです。 Dir_monitor_implは、ブロックしているinofityとの実際の相互作用を処理するため、バックグラウンドスレッドで実行されます。私はそれに同意します。ただし、basic_dir_monitor_serviceには内部ワーカースレッドもあり、実行しているように見えるのは、メインのio_servicedir_monitor_implの間でリクエストをシャッフルすることだけです。コードをいじって、basic_dir_monitor_serviceのワーカースレッドを削除し、代わりにメインのio_serviceに直接リクエストを投稿しましたが、プログラムは以前と同じように実行されました。

  2. Asioの カスタムロガーサービスの例 では、同じアプローチに気づきました。 logger_service は、ロギング要求を処理するための内部ワーカースレッドを生成します。私はそのコードをいじくり回す時間がありませんでしたが、これらのリクエストをメインのio_serviceに直接投稿することも可能であると思います。

これらの「仲介労働者」を持つことの利点は何ですか?すべての作業を常にメインのio_serviceに投稿できませんでしたか? Proactorパターンの重要な側面を見逃しましたか?

私は、パワー不足のシングルコア組み込みシステム用のソフトウェアを書いていることをおそらく言及する必要があります。これらの追加のスレッドを配置すると、不要なコンテキストスイッチが発生するように思われますが、可能であれば避けたいと思います。

27
djf

要するに、一貫性。これらのサービスは、Boost.Asioが提供するサービスによって示されるユーザーの期待に応えようとします。

内部io_serviceを使用すると、所有権とハンドラーの制御を明確に分離できます。カスタムサービスがその内部ハンドラーをユーザーのio_serviceにポストする場合、サービスの内部ハンドラーの実行は暗黙的にユーザーのハンドラーと結合されます。 Boost.Asio Logger Service の例で、これがユーザーの期待にどのように影響するかを検討してください。

  • logger_serviceは、ハンドラー内のファイルストリームに書き込みます。したがって、同期APIのみを使用するプログラムなど、io_serviceイベントループを処理しないプログラムでは、ログメッセージが書き込まれることはありません。
  • logger_serviceはスレッドセーフではなくなり、io_serviceが複数のスレッドによって処理されると、未定義の動作が発生する可能性があります。
  • logger_serviceの内部操作の存続期間は、io_serviceの存続期間によって制約されます。たとえば、サービスのshutdown_service()関数が呼び出されたとき、所有しているio_serviceの存続期間はすでに終了しています。したがって、メッセージはlogger_service::log()内のshutdown_service()を介してログに記録できませんでした。これは、有効期間がすでに終了しているio_serviceに内部ハンドラーをポストしようとするためです。
  • ユーザーは、操作とハンドラーの間の1対1のマッピングを想定できなくなりました。例えば:

    boost::asio::io_service io_service;
    debug_stream_socket socket(io_service);
    boost::asio::async_connect(socket, ..., &connect_handler);
    io_service.poll();
    // Can no longer assume connect_handler has been invoked.
    

    この場合、io_service.poll()は、connect_handler()ではなく、logger_serviceの内部のハンドラーを呼び出すことができます。

さらに、これらの内部スレッドは、Boost.Asioによって内部的に使用される動作を模倣しようとします それ自体

特定のプラットフォーム用のこのライブラリの実装では、1つ以上の内部スレッドを使用して非同期性をエミュレートできます。可能な限り、これらのスレッドはライブラリユーザーには見えないようにする必要があります。


ディレクトリモニターの例

ディレクトリモニターの例では、内部スレッドを使用して、イベントの待機中にユーザーのio_serviceが無期限にブロックされるのを防ぎます。イベントが発生すると、完了ハンドラーを呼び出す準備が整うため、内部スレッドは、ユーザーハンドラーをユーザーのio_serviceにポストして、呼び出しを延期します。この実装は、ユーザーにはほとんど見えない内部スレッドとの非同期性をエミュレートします。

詳細については、非同期モニター操作がdir_monitor::async_monitor()を介して開始されると、basic_dir_monitor_service::monitor_operationが内部io_serviceに投稿されます。この操作が呼び出されると、dir_monitor_impl::popfront_event()が呼び出され、呼び出しがブロックされる可能性があります。したがって、monitor_operationがユーザーのio_serviceに投稿された場​​合、ユーザーのスレッドは無期限にブロックされる可能性があります。次のコードへの影響を考慮してください。

boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor(io_service); 
dir_monitor.add_directory(dir_name); 
// Post monitor_operation into io_service.
dir_monitor.async_monitor(...);
io_service.post(&user_handler);
io_service.run();

上記のコードでは、io_service.run()が最初にmonitor_operationを呼び出すと、dir_monitordir_nameディレクトリのイベントを監視するまで、user_handler()は呼び出されません。したがって、dir_monitorサービスの実装は、ほとんどのユーザーが他のサービスに期待する一貫した方法で動作しません。

Asio Logger Service

内部スレッドとio_serviceの使用:

  • 内部スレッド内で潜在的にブロックまたはコストのかかる呼び出しを実行することにより、ユーザーのスレッドへのログオンのオーバーヘッドを軽減します。
  • 単一の内部スレッドのみがストリームに書き込むため、std::ofstreamのスレッドセーフを保証します。ロギングがlogger_service::log()内で直接行われた場合、またはlogger_serviceがハンドラーをユーザーのio_serviceに投稿した場合、スレッドセーフのために明示的な同期が必要になります。他の同期メカニズムは、実装に大きなオーバーヘッドまたは複雑さをもたらす可能性があります。
  • servicesshutdown_service() 内のメッセージをログに記録できるようにします。 destruction の間、io_serviceは次のようになります。

    1. 各サービスをシャットダウンします。
    2. io_serviceまたはそれに関連するstrandsのいずれかで遅延呼び出しがスケジュールされたすべての未呼び出しハンドラーを破棄します。
    3. その各サービスを破壊します。


    ユーザーのio_serviceの有効期間が終了したため、そのイベントキューは処理されておらず、追加のハンドラーを投稿することもできません。 io_serviceは、独自のスレッドによって処理される独自の内部logger_serviceを持つことにより、他のサービスがshutdown_service()中にメッセージをログに記録できるようにします。


追加の考慮事項

カスタムサービスを実装する際に考慮すべき点がいくつかあります。

  • ねじ山のすべての信号をブロックします。
  • ユーザーのコードを直接呼び出さないでください。
  • 実装が破棄されたときにユーザーハンドラーを追跡して投稿する方法。
  • サービスの実装間で共有される、サービスが所有するリソース。

最後の2つのポイントでは、dir_monitor I/Oオブジェクトはユーザーが予期しない動作を示します。サービス内の単一のスレッドが単一の実装のイベントキューでブロック操作を呼び出すと、それぞれの実装ですぐに完了する可能性のある操作を効果的にブロックします。

boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor1(io_service); 
dir_monitor1.add_directory(dir_name1); 
dir_monitor1.async_monitor(&handler_A);

boost::asio::dir_monitor dir_monitor2(io_service); 
dir_monitor2.add_directory(dir_name2); 
dir_monitor2.async_monitor(&handler_B);
// ... Add file to dir_name2.

{
  // Use scope to enforce lifetime.
  boost::asio::dir_monitor dir_monitor3(io_service); 
  dir_monitor3.add_directory(dir_name3); 
  dir_monitor3.async_monitor(&handler_C);
}
io_service.run();

handler_B()(成功)およびhandler_C()(中止)に関連する操作はブロックされませんが、basic_dir_monitor_serviceの単一スレッドは、dir_name1への変更を待ってブロックされます。

27
Tanner Sansbury