¿Será asincrónico(launch:: async) en C++ 11 que los pools de hilos se vuelvan obsoletos para evitar la costosa creación de hilos?
multithreading asynchronous (1)
Pregunta 1 :
Cambié esto del original porque el original estaba mal. Tenía la impresión de que la creación de subprocesos de Linux era muy barata y, después de las pruebas, determiné que la sobrecarga de una llamada a función con subprocesos frente a una normal es enorme. La sobrecarga para crear un hilo para manejar una llamada de función es algo así como 10000 o más veces más lento que una llamada de función simple. Entonces, si está emitiendo muchas llamadas de función pequeñas, un grupo de subprocesos podría ser una buena idea.
Es bastante evidente que la biblioteca estándar de C ++ que se incluye con g ++ no tiene grupos de hilos. Pero definitivamente puedo ver un caso para ellos. Incluso con la sobrecarga de tener que pasar la llamada a través de algún tipo de cola entre hilos, probablemente sería más barato que iniciar un nuevo hilo. Y el estándar lo permite.
En mi humilde opinión, la gente del kernel de Linux debería trabajar para que la creación de subprocesos sea más barata de lo que es actualmente. Pero, la biblioteca estándar de C ++ también debería considerar usar pool para implementar launch::async | launch::deferred
launch::async | launch::deferred
.
Y el OP es correcto, el uso de ::std::thread
para lanzar un hilo por supuesto obliga a la creación de un nuevo hilo en lugar de utilizar uno de un grupo. Por lo tanto ::std::async(::std::launch::async, ...)
se prefiere ::std::async(::std::launch::async, ...)
.
Pregunta 2 :
Sí, básicamente esto ''implícitamente'' lanza un hilo. Pero en realidad, sigue siendo bastante obvio lo que está sucediendo. Entonces realmente no creo que la palabra implícitamente sea una palabra particularmente buena.
Tampoco estoy convencido de que forzarte a esperar una devolución antes de que la destrucción sea necesariamente un error. No sé si deberías utilizar la llamada async
para crear subprocesos ''daemon'' que no se espera que vuelvan. Y si se espera que regresen, no está bien ignorar las excepciones.
Pregunta 3 :
Personalmente, me gusta que los lanzamientos de hilos sean explícitos. Valoro mucho las islas donde puedes garantizar el acceso serial. De lo contrario, terminas con el estado mutable de que siempre tienes que envolver un mutex en algún lugar y recordar usarlo.
Me gustó el modelo de cola de trabajo mucho mejor que el modelo ''futuro'' porque hay ''islas de serie'' para que pueda manejar de forma más efectiva el estado mutable.
Pero realmente, depende exactamente de lo que estás haciendo.
Prueba de rendimiento
Por lo tanto, probé el rendimiento de varios métodos de llamadas y surgieron estos números en una máquina virtual de 2 CPU que ejecuta Fedora 25 compilada con g ++ 6.3.1:
Do nothing calls per second: 30326536 Empty calls per second: 29348752 New thread calls per second: 15322 Async launch calls per second: 14779 Worker thread calls per second: 1357391
Y nativo, en mi MacBook Retina con Apple LLVM version 8.0.0 (clang-800.0.42.1)
bajo OSX 10.12.3, obtengo esto:
Do nothing calls per second: 20303610 Empty calls per second: 20222685 New thread calls per second: 40539 Async launch calls per second: 45165 Worker thread calls per second: 2662493
Para el hilo de trabajo, inicié un hilo, luego usé una cola sin bloqueo para enviar solicitudes a otro hilo y luego esperé a que se enviara una respuesta "It''s done".
El "No hacer nada" es solo para probar la sobrecarga del arnés de prueba.
Está claro que la sobrecarga de lanzar un hilo es enorme. E incluso el hilo de trabajo con la cola entre subprocesos ralentiza las cosas por un factor de 20 en Fedora 25 en una máquina virtual y por aproximadamente 8 en el sistema operativo nativo X.
Creo un proyecto de Bitbucket con el código que utilicé para la prueba de rendimiento. Se puede encontrar aquí: https://bitbucket.org/omnifarious/launch_thread_performance
Está vagamente relacionado con esta pregunta: ¿están std :: thread mancomunados en C ++ 11? . Aunque la pregunta difiere, la intención es la misma:
Pregunta 1: ¿Sigue teniendo sentido utilizar sus propios grupos de hilos (o bibliotecas de terceros) para evitar la costosa creación de hilos?
La conclusión en la otra pregunta fue que no puede confiar en que std::thread
se agrupe (podría ser o no). Sin embargo, std::async(launch::async)
parece tener muchas más posibilidades de agruparse.
No cree que sea forzado por el estándar, pero en mi humilde opinión, esperaría que todas las buenas implementaciones de C ++ 11 usen la agrupación de subprocesos si la creación de subprocesos es lenta. Solo en plataformas donde no es costoso crear un nuevo hilo, esperaría que siempre generen un nuevo hilo.
Pregunta 2: Esto es justo lo que pienso, pero no tengo hechos para probarlo. Puedo estar equivocado. ¿Es una suposición educada?
Finalmente, aquí proporcioné un código de muestra que primero muestra cómo creo que la creación de subprocesos se puede expresar mediante async(launch::async)
:
Ejemplo 1:
thread t([]{ f(); });
// ...
t.join();
se convierte
auto future = async(launch::async, []{ f(); });
// ...
future.wait();
Ejemplo 2: Fuego y olvido de hilo
thread([]{ f(); }).detach();
se convierte
// a bit clumsy...
auto dummy = async(launch::async, []{ f(); });
// ... but I hope soon it can be simplified to
async(launch::async, []{ f(); });
Questin 3: ¿Prefiere las versiones async
a las versiones de thread
?
El resto ya no es parte de la pregunta, sino solo para una aclaración:
¿Por qué se debe asignar el valor de retorno a una variable ficticia?
Desafortunadamente, el estándar actual de C ++ 11 obliga a capturar el valor de retorno de std::async
, ya que de lo contrario se ejecuta el destructor, que bloquea hasta que la acción finaliza. Es considerado por algunos como un error en el estándar (por ejemplo, por Herb Sutter).
Este ejemplo de cppreference.com ilustra muy bien:
{
std::async(std::launch::async, []{ f(); });
std::async(std::launch::async, []{ g(); }); // does not run until f() completes
}
Otra aclaración:
Sé que los grupos de subprocesos pueden tener otros usos legítimos, pero en esta pregunta solo me interesa el aspecto de evitar los costosos costos de creación de subprocesos .
Creo que todavía hay situaciones en las que los grupos de subprocesos son muy útiles, especialmente si necesita más control sobre los recursos. Por ejemplo, un servidor puede decidir manejar solo un número fijo de solicitudes simultáneamente para garantizar tiempos de respuesta rápidos y aumentar la previsibilidad del uso de la memoria. Los grupos de subprocesos deberían estar bien, aquí.
Las variables de subprocesos locales también pueden ser un argumento para sus propios grupos de subprocesos, pero no estoy seguro de si es revelador en la práctica:
- La creación de un nuevo subproceso con
std::thread
inicia sin las variables locales de subproceso inicializadas. Tal vez esto no es lo que quieres. - En los hilos generados por la
async
, no está claro para mí porque el hilo podría haber sido reutilizado. Según entiendo, no se garantiza que las variables de subprocesos locales se reinicien, pero puedo estar equivocado. - El uso de sus propios grupos de hilos (de tamaño fijo), por otro lado, le da control total si realmente lo necesita.