c++ - ver - me bloquearon en whatsapp




Se confunde cuando boost:: asio:: io_service ejecuta el método bloquea/desbloquea (2)

Siendo un principiante total para Boost.Asio, estoy confundido con io_service::run() . Agradecería que alguien pudiera explicarme cuándo este método bloquea / desbloquea. La documentación dice:

La función run() bloquea hasta que todo el trabajo haya finalizado y no haya más controladores para enviar, o hasta que se haya detenido el io_service .

Varios hilos pueden llamar a la función run() para configurar un conjunto de hilos desde el cual el io_service puede ejecutar manejadores. Todos los subprocesos que están esperando en el grupo son equivalentes y el io_service puede elegir cualquiera de ellos para invocar un controlador.

Una salida normal de la función run() implica que el objeto io_service se detiene (la función stopped() devuelve true). Las llamadas posteriores a run() , run_one() , poll() o poll_one() volverán inmediatamente a menos que haya una llamada previa para reset() .

¿Qué significa la siguiente declaración?

[...] no más manejadores que se despacharán [...]

Al tratar de comprender el comportamiento de io_service::run() , encontré este example (ejemplo 3a). Dentro de él, observo que io_service->run() bloquea y espera órdenes de trabajo.

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

Sin embargo, en el siguiente código en el que estaba trabajando, el cliente se conecta utilizando TCP / IP y los bloques del método de ejecución hasta que los datos se reciben de forma asincrónica.

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

Cualquier explicación de run() que describa su comportamiento en los dos ejemplos a continuación sería apreciada.


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);                             // 1
socket.connect(endpoint);                            // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print);                             // 4
io_service.run();                                    // 5

¿Qué es un controlador ?

Un controlador no es más que una devolución de llamada. En el código de ejemplo, hay 3 manejadores:

  • El controlador de print (1).
  • El manejador handle_async_receive (3).
  • El controlador de print (4).

Aunque la misma función de print() se usa dos veces, se considera que cada uso crea su propio manejador identificable de manera única. Los manipuladores pueden venir en muchas formas y tamaños, que van desde funciones básicas como las anteriores a construcciones más complejas como funtores generados a partir de 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 le solicitó a Boost.Asio en nombre del código de la aplicación. En ocasiones, Boost.Asio puede iniciar parte del trabajo tan pronto como se lo indiquen, y otras veces puede esperar para realizar el trabajo en un momento posterior. Una vez que haya terminado el trabajo, Boost.Asio informará a la aplicación invocando el controlador suministrado.

Boost.Asio garantiza que los manipuladores solo se ejecutarán dentro de un hilo que actualmente está llamando a run() , run_one() , poll() o poll_one() . Estos son los hilos que funcionarán y llamarán a los manejadores . Por lo tanto, en el ejemplo anterior, print() no se invoca cuando se publica en el io_service (1). En cambio, se agrega al io_service y se invocará en un momento posterior. En este caso, dentro de io_service.run() (5).

¿Qué son las operaciones asincrónicas?

Una operación asíncrona 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:

  • Se debe iniciar o informar al servicio io_service asociado que funciona. La operación async_receive (3) informa al io_service que necesitará leer datos de forma asíncrona desde el socket, luego async_receive regresa inmediatamente.
  • Haciendo el trabajo real. En este caso, cuando el socket recibe datos, los bytes serán leídos y copiados en el 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 el io_service (5).
  • Invocando el handle_async_receive ReadHandler . Una vez más, los manejadores solo se invocan dentro de los hilos que ejecutan el io_service . Por lo tanto, independientemente de cuándo se realiza 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 coroutines .

¿Qué hace io_service.run() ?

Cuando un hilo llama a io_service.run() , el trabajo y los manejadores se invocarán desde dentro de este hilo. En el ejemplo anterior, io_service.run() (5) bloqueará hasta que:

  • Ha invocado y devuelto desde ambos controladores de print , la operación de recepción se completa con éxito o error, y se ha invocado y devuelto su controlador handle_async_receive .
  • 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 flujo de psuedo-ish potencial podría describirse de la siguiente manera:

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

Observe cómo cuando finalizó la lectura, agregó otro controlador al io_service . Este detalle sutil es una característica importante de la programación asincrónica. Permite que los controladores estén encadenados juntos. Por ejemplo, si handle_async_receive no obtuvo todos los datos que esperaba, su implementación podría publicar otra operación de lectura asíncrona, lo que io_service resultado que io_service tuviera más trabajo y, por lo tanto, no volviera de io_service.run() .

Tenga en cuenta que cuando io_service ha quedado sin trabajo, la aplicación debe reset() el io_service antes de io_service ejecutarlo.

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 finalice con éxito o error, y ClientReceiveEvent habrá terminado de ejecutarse o ClientReceiveEvent una excepción.

example Código

Con la esperanza de que sea más fácil de entender, aquí hay un ejemplo anotado 3a 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 =       // '. 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
}

En un nivel alto, el programa creará 2 subprocesos que procesarán el bucle de eventos de io_service (2). Esto da como resultado un grupo de subprocesos simple que calculará los números de Fibonacci (3).

La principal diferencia entre el código de la pregunta y este código es que este código invoca io_service::run() (2) antes del trabajo real y los controladores se agregan al io_service (3). Para evitar que io_service::run() vuelva inmediatamente, se io_service::work un objeto io_service::work (1). Este objeto evita que io_service sin trabajo; por lo tanto, io_service::run() no regresará como resultado de ningún trabajo.

El flujo general es el siguiente:

  1. Cree y agregue el objeto io_service::work agregado al io_service .
  2. io_service::run() creado que invoca io_service::run() . Estos subprocesos de trabajo no volverán desde io_service debido al objeto io_service::work .
  3. Agregue 3 manejadores que calculen los números de Fibonacci al io_service , y regrese inmediatamente. Los subprocesos de trabajo, no el subproceso principal, pueden comenzar a ejecutar estos controladores inmediatamente.
  4. Eliminar el objeto io_service::work .
  5. Espere a que los subprocesos de trabajo terminen de ejecutarse. Esto solo ocurrirá una vez que los 3 manejadores hayan terminado la ejecución, ya que io_service no tiene manejadores ni trabajo.

El código podría escribirse de manera diferente, de la misma manera que el Código original, donde los manejadores se agregan al servicio io_service , y luego se io_service bucle de evento io_service . 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));              //   :- 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
}

Sincrónico vs. Asíncrono

Aunque el código en la pregunta utiliza una operación asincrónica, está funcionando efectivamente de forma síncrona, ya que está esperando 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 asíncronas. Muchas veces, puede convertir un sistema complejo en un sistema complicado. Esta answer resalta las ventajas de la programación asincrónica, algunas de las cuales también se tratan en la documentation Boost.Asio.


Para simplificar la forma en que run , piense en ello como un empleado que debe procesar una pila de papel; toma una hoja, hace lo que dice la hoja, arroja la hoja y toma la siguiente; cuando se queda sin sábanas, sale de la oficina. En cada hoja puede haber cualquier tipo de instrucción, incluso agregando una nueva hoja a la pila. De vuelta a asio: puede realizar un trabajo de io_service de dos maneras, esencialmente: utilizando post en él como en la muestra que ha vinculado, o utilizando otros objetos que llaman internamente a la post en el io_service , como el socket y sus métodos async_* .





boost-asio