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.
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 :
- Créez et ajoutez le
io_service::work
ajouté à l'objet io_service
.
- 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.
- 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.
- Supprimer le
io_service::work
objet.
- 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 .