web-dev-qa-db-ja.com

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

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);

以下の2つの例でその動作を説明するrun()の説明は歓迎します。

77
MistyD

基礎

簡単な例から始めて、関連する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

AHandlerとは?

handlerはコールバックにすぎません。サンプルコードには、3つのハンドラーがあります。

  • printハンドラー(1)。
  • handle_async_receiveハンドラー(3)。
  • printハンドラー(4)。

同じprint()関数が2回使用されますが、各使用は独自の一意に識別可能なハンドラーを作成すると見なされます。ハンドラーは、上記のような基本的な関数から、boost::bind()やlambdasから生成されるファンクターなどのより複雑な構成体に至るまで、さまざまな形とサイズで提供されます。複雑さにかかわらず、ハンドラーはコールバック以外の何物でもありません。

Workとは何ですか?

作業とは、アプリケーションコードの代わりにBoost.Asioに要求された処理のことです。 Boost.Asioは、通知された直後に一部の作業を開始する場合もあれば、後の時点で作業を待機する場合もあります。作業が完了すると、Boost.Asioは提供されたhandlerを呼び出すことでアプリケーションに通知します。

Boost.Asioは、handlersが現在run()run_one()poll()、またはpoll_one()を呼び出しているスレッド内でのみ実行されることを保証します。これらは動作し、handlersを呼び出すスレッドです。したがって、上記の例では、io_service(1)にポストされるときにprint()は呼び出されません。代わりに、io_serviceに追加され、後で呼び出されます。この場合、io_service.run()(5)内にあります。

非同期操作とは

非同期操作 は作業を作成し、Boost.Asioはhandlerを呼び出して、作業が完了したときにアプリケーションに通知します。非同期操作は、名前に接頭辞async_が付いた関数を呼び出すことにより作成されます。これらの関数は、開始関数とも呼ばれます。

非同期操作は、次の3つの固有のステップに分解できます。

  • 関連するio_serviceを開始、または通知する必要があります。 async_receive操作(3)は、ソケットからデータを非同期で読み取る必要があることをio_serviceに通知し、async_receiveはすぐに戻ります。
  • 実際の仕事をする。この場合、socketがデータを受信すると、バイトが読み取られ、bufferにコピーされます。実際の作業は次のいずれかで行われます。
    • Boost.Asioがブロックしないと判断できる場合、開始関数(3)。
    • アプリケーションがio_service(5)を明示的に実行するとき。
  • handle_async_receiveの呼び出し ReadHandler 。繰り返しますが、handlersio_serviceを実行しているスレッド内でのみ呼び出されます。したがって、作業がいつ完了するか(3または5)に関係なく、handle_async_receive()io_service.run()(5)内でのみ呼び出されることが保証されます。

これら3つのステップ間の時間と空間の分離は、制御フロー反転として知られています。非同期プログラミングを困難にする複雑さの1つです。ただし、 coroutines を使用するなど、これを軽減するのに役立つテクニックがあります。

io_service.run()は何をしますか?

スレッドがio_service.run()を呼び出すと、workとhandlersがこのスレッド内から呼び出されます。上記の例では、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つのハンドラー
はソケットにデータがありますか?いいえ、何もしません
 run print handler(1)
作業またはハンドラーがありますか?
はい、1つの作業があり、1つのハンドラーがあります。いいえ、何もしません
 printハンドラーを実行します(4)
作業またはハンドラーはありますか?
はい、1つの作業があります
ソケットにはデータがありますか?いいえ、待機を続けます
-ソケットはデータを受信します-
ソケットにはデータがあり、それをバッファに読み込みます
 handle_async_receiveハンドラーをio_service 
に追加しますか? 
はい、1つのハンドラーがあります
 run handle_async_receive handler(3)
作業またはハンドラーがありますか?
いいえ、io_serviceを停止として設定して戻ります

読み取りが終了すると、io_serviceに別のhandlerが追加されることに注意してください。この微妙な詳細は、非同期プログラミングの重要な機能です。 handlersを連鎖させることができます。たとえば、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つは、このコードがio_service::run()(2)beforeを呼び出し、実際の作業とハンドラーがio_service(3)に追加されることです。 io_service::run()がすぐに返されないように、 io_service::work オブジェクトが作成されます(1)。このオブジェクトは、io_serviceの作業不足を防ぎます。したがって、io_service::run()は、作業の結果として返されません。

全体的な流れは次のとおりです。

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

コードは、ハンドラーが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);

一般的な経験則として、同期操作と非同期操作を混在させないようにしてください。多くの場合、複雑なシステムを複雑なシステムに変えることができます。これは、非同期プログラミングの利点を強調する answer であり、その一部はBoost.Asioでもカバーされています documentation

209
Tanner Sansbury

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

16
Loghorn