Fundación
Comencemos con un ejemplo simplificado y examinemos las piezas relevantes 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);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
¿Qué es un manejador ?
Un controlador no es más que una devolución de llamada. En el código de ejemplo, hay 3 controladores:
- El
print
manejador (1).
- El
handle_async_receive
manejador (3).
- El
print
manejador (4).
Aunque la misma print()
función se utiliza dos veces, se considera que cada uso crea su propio controlador identificable de forma única. Los controladores pueden tener muchas formas y tamaños, que van desde funciones básicas como las anteriores hasta construcciones más complejas como functores generados desde boost::bind()
y lambdas. Independientemente de la complejidad, el controlador sigue siendo nada más que una devolución de llamada.
¿Qué es el trabajo ?
El trabajo es un procesamiento que se ha solicitado a Boost.Asio en nombre del código de la aplicación. A veces, Boost.Asio puede comenzar parte del trabajo tan pronto como se le haya informado, y otras veces puede esperar para hacer el trabajo en un momento posterior. Una vez finalizado el trabajo, Boost.Asio informará a la aplicación invocando el controlador suministrado .
Boost.Asio garantiza que los controladores pueden funcionar sólo dentro de un hilo que está llamando actualmente run()
, run_one()
, poll()
, o poll_one()
. Estos son los hilos que funcionarán y llamarán a los controladores . Por lo tanto, en el ejemplo anterior, print()
no se invoca cuando se publica en io_service
(1). En su lugar, se agrega a io_service
y se invocará en un momento posterior. En este caso, está dentro de io_service.run()
(5).
¿Qué son las operaciones asincrónicas?
Una operación asincrónica crea trabajo y Boost.Asio invocará un controlador para informar a la aplicación cuando el trabajo se haya completado. Las operaciones asincrónicas se crean llamando a una función que tiene un nombre con el prefijo async_
. Estas funciones también se conocen como funciones de inicio .
Las operaciones asincrónicas se pueden descomponer en tres pasos únicos:
- Iniciar, o informar, el asociado
io_service
que trabaja debe hacerse. La async_receive
operación (3) informa al io_service
que necesitará leer datos de forma asincrónica desde el socket, luego async_receive
regresa inmediatamente.
- Haciendo el trabajo real. En este caso, cuando
socket
reciba datos, se leerán y copiarán bytes buffer
. El trabajo real se realizará en:
- La función de inicio (3), si Boost.Asio puede determinar que no bloqueará.
- Cuando la aplicación ejecuta explícitamente
io_service
(5).
- Invocando el
handle_async_receive
ReadHandler . Una vez más, los controladores solo se invocan dentro de los subprocesos que ejecutan io_service
. Así, independientemente de cuándo se realice el trabajo (3 o 5), se garantiza que handle_async_receive()
solo se invocará dentro de io_service.run()
(5).
La separación en el tiempo y el espacio entre estos tres pasos se conoce como inversión de flujo de control. Es una de las complejidades que dificulta la programación asincrónica. Sin embargo, existen técnicas que pueden ayudar a mitigar esto, como el uso de corrutinas .
¿Qué hace io_service.run()
?
Cuando un hilo llama io_service.run()
, el trabajo y los controladores se invocarán desde dentro de este hilo. En el ejemplo anterior, io_service.run()
(5) se bloqueará hasta que:
- Ha invocado y regresado de ambos
print
controladores, la operación de recepción se completa con éxito o falla, y su handle_async_receive
controlador ha sido invocado y devuelto.
- El
io_service
se detiene explícitamente a través de io_service::stop()
.
- Se lanza una excepción desde dentro de un controlador.
Un posible flujo pseudo-ish podría describirse como el siguiente:
crear io_service
crear socket
agregar controlador de impresión a io_service (1)
espere a que el enchufe se conecte (2)
agregue una solicitud de trabajo de lectura asincrónica a io_service (3)
agregar controlador de impresión a io_service (4)
ejecutar el io_service (5)
hay trabajo o manipuladores?
sí, hay 1 trabajo y 2 manipuladores
¿Socket tiene datos? no, no hagas nada
ejecutar controlador de impresión (1)
hay trabajo o manipuladores?
sí, hay 1 trabajo y 1 manejador
¿Socket tiene datos? no, no hagas nada
ejecutar controlador de impresión (4)
hay trabajo o manipuladores?
si, hay 1 trabajo
¿Socket tiene datos? no sigue esperando
- socket recibe datos -
socket tiene datos, léalo en el búfer
agregue el controlador handle_async_receive a io_service
hay trabajo o manipuladores?
sí, hay 1 controlador
ejecutar handle_async_receive handler (3)
hay trabajo o manipuladores?
no, establezca io_service como detenido y vuelva
Observe cómo cuando terminó la lectura, agregó otro controlador al io_service
. Este sutil detalle es una característica importante de la programación asincrónica. Permite encadenar a los manipuladores . Por ejemplo, si handle_async_receive
no obtuvo todos los datos que esperaba, entonces su implementación podría publicar otra operación de lectura asincrónica, lo que resultaría en io_service
tener más trabajo y, por lo tanto, no regresar io_service.run()
.
Ten en cuenta que cuando la io_service
cuenta se quedó sin trabajo, la aplicación debe reset()
al io_service
antes de ejecutar de nuevo.
Pregunta de ejemplo y código de ejemplo 3a
Ahora, examinemos las dos piezas de código a las que se hace referencia en la pregunta.
Código de pregunta
socket->async_receive
agrega trabajo al io_service
. Por lo tanto, io_service->run()
se bloqueará hasta que la operación de lectura se complete con éxito o error, y ClientReceiveEvent
haya terminado de ejecutarse o arroje una excepción.
Con la esperanza de que sea más fácil de entender, aquí hay un Ejemplo 3a anotado más pequeño:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
En un nivel alto, el programa creará 2 subprocesos que procesarán el io_service
bucle de eventos de (2). Esto da como resultado un grupo de subprocesos simple que calculará los números de Fibonacci (3).
La única diferencia principal entre el Código de preguntas y este código es que este código invoca io_service::run()
(2) antes de que se agreguen el trabajo real y los controladores a io_service
(3). Para evitar que io_service::run()
vuelva inmediatamente, io_service::work
se crea un objeto (1). Este objeto evita que se io_service
quede sin trabajo; por lo tanto, io_service::run()
no regresará por no haber trabajado.
El flujo general es el siguiente:
- Cree y agregue el
io_service::work
objeto agregado al io_service
.
- Grupo de subprocesos creado que invoca
io_service::run()
. Estos subprocesos de trabajo no volverán io_service
debido al io_service::work
objeto.
- Agregue 3 controladores que calculan números de Fibonacci al
io_service
, y regrese inmediatamente. Los subprocesos de trabajo, no el subproceso principal, pueden comenzar a ejecutar estos controladores de inmediato.
- Elimina el
io_service::work
objeto.
- Espere a que terminen de ejecutarse los subprocesos de trabajo. Esto solo ocurrirá una vez que los 3 controladores hayan finalizado la ejecución, ya que
io_service
ni tienen controladores ni trabajo.
El código podría escribirse de manera diferente, de la misma manera que el Código original, donde se agregan controladores al io_service
, y luego io_service
se procesa el bucle de eventos. Esto elimina la necesidad de usar io_service::work
y da como resultado el siguiente código:
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
Sincrónico frente a asincrónico
Aunque el código de la pregunta utiliza una operación asincrónica, está funcionando efectivamente de forma sincrónica, ya que está esperando a que se complete la operación asincrónica:
socket.async_receive(buffer, handler)
io_service.run();
es equivalente a:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
Como regla general, trate de evitar mezclar operaciones sincrónicas y asincrónicas. A menudo, puede convertir un sistema complejo en un sistema complicado. Esta respuesta destaca las ventajas de la programación asincrónica, algunas de las cuales también se tratan en la documentación de Boost.Asio .