¿Por qué necesito hebra por conexión cuando uso boost :: asio?

Estoy revisando el ejemplo de HTTP Server 3 en el sitio web de Boost.

¿Podrían explicarme por qué necesito un strand por conexión? Como puedo ver, llamamos read_some solo en handler de read-event. Así que, básicamente, read_some llamadas son secuenciales, por lo tanto, no hay necesidad de cadena (y el artículo 2 del 3er párrafo dice lo mismo). ¿Dónde está el riesgo en el entorno de multi-threading?

La documentación es correcta Con una implementación de protocolo half duplex, como HTTP Server 3 , el strand no es necesario. Las cadenas de llamadas se pueden ilustrar de la siguiente manera:

 void connection::start() { socket.async_receive_from(..., &handle_read); ----. } | .------------------------------------------------' | .-----------------------------------------. VV | void connection::handle_read(...) | { | if (result) | boost::asio::async_write(..., &handle_write); ---|--. else if (!result) | | boost::asio::async_write(..., &handle_write); --|--| else | | socket_.async_read_some(..., &handle_read); ----' | } | .---------------------------------------------------' | V void handle_write(...) 

Como se muestra en la ilustración, solo se inicia un solo evento asíncrono por ruta. Sin posibilidad de ejecución concurrente de los manejadores u operaciones en socket_ , se dice que se está ejecutando en un capítulo implícito.


Seguridad de subprocesos

Si bien no se presenta como un problema en el ejemplo, me gustaría destacar un detalle importante de cadenas y operaciones compuestas, como boost::asio::async_write . Antes de explicar los detalles, primero cubramos el modelo de seguridad del hilo con Boost.Asio. Para la mayoría de los objetos Boost.Asio, es seguro tener múltiples operaciones asíncronas pendientes en un objeto; solo se especifica que las llamadas simultáneas al objeto no son seguras. En los diagtwigs siguientes, cada columna representa un hilo y cada línea representa lo que está haciendo un hilo en un momento determinado.

Es seguro que un único hilo realice llamadas secuenciales mientras que otros hilos no generan ninguno.

  thread_1 |  thread_2
 -------------------------------------- + ----------- ----------------------------
 socket.async_receive (...);  |  ...
 socket.async_write_some (...);  |  ... 

Es seguro que múltiples hilos realicen llamadas, pero no al mismo tiempo:

  thread_1 |  thread_2
 -------------------------------------- + ----------- ----------------------------
 socket.async_receive (...);  |  ...
 ... |  socket.async_write_some (...); 

Sin embargo, no es seguro que múltiples hilos hagan llamadas simultáneamente 1 :

  thread_1 |  thread_2
 -------------------------------------- + ----------- ----------------------------
 socket.async_receive (...);  |  socket.async_write_some (...);
 ... |  ... 

Hilos

Para evitar invocaciones concurrentes, los manejadores a menudo se invocan desde dentro de filamentos. Esto se hace por:

  • Envolver el controlador con strand.wrap . Esto devolverá un nuevo controlador, que se enviará a través del capítulo.
  • Contabilizando o enviando directamente a través del capítulo.

Las operaciones compuestas son únicas en el sentido de que las llamadas intermedias a la secuencia se invocan dentro de la cadena del manejador , si hay una presente, en lugar de la cadena en la que se inicia la operación compuesta. Cuando se compara con otras operaciones, esto presenta una inversión de donde se especifica el filamento. Aquí hay un ejemplo de código que se enfoca en el uso de filamentos, que demostrará un socket que se lee a través de una operación no compuesta, y se escribirá simultáneamente con una operación compuesta.

 void start() { // Start read and write chains. If multiple threads have called run on // the service, then they may be running concurrently. To protect the // socket, use the strand. strand_.post(&read); strand_.post(&write); } // read always needs to be posted through the strand because it invokes a // non-composed operation on the socket. void read() { // async_receive is initiated from within the strand. The handler does // not affect the strand in which async_receive is executed. socket_.async_receive(read_buffer_, &handle_read); } // This is not running within a strand, as read did not wrap it. void handle_read() { // Need to post read into the strand, otherwise the async_receive would // not be safe. strand_.post(&read); } // The entry into the write loop needs to be posted through a strand. // All intermediate handlers and the next iteration of the asynchronous write // loop will be running in a strand due to the handler being wrapped. void write() { // async_write will make one or more calls to socket_.async_write_some. // All intermediate handlers (calls after the first), are executed // within the handler's context (strand_). boost::asio::async_write(socket_, write_buffer_, strand_.wrap(&handle_write)); } // This will be invoked from within the strand, as it was a wrapped // handler in write(). void handle_write() { // handler_write() is invoked within a strand, so write() does not // have to dispatched through the strand. write(); } 

Importancia de los tipos de manipuladores

Además, dentro de las operaciones compuestas, Boost.Asio utiliza la búsqueda dependiente de argumento (ADL) para invocar manejadores intermedios a través del filamento del manejador de finalización. Como tal, es importante que el tipo del manejador de finalización tenga los asio_handler_invoke() apropiados asio_handler_invoke() . Si se produce borrado de tipo a un tipo que no tiene los ganchos asio_handler_invoke() apropiados, como en el caso en que se construye una boost::function del tipo de retorno de strand.wrap , los manejadores intermedios se ejecutarán fuera del capítulo, y solo el controlador de finalización se ejecutará dentro del capítulo. Vea esta respuesta para más detalles.

En el siguiente código, todos los controladores intermedios y el controlador de finalización se ejecutarán dentro del capítulo:

 boost::asio::async_write(stream, buffer, strand.wrap(&handle_write)); 

En el siguiente código, solo el controlador de finalización se ejecutará dentro del capítulo. Ninguno de los controladores intermedios se ejecutará dentro del capítulo:

 boost::function handler(strand.wrap(&handle_write)); boost::asio::async_write(stream, buffer, handler); 

1. El historial de revisión documenta una anomalía a esta regla. Si el sistema operativo lo admite, las operaciones de lectura, escritura, aceptación y conexión síncronas son seguras para hilos. Lo incluyo aquí para completarlo, pero sugiero usarlo con precaución.

Creo que es porque la operación compuesta async_write . async_write se compone de múltiples socket :: async_write_some asincrónicamente. Strand es útil para serializar esas operaciones. Chris Kohlhoff, el autor de asio, habla brevemente de ello en su charla de boostcon alrededor de 1:17.