web-dev-qa-db-fra.com

Confondu lorsque la méthode boost :: asio :: io_service est bloquée / débloquée

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

La fonction run() est bloquée jusqu'à ce que tout le travail soit terminé et qu'il n'y ait plus de gestionnaires à envoyer, ou jusqu'à ce que io_service Soit arrêté.

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

Une sortie normale de la fonction run() implique que l'objet io_service Est arrêté (la fonction stopped() renvoie la valeur true). Les appels suivants à run(), run_one(), poll() ou poll_one() seront immédiatement renvoyés, à moins d'un appel préalable à reset().

Que signifie la déclaration suivante?

[...] plus de gestionnaires à envoyer [...]


En essayant de comprendre le comportement de io_service::run(), je suis tombé sur ceci exemple (exemple 3a). À l'intérieur, 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();

Toutefois, dans le code suivant sur lequel je travaillais, le client se connecte à l'aide de 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() décrivant son comportement dans les deux exemples ci-dessous serait appréciée.

77
MistyD

Fondation

Commençons par un exemple simplifié et examinons les éléments Boost.Asio pertinents:

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 gestionnaire ?

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

  • Le gestionnaire print (1).
  • Le gestionnaire handle_async_receive (3).
  • Le gestionnaire print (4).

Même si la même fonction 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 prendre différentes formes et tailles, allant des fonctions de base telles que celles décrites ci-dessus à des constructions plus complexes telles que les foncteurs générés à partir de boost::bind() et de lambdas. Indépendamment de la complexité, le gestionnaire reste toujours rien de plus qu'un rappel.

Qu'est-ce que fonctionne ?

Le travail est un traitement que Boost.Asio a été invité à faire 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 que le travail soit effectué ultérieurement. Une fois le travail terminé, Boost.Asio informera l'application en appelant le gestionnaire fourni .

Boost.Asio garantit que les gestionnaires ne fonctionneront que dans un thread qui appelle actuellement run(), run_one(), poll() ou poll_one(). Ce sont les threads qui vont fonctionner et appeler des gestionnaires . Par conséquent, dans l'exemple ci-dessus, print() n'est pas appelé lorsqu'il est publié dans le io_service (1). Au lieu de cela, il est ajouté au io_service Et sera invoqué ultérieurement. Dans ce cas, il se trouve dans io_service.run() (5).

Que sont les opérations asynchrones?

Un opération asynchrone crée un travail et Boost.Asio appellera un gestionnaire pour informer l'application lorsque le travail est terminé. Les opérations asynchrones sont créées en appelant une fonction qui porte un nom avec le préfixe async_. Ces fonctions sont également appelées fonctions d'initiation .

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

  • Le lancement ou l’information du io_service Associé qui fonctionne doit être effectué. L'opération async_receive (3) informe le io_service Qu'il lui faudra lire les données de manière asynchrone à partir de la socket, puis async_receive Est renvoyé immédiatement.
  • Faire le travail réel. Dans ce cas, lorsque socket recevra des données, les octets seront lus et copiés dans buffer. Le travail réel sera effectué soit:
    • La fonction initiatrice (3), si Boost.Asio peut déterminer qu’elle ne bloquera pas.
    • Lorsque l'application exécute explicitement le io_service (5).
  • Invoquer le handle_async_receiveReadHandler . Encore une fois, les gestionnaires ne sont appelés que dans les threads exécutant le io_service. Ainsi, quel que soit le moment où le travail est terminé (3 ou 5), il est garanti que handle_async_receive() ne sera appelé que dans io_service.run() (5).

La séparation dans le temps et dans l’espace entre ces trois étapes est appelée inversion du flux de contrôle. C'est l'une des complexités qui rend difficile la programmation asynchrone. Cependant, certaines techniques peuvent aider à atténuer ce problème, par exemple en utilisant coroutines .

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

Lorsqu'un thread appelle io_service.run(), work et les gestionnaires seront appelés à partir de ce thread. Dans l'exemple ci-dessus, io_service.run() (5) bloquera jusqu'à ce que:

  • Il a appelé et renvoyé des deux gestionnaires print, l'opération de réception se termine avec succès ou échec et son gestionnaire handle_async_receive A été appelé et renvoyé.
  • Le io_service Est explicitement arrêté via io_service::stop() .
  • Une exception est levée depuis un gestionnaire.

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

 create io_service 
 create socket 
 ajoute un gestionnaire d'impression à io_service (1) 
 attend que le socket soit connecté (2) 
 ajoute une lecture asynchrone demande au service io_service (3) 
 ajouter un gestionnaire d'impression à io_service (4) 
 lancer le service io_service (5) 
 existe-t-il un travail ou des gestionnaires? 
 oui, il est 1 travail et 2 gestionnaires 
 socket a des données? non, ne fait rien 
 exécuter le gestionnaire d'impression (1) 
 existe-t-il un travail ou des gestionnaires? 
 oui, il y a 1 travail et 1 gestionnaire 
 le socket contient-il des données? non, ne fait rien 
 exécuter le gestionnaire d'impression (4) 
 existe-t-il un travail ou des gestionnaires? 
 oui, il y a 1 travail 
 le socket contient-il des données? non, continuez à attendre 
 - socket reçoit des données - 
 socket contient des données, lisez-les dans la mémoire tampon 
 ajoutez un gestionnaire handle_async_receive à io_service 
. Existe-t-il un travail ou des gestionnaires? 
 oui, il y a 1 gestionnaire 
 exécuter handle_async_receive (3) 
 existe-t-il un travail ou des gestionnaires? 
 non, définissez io_service sur arrêt et retour

Notez que lorsque la lecture s'est terminée, il a ajouté un autre gestionnaire au io_service. Ce détail subtil est une caractéristique importante de la programmation asynchrone. Cela permet aux gestionnaires d'être chaînés. Par exemple, si handle_async_receive N’a pas obtenu toutes les données attendues, son implémentation pourrait alors publier une autre opération de lecture asynchrone, ce qui aurait pour effet que io_service Aurait davantage de travail et ne reviendrait donc pas de io_service.run().

Notez que lorsque le io_service Est épuisé, l'application doit reset() le io_service Avant de l'exécuter à nouveau.


Exemple de question et exemple de code 3a

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

Code de question

socket->async_receive Ajoute du travail au io_service. Ainsi, io_service->run() sera bloqué jusqu'à ce que l'opération de lecture se termine avec succès ou une erreur, et ClientReceiveEvent aura été exécuté ou lève une exception.

Exemple 3a Code

Dans l’espoir de faciliter la compréhension, voici un exemple plus petit annoté 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
}

A un niveau élevé, le programme créera 2 threads qui traiteront la boucle d'événement de io_service (2). Il en résulte un pool de threads simple qui calculera les nombres de Fibonacci (3).

La principale différence entre le code de question et ce code est que ce code appelle io_service::run() (2) avant le travail réel et les gestionnaires sont ajouté au io_service (3). Pour empêcher la io_service::run() de revenir immédiatement, un objet io_service::work est créé (1). Cet objet empêche le io_service De manquer de travail; Par conséquent, io_service::run() ne reviendra pas car aucun travail n'est effectué.

Le flux global est comme suit:

  1. Créez et ajoutez l'objet io_service::work Ajouté au io_service.
  2. Pool de threads créé qui appelle io_service::run(). Ces threads de travail ne renverront pas à partir de io_service À cause de l'objet io_service::work.
  3. Ajoutez 3 gestionnaires calculant le nombre de Fibonacci au io_service Et revenez immédiatement. Les threads de travail, et non le thread principal, peuvent commencer à exécuter ces gestionnaires immédiatement.
  4. Supprimez l'objet io_service::work.
  5. Attendez que les threads de travail soient terminés. Cela ne se produira que lorsque les 3 gestionnaires auront terminé l'exécution, car io_service N'a ni gestionnaires ni travail.

Le code pourrait être écrit différemment, de la même manière que le code d'origine, où les gestionnaires sont ajoutés au io_service, Puis la boucle d'événement io_service Est traitée. Cela supprime la nécessité d'utiliser io_service::work Et donne le 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 vs asynchrone

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

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, évitez de mélanger des opérations synchrones et asynchrones. Souvent, cela peut transformer un système complexe en un système compliqué. Ceci réponse met en évidence les avantages de la programmation asynchrone, dont certains sont également décrits dans le Boost.Asio documentation .

209
Tanner Sansbury

Pour simplifier la tâche de run, considérez-le comme un employé qui doit traiter une pile de papier; il prend une feuille, fait ce que la feuille dit, jette la feuille et prend la suivante; quand il est à court de draps, il quitte le bureau. Sur chaque feuille, il peut y avoir n'importe quel type d’instruction, même en ajoutant une nouvelle feuille à la pile. Retour à asio: vous pouvez donner à un io_service fonctionne de deux manières, essentiellement: en utilisant post comme dans l'exemple que vous avez lié, ou en utilisant d'autres objets qui appellent en interne post sur le io_service, comme le socket et ses async_* méthodes.

16
Loghorn