財団
簡単な例から始めて、関連するBoost.Asioの部分を調べてみましょう。
void handle_async_receive(...) { ... }
void print() { ... }
...
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
...
io_service.post(&print);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
ハンドラーとは何ですか?
ハンドラは、コールバック以外の何ものでもありません。サンプルコードには、次の3つのハンドラーがあります。
print
ハンドラ(1)。
handle_async_receive
ハンドラ(3)。
print
ハンドラ(4)。
同じprint()
関数が2回使用されたとしても、使用するたびに、一意に識別可能な独自のハンドラーが作成されると見なされます。ハンドラーには、上記のような基本的な関数からboost::bind()
、ラムダから生成されたファンクターやラムダなどのより複雑な構造まで、さまざまな形やサイズがあります。複雑さに関係なく、ハンドラーはコールバックにすぎません。
仕事とは?
作業は、Boost.Asioがアプリケーションコードに代わって実行するように要求された処理です。Boost.Asioは、通知されるとすぐに一部の作業を開始する場合もあれば、後の時点で作業を待機する場合もあります。作業が完了すると、Boost.Asioは提供されたハンドラーを呼び出してアプリケーションに通知します。
Boost.Asioがあることを保証ハンドラは唯一、現在呼び出しているスレッド内で実行されますrun()
、run_one()
、poll()
、またはpoll_one()
。これらは、作業を行い、ハンドラーを呼び出すスレッドです。したがって、上記の例でprint()
は、io_service
(1)に投稿されたときに呼び出されません。代わりに、に追加io_service
され、後で呼び出されます。この場合、io_service.run()
(5)以内です。
非同期操作とは何ですか?
非同期操作は、仕事を作成し、Boost.Asioが起動しますハンドラを作業が完了したときにアプリケーションに通知します。非同期操作は、接頭辞が付いた名前を持つ関数を呼び出すことによって作成されますasync_
。これらの関数は、開始関数とも呼ばれます。
非同期操作は、次の3つの固有のステップに分解できます。
io_service
動作する関連を開始または通知する必要があります。async_receive
操作(3)に通知するio_service
ことは、その後、非同期ソケットからデータを読み取る必要があることにasync_receive
すぐに戻ります。
- 実際の仕事をしています。この場合、
socket
データを受信すると、バイトが読み取られてにコピーされbuffer
ます。実際の作業は次のいずれかで行われます。
- Boost.Asioがブロックしないと判断できる場合は、開始関数(3)。
- アプリケーションが明示的に実行する場合
io_service
(5)を。
- 起動
handle_async_receive
ReadHandlerを。繰り返しになりますが、ハンドラーはを実行してio_service
いるスレッド内でのみ呼び出されます。したがって、作業がいつ行われるか(3または5)に関係なく、handle_async_receive()
内でのみ呼び出されることが保証されます。io_service.run()
(5)。
これらの3つのステップ間の時間と空間の分離は、制御フローの反転として知られています。これは、非同期プログラミングを困難にする複雑さの1つです。ただし、これを軽減するのに役立つテクニックがあります。コルーチン。
何をしますか io_service.run()
ますか?
スレッドがを呼び出すとio_service.run()
、このスレッド内から作業とハンドラーが呼び出されます。上記の例では、io_service.run()
(5)は次のいずれかになるまでブロックします。
- 両方の
print
ハンドラーから呼び出されて返され、受信操作は成功または失敗で完了し、handle_async_receive
ハンドラーが呼び出されて返されました。
io_service
明示的に経由して停止していますio_service::stop()
。
- ハンドラー内から例外がスローされます。
潜在的な疑似っぽい流れの1つは、次のように説明できます。
io_serviceを作成します
ソケットを作成する
印刷ハンドラーをio_serviceに追加します(1)
ソケットが接続するのを待つ(2)
非同期読み取り作業要求をio_serviceに追加します(3)
印刷ハンドラーをio_serviceに追加します(4)
io_service(5)を実行します
仕事やハンドラーはありますか?
はい、1つの作業と2つのハンドラーがあります
ソケットにはデータがありますか?いいえ、何もしません
印刷ハンドラーの実行(1)
仕事やハンドラーはありますか?
はい、1つの作業と1つのハンドラーがあります
ソケットにはデータがありますか?いいえ、何もしません
印刷ハンドラーの実行(4)
仕事やハンドラーはありますか?
はい、1つの作品があります
ソケットにはデータがありますか?いいえ、待ち続けます
-ソケットはデータを受信します-
ソケットにデータがあり、バッファに読み込みます
handle_async_receiveハンドラーをio_serviceに追加します
仕事やハンドラーはありますか?
はい、1つのハンドラーがあります
handle_async_receiveハンドラーを実行します(3)
仕事やハンドラーはありますか?
いいえ、io_serviceを停止として設定して戻ります
読み取りが終了すると、に別のハンドラーが追加されたことに注意してくださいio_service
。この微妙な詳細は、非同期プログラミングの重要な機能です。これにより、ハンドラーをチェーン化できます。たとえば、handle_async_receive
期待したすべてのデータを取得できなかった場合、その実装は別の非同期読み取り操作をポストする可能性があり、その結果、io_service
より多くの作業が発生し、から返されませんio_service.run()
。
io_service
が機能しなくなっreset()
た場合、アプリケーションはio_service
再実行する前に実行する必要があることに注意してください。
質問の例とコードの例3a
それでは、質問で参照されている2つのコードを調べてみましょう。
質問コード
socket->async_receive
に作業を追加しますio_service
。したがって、io_service->run()
読み取り操作が成功またはエラーで完了し、ClientReceiveEvent
実行が終了するか、例外がスローされるまでブロックします。
理解しやすくするために、注釈付きの小さな例3aを次に示します。
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
高レベルでは、プログラムはio_service
のイベントループを処理する2つのスレッドを作成します(2)。これにより、フィボナッチ数を計算する単純なスレッドプールが作成されます(3)。
質問コードとこのコードの大きな違いの1つは、実際の作業とハンドラーが(3)に追加される前に、このコードがio_service::run()
(2)を呼び出すことです。がすぐに戻らないようにするために、オブジェクトが作成されます(1)。このオブジェクトは、が不足するのを防ぎます。したがって、作業がない場合は戻りません。io_service
io_service::run()
io_service::work
io_service
io_service::run()
全体的なフローは次のとおりです。
- に追加された
io_service::work
オブジェクトを作成して追加しますio_service
。
- を呼び出すスレッドプールが作成されました
io_service::run()
。これらのワーカースレッドはio_service
、io_service::work
オブジェクトのためにから戻りません。
- フィボナッチ数を計算する3つのハンドラーをに追加し、
io_service
すぐに戻ります。メインスレッドではなく、ワーカースレッドがこれらのハンドラーの実行をすぐに開始する場合があります。
io_service::work
オブジェクトを削除します。
- ワーカースレッドの実行が終了するのを待ちます。これは、3つのハンドラーすべてが実行を終了した後にのみ発生します。これは、
io_service
どちらにもハンドラーも動作もないためです。
コードは、元のコードと同じ方法で別の方法で記述できます。この場合、ハンドラーがに追加されio_service
、io_service
イベントループが処理されます。これにより、を使用する必要がなくなりio_service::work
、次のコードになります。
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
同期vs.非同期
問題のコードは非同期操作を使用していますが、非同期操作が完了するのを待っているため、効果的に同期的に機能しています。
socket.async_receive(buffer, handler)
io_service.run();
と同等です:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
一般的な経験則として、同期操作と非同期操作を混在させないようにしてください。多くの場合、それは複雑なシステムを複雑なシステムに変えることができます。この回答は、非同期プログラミングの利点を強調しています。その一部は、Boost.Asioのドキュメントでも説明されています。