財団
簡単な例から始めて、関連する 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); // 1
socket.connect(endpoint); // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print); // 4
io_service.run(); // 5
ハンドラーとは?
ハンドラーは単なるコールバックにすぎません。サンプル コードには、3 つのハンドラがあります。
print
ハンドラー (1) 。
handle_async_receive
ハンドラー (3) 。
print
ハンドラー (4) 。
同じprint()
関数が 2 回使用されたとしても、それぞれの使用は、独自の一意に識別可能なハンドラーを作成すると見なされます。ハンドラーには、上記のような基本的な関数からboost::bind()
、ラムダから生成されたファンクターなどのより複雑な構造に至るまで、さまざまな形状とサイズがあります。複雑さに関係なく、ハンドラーは依然としてコールバックにすぎません。
仕事とは?
作業は、Boost.Asio がアプリケーション コードに代わって行うように要求された処理です。Boost.Asio は、通知されるとすぐに一部の作業を開始する場合もあれば、後で作業を行うために待機する場合もあります。作業が完了すると、Boost.Asio は提供されたhandlerを呼び出してアプリケーションに通知します。
Boost.Asioは、現在、、、またはを呼び出しているスレッド内でのみハンドラーが実行されることを保証します。これらは、作業を行い、ハンドラを呼び出すスレッドです。したがって、上記の例では、 (1)にポストされたときに呼び出されません。代わりに、に追加され、後で呼び出されます。この場合は(5)以内です。run()
run_one()
poll()
poll_one()
print()
io_service
io_service
io_service.run()
非同期操作とは
非同期操作は作業を作成し、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)、 (5)handle_async_receive()
内でのみ呼び出されることが保証されますio_service.run()
。
これら 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 = // '. 1
boost::in_place(boost::ref(io_service)); // .'
boost::thread_group worker_threads; // -.
for(int x = 0; x < 2; ++x) // :
{ // '.
worker_threads.create_thread( // :- 2
boost::bind(&boost::asio::io_service::run, &io_service) // .'
); // :
} // -'
io_service.post(boost::bind(CalculateFib, 3)); // '.
io_service.post(boost::bind(CalculateFib, 4)); // :- 3
io_service.post(boost::bind(CalculateFib, 5)); // .'
work = boost::none; // 4
worker_threads.join_all(); // 5
}
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)); // :- 3
io_service.post(boost::bind(CalculateFib, 5)); // .'
boost::thread_group worker_threads; // -.
for(int x = 0; x < 2; ++x) // :
{ // '.
worker_threads.create_thread( // :- 2
boost::bind(&boost::asio::io_service::run, &io_service) // .'
); // :
} // -'
worker_threads.join_all(); // 5
}
同期と非同期
問題のコードは非同期操作を使用していますが、非同期操作が完了するのを待っているため、効果的に同期的に機能しています。
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ドキュメントにも記載されています。