¿async (launch::async) en C++11 hace que los grupos de subprocesos sean obsoletos para evitar la costosa creación de subprocesos?


Está vagamente relacionado con esta pregunta: ¿Se agrupan los hilos std::en C++11?. Aunque la pregunta difiere, la intención es la misma:

Pregunta 1: ¿Todavía tiene sentido usar sus propios grupos de subprocesos (o una biblioteca de terceros) para evitar la costosa creación de subprocesos?

La conclusión en la otra pregunta fue que no se puede confiar en std::thread para ser agrupado (podría o no podría ser). Sin embargo, std::async(launch::async) parece tener una probabilidad mucho mayor de ser agrupados.

No creo que sea forzado por el estándar, pero en mi humilde opinión, esperaría que todas las buenas implementaciones de C++11 usarían la agrupación de subprocesos si la creación de subprocesos es lenta. Solo en plataformas donde es barato crear un nuevo hilo, esperaría que siempre generen un nuevo hilo.

Pregunta 2: Esto es justo lo que pienso, pero no tengo hechos que lo prueben. Puede que me equivoque. Es una conjetura?

Finalmente, aquí he proporcionado un código de ejemplo que primero muestra cómo creo que la creación de hilos se puede expresar por async(launch::async):

Ejemplo 1:

 thread t([]{ f(); });
 // ...
 t.join();

Se convierte en

 auto future = async(launch::async, []{ f(); });
 // ...
 future.wait();

Ejemplo 2: Hilo de fuego y olvido

 thread([]{ f(); }).detach();

Se convierte en

 // a bit clumsy...
 auto dummy = async(launch::async, []{ f(); });

 // ... but I hope soon it can be simplified to
 async(launch::async, []{ f(); });

Questin 3: ¿Preferirías las versiones async a las versiones thread?


El resto ya no es parte de la pregunta, sino solo para aclaración:

¿Por qué debe asignarse el valor devuelto a un maniquí variable?

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 se bloquea hasta que la acción termina. Es por algunos considerado un error en el estándar (por ejemplo, por Herb Sutter).

Este ejemplo de cppreference.com lo 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 este pregunta Solo estoy interesado en el aspecto de evitar los costosos costos de creación de hilos.

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 memoria. Los grupos de hilos deberían estar bien, aquí.

Las variables Thread-local también pueden ser argumento para sus propios grupos de hilos, pero no estoy seguro de si es revelante en la práctica:

  • La creación de un nuevo subproceso con std::thread comienza sin variables locales de subproceso inicializadas. Tal vez esto no es lo que quieres.
  • En los hilos generados por async, es algo confuso para mí porque el hilo podría haber sido reutilizado. Desde mi punto de vista, las variables thread-local no están garantizadas para ser restablecidas, pero puedo estar equivocado.
  • Usando sus propios grupos de subprocesos (de tamaño fijo) , por otro lado, te da control total si realmente lo necesitas.
Author: Raedwald, 2013-01-16

1 answers

Pregunta 1:

Cambié esto del original porque el original estaba equivocado. Tenía la impresión de que La creación de subprocesos de Linux era muy barata y después de probar determiné que la sobrecarga de la llamada a la función en un subproceso nuevo vs.uno normal es enorme. La sobrecarga para crear un hilo para manejar una llamada a función es algo así como 10000 o más veces más lenta que una llamada a función simple. Por lo tanto, si usted está emitiendo un montón de pequeñas llamadas a funciones, un hilo la piscina podría ser una buena idea.

Es bastante evidente que la biblioteca estándar de C++ que se envía con g++ no tiene grupos de subprocesos. Pero definitivamente puedo ver un caso para ellos. Incluso con la sobrecarga de tener que empujar 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 permite esto.

En mi humilde opinión, la gente del kernel de Linux debería trabajar en hacer 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 el uso de pool para implementar launch::async | launch::deferred.

Y el OP es correcto, usar ::std::thread para lanzar un subproceso por supuesto fuerza la creación de un nuevo subproceso en lugar de usar uno de un pool. Así que 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á pasando. Así que realmente no creo que la palabra implícitamente sea una palabra particularmente buena.

Tampoco estoy convencido de que forzarte esperar un retorno antes de la destrucción es necesariamente un error. No se que debas usar la llamada async para crear subprocesos 'daemon' que no se espera que regresen. 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. Pongo mucho valor en las islas donde se puede garantizar el acceso en serie. De lo contrario, terminas con un estado mutable que siempre tienes que envolver un mutex en algún lugar y recordando usarlo.

Me gustó el modelo de cola de trabajo mucho mejor que el modelo 'futuro' porque hay 'islas de serie' por ahí para que pueda manejar más eficazmente el estado mutable.

Pero en realidad, depende exactamente de lo que estás haciendo.

Prueba de rendimiento

Entonces, probé el rendimiento de varios métodos de llamar a las cosas y se me ocurrieron 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 subproceso worker, inicié un subproceso, luego usé una cola sin bloqueo para enviar solicitudes a otro subproceso y luego esperé a que se enviara una respuesta de "Está hecho".

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 trabajador con el la cola entre subprocesos ralentiza las cosas en un factor de 20 o así en Fedora 25 en una máquina virtual, y en aproximadamente 8 en OS X nativo.

He creado un proyecto Bitbucket que contiene el código que utilicé para la prueba de rendimiento. Se puede encontrar aquí: https://bitbucket.org/omnifarious/launch_thread_performance

 36
Author: Omnifarious,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-18 19:48:45