¿Por qué necesito un hilo por conexión cuando uso boost::asio?
Estoy revisando el ejemplo de HTTP Server 3 en el sitio web de Boost.
¿ Podrían explicar por qué necesito strand
por conexión? Como puedo ver, llamamos read_some
solo al controlador de evento de lectura. Básicamente, read_some
las llamadas son secuenciales, por lo que no hay necesidad de hilo (y el punto 2 del tercer párrafo dice lo mismo). ¿Dónde está el riesgo en un entorno de subprocesos múltiples?
La documentación es correcta. Con una implementación de protocolo semidúplex, como HTTP Server 3 , strand
no es necesario. Las cadenas de llamadas se pueden ilustrar de la siguiente manera:
void connection::start()
{
socket.async_receive_from(..., &handle_read); ----.
} |
.------------------------------------------------'
| .-----------------------------------------.
V V |
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 evento asincrónico por ruta. Sin posibilidad de ejecución simultánea de los controladores u operaciones en socket_
, se dice que se ejecuta en una cadena implícita.
Seguridad del hilo
Si bien no se presenta como un problema en el ejemplo, me gustaría resaltar un detalle importante de las hebras y operaciones compuestas, como boost::asio::async_write
. Antes de explicar los detalles, primero cubramos el modelo de seguridad de subprocesos con Boost.Asio. Para la mayoría de los objetos Boost.Asio, es seguro tener múltiples operaciones asincrónicas pendientes en un objeto; simplemente se especifica que las llamadas simultáneas al objeto no son seguras. En los diagramas siguientes, cada columna representa un hilo y cada línea representa lo que está haciendo un hilo en un momento dado.
Es seguro que un solo subproceso realice llamadas secuenciales mientras que otros subprocesos no realizan ninguna:
hilo_1 | hilo_2 --------------------------------------+----------- ---------------------------- socket.async_receive(...); | ... socket.async_write_some(...); | ...
Es seguro que varios subprocesos realicen llamadas, pero no al mismo tiempo:
hilo_1 | hilo_2 --------------------------------------+----------- ---------------------------- socket.async_receive(...); | ... ... | socket.async_write_some(...);
Sin embargo, no es seguro que varios subprocesos realicen llamadas simultáneamente 1 :
hilo_1 | hilo_2 --------------------------------------+----------- ---------------------------- socket.async_receive(...); | socket.async_write_some(...); ... | ...
Hilos
Para evitar invocaciones simultáneas, los controladores a menudo se invocan desde dentro de las hebras. Esto se hace mediante:
- Envolviendo el controlador con
strand.wrap
. Esto devolverá un nuevo controlador, que se enviará a través del hilo. - Publicación o envío directo a través del hilo.
Las operaciones compuestas son únicas en el sentido de que las llamadas intermedias a la secuencia se invocan dentro de la cadena del controlador , si hay una presente, en lugar de la cadena en la que se inicia la operación compuesta. En comparación con otras operaciones, esto presenta una inversión del lugar donde se especifica el hilo. A continuación se muestra un código de ejemplo centrado en el uso de hebras, que demostrará un socket del que se lee mediante una operación no compuesta y se escribe 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 controlador
Además, dentro de las operaciones compuestas, Boost.Asio utiliza la búsqueda dependiente de argumentos (ADL) para invocar controladores intermedios a través de la cadena del controlador de finalización. Como tal, es importante que el tipo de controlador de finalización tenga los asio_handler_invoke()
ganchos adecuados. Si se produce un borrado de tipo en un tipo que no tiene los asio_handler_invoke()
ganchos apropiados, como en el caso en el que a boost::function
se construye a partir del tipo de retorno de strand.wrap
, entonces los controladores intermedios se ejecutarán fuera de la cadena, y solo el controlador de finalización se ejecutará dentro de la cadena. Consulte esta respuesta para obtener más detalles.
En el siguiente código, todos los controladores intermedios y el controlador de finalización se ejecutarán dentro de la cadena:
boost::asio::async_write(stream, buffer, strand.wrap(&handle_write));
En el siguiente código, solo se ejecutará el controlador de finalización dentro de la cadena. Ninguno de los controladores intermedios se ejecutará dentro de la cadena:
boost::function<void()> handler(strand.wrap(&handle_write));
boost::asio::async_write(stream, buffer, handler);
1. El historial de revisiones documenta una anomalía en esta regla. Si el sistema operativo lo admite, las operaciones síncronas de lectura, escritura, aceptación y conexión son seguras para subprocesos. Lo incluyo aquí para que esté completo, pero sugiero usarlo con precaución.
Creo que se debe a la operación compuesta async_write . async_write
se compone de múltiples sockets::async_write_some de forma asincrónica. Strand es útil para serializar esas operaciones. Chris Kohlhoff, el autor de asio, habla brevemente de ello en su charla de boostcon alrededor del minuto 1:17.