É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 queio_service
Soit arrêté.Plusieurs threads peuvent appeler la fonction
run()
pour configurer un pool de threads à partir duquel leio_service
Peut exécuter des gestionnaires. Tous les threads en attente dans le pool sont équivalents et leio_service
Peut choisir n'importe lequel d'entre eux pour appeler un gestionnaire.Une sortie normale de la fonction
run()
implique que l'objetio_service
Est arrêté (la fonctionstopped()
renvoie la valeur true). Les appels suivants àrun()
,run_one()
,poll()
oupoll_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.
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
Un gestionnaire n'est rien d'autre qu'un rappel. Dans l'exemple de code, il y a 3 gestionnaires:
print
(1).handle_async_receive
(3).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.
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).
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:
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.socket
recevra des données, les octets seront lus et copiés dans buffer
. Le travail réel sera effectué soit: io_service
(5).handle_async_receive
ReadHandler . 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 .
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:
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é.io_service
Est explicitement arrêté via io_service::stop()
.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.
Maintenant, examinons les deux morceaux de code référencés dans la 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.
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:
io_service::work
Ajouté au io_service
.io_service::run()
. Ces threads de travail ne renverront pas à partir de io_service
À cause de l'objet io_service::work
.io_service
Et revenez immédiatement. Les threads de travail, et non le thread principal, peuvent commencer à exécuter ces gestionnaires immédiatement.io_service::work
.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
}
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 .
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.