現在使用している既存の独自のサードパーティネットワークプロトコルの上にカスタムAsioサービスを作成することを考えています。
Highscore Asioガイド によると、カスタムAsioサービスを作成するには3つのクラスを実装する必要があります。
boost::asio::basic_io_object
から派生したクラス。boost::asio::io_service::service
から派生したクラスで、I/Oサービスに登録され、I/Oオブジェクトからアクセスできるサービスを表します。ネットワークプロトコルの実装はすでに非同期操作を提供しており、(ブロッキング)イベントループがあります。そこで、それをサービス実装クラスに入れて、内部ワーカースレッドでイベントループを実行すると思いました。ここまでは順調ですね。
カスタムサービスのいくつかの例を見ると、サービスクラスが独自の内部スレッドを生成していることに気付きました(実際、それらは独自の内部io_serviceインスタンスをインスタンス化します)。例えば:
ハイスコアページは ディレクトリモニターの例 を提供します。これは本質的にinotifyのラッパーです。興味深いクラスはinotify/basic_dir_monitor_service.hpp
とinotify/dir_monitor_impl.hpp
です。 Dir_monitor_impl
は、ブロックしているinofityとの実際の相互作用を処理するため、バックグラウンドスレッドで実行されます。私はそれに同意します。ただし、basic_dir_monitor_service
には内部ワーカースレッドもあり、実行しているように見えるのは、メインのio_service
とdir_monitor_impl
の間でリクエストをシャッフルすることだけです。コードをいじって、basic_dir_monitor_service
のワーカースレッドを削除し、代わりにメインのio_serviceに直接リクエストを投稿しましたが、プログラムは以前と同じように実行されました。
Asioの カスタムロガーサービスの例 では、同じアプローチに気づきました。 logger_service
は、ロギング要求を処理するための内部ワーカースレッドを生成します。私はそのコードをいじくり回す時間がありませんでしたが、これらのリクエストをメインのio_serviceに直接投稿することも可能であると思います。
これらの「仲介労働者」を持つことの利点は何ですか?すべての作業を常にメインのio_serviceに投稿できませんでしたか? Proactorパターンの重要な側面を見逃しましたか?
私は、パワー不足のシングルコア組み込みシステム用のソフトウェアを書いていることをおそらく言及する必要があります。これらの追加のスレッドを配置すると、不要なコンテキストスイッチが発生するように思われますが、可能であれば避けたいと思います。
要するに、一貫性。これらのサービスは、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_monitor
がdir_name
ディレクトリのイベントを監視するまで、user_handler()
は呼び出されません。したがって、dir_monitor
サービスの実装は、ほとんどのユーザーが他のサービスに期待する一貫した方法で動作しません。
内部スレッドとio_service
の使用:
std::ofstream
のスレッドセーフを保証します。ロギングがlogger_service::log()
内で直接行われた場合、またはlogger_service
がハンドラーをユーザーのio_service
に投稿した場合、スレッドセーフのために明示的な同期が必要になります。他の同期メカニズムは、実装に大きなオーバーヘッドまたは複雑さをもたらす可能性があります。services
が shutdown_service()
内のメッセージをログに記録できるようにします。 destruction の間、io_service
は次のようになります。
io_service
またはそれに関連するstrand
sのいずれかで遅延呼び出しがスケジュールされたすべての未呼び出しハンドラーを破棄します。
ユーザーの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
への変更を待ってブロックされます。