100 votes

Confusion lors du blocage/déblocage de la méthode d'exécution de boost::asio::io_service

Étant un débutant total à Boost.Asio, je suis confus avec io_service::run() . J'apprécierais que quelqu'un puisse m'expliquer quand cette méthode se bloque/débloque. La documentation indique :

En run() bloque jusqu'à ce que tout le travail soit terminé et qu'il n'y ait plus de gestionnaires à distribuer, ou jusqu'à ce que la fonction io_service a été arrêté.

Plusieurs threads peuvent appeler le run() pour mettre en place un pool de threads à partir desquels la fonction io_service peut exécuter des gestionnaires. Tous les threads qui sont en attente dans le pool sont équivalents et la fonction io_service peut choisir n'importe lequel d'entre eux pour invoquer un gestionnaire.

Une sortie normale de la run() implique que la fonction io_service est arrêté (l'objet stopped() retourne vrai). Les appels ultérieurs à run() , run_one() , poll() o poll_one() reviendra immédiatement, sauf s'il y a un appel préalable à reset() .

Que signifie l'affirmation suivante ?

[...] plus aucun gestionnaire à distribuer [...].


En essayant de comprendre le comportement de io_service::run() je suis tombé sur ceci exemple (exemple 3a). A l'intérieur de celui-ci, j'observe que io_service->run() bloque et attend les ordres de travail.

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

Cependant, dans le code suivant sur lequel je travaillais, le client se connecte en utilisant TCP/IP et la méthode d'exécution se bloque jusqu'à la réception asynchrone des données.

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

Toute explication de run() qui décrit son comportement dans les deux exemples ci-dessous serait appréciée.

263voto

Tanner Sansbury Points 17166

Fondation

Commençons par un exemple simplifié et examinons les pièces pertinentes de 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

Qu'est-ce qu'un Manipulateur ?

A manipulateur n'est rien d'autre qu'un rappel. Dans le code de l'exemple, il y a 3 gestionnaires :

  • En print manipulateur (1).
  • En handle_async_receive manipulateur (3).
  • En print manipulateur (4).

Même si le même print() est utilisée deux fois, chaque utilisation est considérée comme créant son propre gestionnaire identifiable de manière unique. Les gestionnaires peuvent avoir de nombreuses formes et tailles, allant des fonctions de base comme celles ci-dessus à des constructions plus complexes comme les foncteurs générés à partir de boost::bind() et lambdas. Quelle que soit la complexité, le gestionnaire ne reste rien de plus qu'un callback.

Qu'est-ce que Travail ?

Le travail est un traitement que Boost.Asio a été chargé d'effectuer pour le compte du code de l'application. Parfois, Boost.Asio peut commencer une partie du travail dès qu'il en a été informé, et d'autres fois, il peut attendre pour effectuer le travail à un moment ultérieur. Une fois qu'il a terminé le travail, Boost.Asio en informe l'application en invoquant la fonction fournie manipulateur .

Boost.Asio garantit que manipulateurs ne sera exécuté qu'au sein d'un thread qui est en train d'appeler run() , run_one() , poll() ou poll_one() . Ce sont les fils qui vont faire le travail et appeler manipulateurs . Par conséquent, dans l'exemple ci-dessus, print() n'est pas invoquée lorsqu'elle est postée dans la section io_service (1). Au lieu de cela, il est ajouté à la io_service et sera invoquée à un moment ultérieur. Dans ce cas, il s'agit de io_service.run() (5).

Que sont les opérations asynchrones ?

Un site fonctionnement asynchrone crée du travail et Boost.Asio invoquera une manipulateur pour informer l'application de la fin des travaux. Les opérations asynchrones sont créées en appelant une fonction dont le nom porte le préfixe async_ . Ces fonctions sont également connues sous le nom de fonctions de déclenchement .

Les opérations asynchrones peuvent être décomposées en trois étapes uniques :

  • Initier, ou informer, l'associé io_service que le travail doit être fait. Le site async_receive l'opération (3) informe le io_service qu'il devra lire des données de manière asynchrone depuis le socket, alors async_receive revient immédiatement.
  • Faire le travail réel. Dans ce cas, lorsque socket reçoit des données, des octets seront lus et copiés dans buffer . Le travail effectif sera effectué soit dans :
    • La fonction de déclenchement (3), si Boost.Asio peut déterminer qu'elle ne bloquera pas.
    • Lorsque l'application exécute explicitement le io_service (5).
  • En invoquant le handle_async_receive ReadHandler . Encore une fois, manipulateurs ne sont invoqués qu'à l'intérieur des threads qui exécutent le io_service . Ainsi, quel que soit le moment où le travail est effectué (3 ou 5), il est garanti que handle_async_receive() ne sera invoqué que dans le cadre de io_service.run() (5).

La séparation dans le temps et l'espace entre ces trois étapes est connue sous le nom d'inversion du flux de contrôle. C'est l'une des complexités qui rendent la programmation asynchrone difficile. Cependant, il existe des techniques qui permettent de l'atténuer, comme l'utilisation de l'option coroutines .

Qu'est-ce que io_service.run() Faire ?

Lorsqu'un thread appelle io_service.run() le travail et manipulateurs sera invoqué à partir de ce fil. Dans l'exemple ci-dessus, io_service.run() (5) bloquera jusqu'à ce que soit :

  • Il a invoqué et renvoyé les deux print l'opération de réception s'achève avec succès ou non, et l'opération de réception se termine. handle_async_receive a été invoqué et renvoyé.
  • En io_service est explicitement arrêté via io_service::stop() .
  • Une exception est levée à l'intérieur d'un gestionnaire.

Un flux psuedo-ish potentiel pourrait être décrit comme suit :

create io\_service
create socket
add print handler to io\_service (1)
wait for socket to connect (2)
add an asynchronous read work request to the io\_service (3)
add print handler to io\_service (4)
run the io\_service (5)
  is there work or handlers?
    yes, there is 1 work and 2 handlers
      does socket have data? no, do nothing
      run print handler (1)
  is there work or handlers?
    yes, there is 1 work and 1 handler
      does socket have data? no, do nothing
      run print handler (4)
  is there work or handlers?
    yes, there is 1 work
      does socket have data? no, continue waiting
  -- socket receives data --
      socket has data, read it into buffer
      add handle\_async\_receive handler to io\_service
  is there work or handlers?
    yes, there is 1 handler
      run handle\_async\_receive handler (3)
  is there work or handlers?
    no, set io\_service as stopped and return

Remarquez comment quand la lecture s'est terminée, elle a ajouté un autre manipulateur au io_service . Ce détail subtil est une caractéristique importante de la programmation asynchrone. Il permet manipulateurs pour être enchaînés ensemble. Par exemple, si handle_async_receive n'a pas obtenu toutes les données qu'il attendait, sa mise en œuvre pourrait alors poster une autre opération de lecture asynchrone, ce qui aurait pour effet de io_service avoir plus de travail, et donc ne pas revenir de io_service.run() .

Notez que lorsque le io_service s'est retrouvé au chômage, la demande doit reset() le site io_service avant de l'exécuter à nouveau.


Exemple de code Question et Exemple 3a

Maintenant, examinons les deux morceaux de code référencés dans la question.

Code des questions

socket->async_receive ajoute du travail à la io_service . Ainsi, io_service->run() bloquera jusqu'à ce que l'opération de lecture se termine avec succès ou par erreur, et ClientReceiveEvent a terminé son exécution ou lève une exception.

Exemple 3a Code

Dans l'espoir de faciliter la compréhension, voici un exemple 3a annoté plus petit :

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
}

À un haut niveau, le programme créera 2 threads qui traiteront les données suivantes io_service La boucle de l'événement (2). Il en résulte un simple pool de threads qui calculera les nombres de Fibonacci (3).

La seule différence majeure entre le code de la question et ce code est que ce dernier invoque io_service::run() (2) avant le travail réel et les manipulateurs sont ajoutés à la io_service (3). Pour éviter que le io_service::run() de revenir immédiatement, un io_service::work est créé (1). Cet objet empêche le io_service de se retrouver au chômage, donc, io_service::run() ne reviendront pas en raison de l'absence de travail.

Le flux global est le suivant :

  1. Créez et ajoutez le io_service::work ajouté à l'objet io_service .
  2. Création d'un pool de threads qui invoque io_service::run() . Ces fils de travail ne reviendront pas de io_service en raison de la io_service::work objet.
  3. Ajoutez 3 gestionnaires qui calculent les nombres de Fibonacci à l'outil de gestion de l'information. io_service et revenir immédiatement. Les threads des travailleurs, et non le thread principal, peuvent commencer à exécuter ces gestionnaires immédiatement.
  4. Supprimer le io_service::work objet.
  5. Attendre la fin de l'exécution des threads de travail. Cela ne se produira qu'une fois que les 3 gestionnaires auront fini de s'exécuter, car la fonction io_service n'a ni maître ni travail.

Le code pourrait être écrit différemment, de la même manière que le code original, où des gestionnaires sont ajoutés à l'option io_service et ensuite le io_service la boucle d'événement est traitée. Cela supprime la nécessité d'utiliser io_service::work et donne lieu au code suivant :

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
}

Synchrone ou asynchrone ?

Bien que le code de la question utilise une opération asynchrone, il fonctionne effectivement de manière synchrone, car il attend que l'opération asynchrone soit terminée :

socket.async_receive(buffer, handler)
io_service.run();

est équivalent à :

boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);

En règle générale, essayez d'éviter de mélanger les opérations synchrones et asynchrones. Souvent, cela peut transformer un système complexe en un système compliqué. Voici réponse met en évidence les avantages de la programmation asynchrone, dont certains sont également abordés dans le document Boost.Asio documentation .

21voto

Loghorn Points 1340

Pour simplifier comment ce run pense à un employé qui doit traiter une pile de papier ; il prend une feuille, fait ce que la feuille lui dit, jette la feuille et prend la suivante ; quand il n'a plus de feuilles, il quitte le bureau. Sur chaque feuille, il peut y avoir n'importe quel type d'instruction, même ajouter une nouvelle feuille à la pile. Retour à l'asio : on peut donner à un io_service fonctionnent essentiellement de deux manières : en utilisant post sur lui comme dans l'exemple que vous avez lié, ou en utilisant d'autres objets qui appellent intérieurement post sur le io_service comme le socket et son async_* méthodes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X