boost :: asio :: io_service実行メソッドのブロック/ブロック解除時に混乱する


88

Boost.Asioの完全な初心者であるため、私はと混同していio_service::run()ます。このメソッドがブロック/ブロック解除するときに誰かが私に説明してくれれば幸いです。ドキュメントには次のように記載されています。

このrun()関数は、すべての作業が終了してディスパッチするハンドラーがなくなるまで、またはio_serviceが停止するまでブロックします。

複数のスレッドがrun()関数を呼び出して、io_serviceハンドラーを実行できるスレッドのプールを設定できます。プールで待機しているすべてのスレッドは同等であり、io_serviceハンドラーを呼び出すためにそれらのいずれかを選択できます。

run()関数の通常の終了は、io_serviceオブジェクトが停止していることを意味します(stopped()関数はtrueを返します)。後続の呼び出しはするrun()run_one()poll()またはpoll_one()前のコールがない限り、すぐに戻りますreset()

次のステートメントはどういう意味ですか?

[...]ディスパッチされるハンドラーはもうありません[...]


の動作を理解しようとしているときにio_service::run()、この(例3a)に出くわしました。その中で、私はそれio_service->run()が作業指示をブロックして待つのを観察します。

// WorkerThread invines io_service->run()
void WorkerThread(boost::shared_ptr<boost::asio::io_service> io_service);
void CalculateFib(size_t);

boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service::work> work(
   new boost::asio::io_service::work(*io_service));

// ...

boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
  worker_threads.create_thread(boost::bind(&WorkerThread, io_service));
}

io_service->post( boost::bind(CalculateFib, 3));
io_service->post( boost::bind(CalculateFib, 4));
io_service->post( boost::bind(CalculateFib, 5));

work.reset();
worker_threads.join_all();

ただし、私が取り組んでいた次のコードでは、クライアントはTCP / IPを使用して接続し、runメソッドはデータが非同期に受信されるまでブロックします。

typedef boost::asio::ip::tcp tcp;
boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<tcp::socket> socket(new tcp::socket(*io_service));

// Connect to 127.0.0.1:9100.
tcp::resolver resolver(*io_service);
tcp::resolver::query query("127.0.0.1", 
                           boost::lexical_cast< std::string >(9100));
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
socket->connect(endpoint_iterator->endpoint());

// Just blocks here until a message is received.
socket->async_receive(boost::asio::buffer(buf_client, 3000), 0,
                      ClientReceiveEvent);
io_service->run();

// Write response.
boost::system::error_code ignored_error;
std::cout << "Sending message \n";
boost::asio::write(*socket, boost::asio::buffer("some data"), ignored_error);

run()以下の2つの例でその動作を説明している説明をいただければ幸いです。

回答:


234

財団

簡単な例から始めて、関連する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は提供されたハンドラーを呼び出してアプリケーションに通知します。

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コード

理解しやすくするために、注釈付きの小さな例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_serviceio_service::run()io_service::workio_serviceio_service::run()

全体的なフローは次のとおりです。

  1. に追加されたio_service::workオブジェクトを作成して追加しますio_service
  2. を呼び出すスレッドプールが作成されましたio_service::run()。これらのワーカースレッドはio_serviceio_service::workオブジェクトのためにから戻りません。
  3. フィボナッチ数を計算する3つのハンドラーをに追加し、io_serviceすぐに戻ります。メインスレッドではなく、ワーカースレッドがこれらのハンドラーの実行をすぐに開始する場合があります。
  4. io_service::workオブジェクトを削除します。
  5. ワーカースレッドの実行が終了するのを待ちます。これは、3つのハンドラーすべてが実行を終了した後にのみ発生します。これは、io_serviceどちらにもハンドラーも動作もないためです。

コードは、元のコードと同じ方法で別の方法で記述できます。この場合、ハンドラーがに追加されio_serviceio_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
}

同期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のドキュメントでも説明されています


13
素晴らしい投稿。十分な注意が払われていないと感じたので、1つだけ追加したいと思います。run()が戻った後、再度run()を実行する前に、io_serviceでreset()を呼び出す必要があります。そうしないと、async_操作が待機しているかどうかに関係なく、すぐに戻る可能性があります。
DeVadder 2013年

バッファはどこから来るのですか?それは何ですか?
ruipacheco 2015

私はまだ混乱しています。ミキシングが同期で非同期が推奨されない場合、純粋な非同期モードとは何ですか?io_service.run();なしのコードを示す例を挙げていただけますか?
スプラッシュ

@Splash Oneを使用するio_service.poll()と、未処理の操作をブロックせずにイベントループを処理できます。同期操作と非同期操作の混合を回避するための主な推奨事項は、不必要な複雑さの追加を回避し、ハンドラーの完了に長い時間がかかる場合の応答性の低下を防ぐことです。同期操作がブロックされないことがわかっている場合など、安全な場合があります。
タナーサンズベリー2015年

「Boost.Asioは、ハンドラーが現在呼び出しているスレッド内でrun()のみ実行されることを保証します...」の「現在」とはどういう意味ですか?N個のスレッド(と呼ばれているrun())がある場合、どれが「現在の」スレッドですか?たくさんありますか?または、async_*()(たとえばasync_read)の実行を終了したスレッドが、そのハンドラーも呼び出すことが保証されているということですか?
ナワズ2016

18

何をするかを単純化するためにrun、それを紙の山を処理しなければならない従業員と考えてください。それは1枚のシートを取り、そのシートが言うことを行い、そのシートを捨てて次のシートを取ります。彼がシーツを使い果たすと、それはオフィスを去ります。各シートには、新しいシートを山に追加する場合でも、あらゆる種類の指示があります。戻るASIOへ:あなたが与えることができio_service、本質的に、2つの方法での作業:使用してpostリンクされたサンプルのように、その上、または内部呼び出して他のオブジェクト使用してpost上のio_serviceように、socketそしてそのasync_*方法。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.