Está vagamente relacionado con esta pregunta: ¿std :: thread está agrupado en C ++ 11? . Aunque la pregunta es diferente, la intención es la misma:
Pregunta 1: ¿Todavía tiene sentido usar su propio grupo de subprocesos (o una biblioteca de terceros) para evitar la creación de subprocesos costosa?
La conclusión en la otra pregunta fue que no se puede confiar en std::thread
que se agruparán (podría serlo o no). Sin embargo, std::async(launch::async)
parece tener una probabilidad mucho mayor de agruparse.
No creo que esté 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 es económico crear un nuevo hilo, esperaría que siempre generen un nuevo hilo.
Pregunta 2: Esto es solo lo que pienso, pero no tengo hechos que lo prueben. Bien puedo estar equivocado. ¿Es una suposición fundamentada?
Finalmente, aquí he proporcionado 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 en
auto future = async(launch::async, []{ f(); });
// ...
future.wait();
Ejemplo 2: disparar y olvidar hilo
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(); });
Pregunta 3: ¿Preferiría las async
versiones a las thread
versiones?
El resto ya no es parte de la pregunta, sino solo para aclarar:
¿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 std::async
, ya que de lo contrario se ejecuta el destructor, que se bloquea hasta que finaliza la acción. Algunos lo consideran 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 esta pregunta solo me interesa el aspecto de evitar 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 locales de subprocesos también pueden ser un argumento para sus propios grupos de subprocesos, pero no estoy seguro de si es relevante en la práctica:
- Crear un nuevo hilo con
std::thread
inicios sin variables locales de hilo inicializadas. Quizás esto no es lo que quieres. - En los subprocesos generados por
async
, no está claro para mí porque el subproceso podría haberse reutilizado. Según tengo entendido, no se garantiza que se restablezcan las variables locales de subprocesos, pero puedo estar equivocado. - El uso de sus propios grupos de subprocesos (de tamaño fijo), por otro lado, le brinda un control total si realmente lo necesita.
fuente
std::async(launch::async)
parece tener una probabilidad mucho mayor de ser agrupada". No, creostd::async(launch::async | launch::deferred)
que se puede agrupar. Con sololaunch::async
la tarea se supone que debe iniciarse en un nuevo hilo independientemente de qué otras tareas se estén ejecutando. Con la políticalaunch::async | launch::deferred
, la implementación puede elegir qué política, pero lo más importante es retrasar la elección de qué política. Es decir, puede esperar hasta que un subproceso en un grupo de subprocesos esté disponible y luego elegir la política asincrónica.std::async()
. Todavía tengo curiosidad por ver cómo admiten destructores thread_local no triviales en un grupo de subprocesos.launch::async
entonces la trata como si fuera únicalaunch::deferred
y nunca la ejecuta de forma asincrónica, por lo que, en efecto, esa versión de libstdc ++ "elige" utilizar siempre diferido a menos que se fuerce de otra manera.std::async
podría haber sido algo hermoso para el rendimiento; podría haber sido el sistema estándar de ejecución de tareas de ejecución corta, naturalmente respaldado por un grupo de subprocesos. En este momento, es solo unastd::thread
con algunas tonterías agregadas para que la función del hilo pueda devolver un valor. Ah, y agregaron una funcionalidad "diferida" redundante que se superponestd::function
completamente al trabajo .Respuestas:
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 probar, determiné que la sobrecarga de la llamada de función en un subproceso nuevo frente a uno 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 lenta que una llamada de función simple. Entonces, si está emitiendo muchas llamadas a funciones pequeñas, un grupo de subprocesos podría ser una buena idea.
Es bastante evidente que la biblioteca C ++ estándar 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 subprocesos, probablemente sería más barato que iniciar un nuevo subproceso. Y el estándar lo permite.
En mi humilde opinión, la gente del kernel de Linux debería trabajar para hacer que la creación de hilos 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 hilo, por supuesto, fuerza la creación de un nuevo hilo en lugar de usar uno de un grupo. Entonces::std::async(::std::launch::async, ...)
se prefiere.Pregunta 2 :
Sí, básicamente esto lanza un hilo 'implícitamente'. Pero realmente, todavía es bastante obvio lo que está sucediendo. Así que no creo que la palabra implícitamente sea una palabra particularmente buena.
Tampoco estoy convencido de que obligarte a esperar una devolución antes de la destrucción sea necesariamente un error. No sé si debería usar la
async
llamada 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. Valoro mucho las islas donde puede garantizar el acceso en serie. De lo contrario, terminará con un estado mutable en el que siempre tendrá que envolver un mutex en algún lugar y recordar usarlo.
Me gustó mucho más el modelo de cola de trabajo que el modelo 'futuro' porque hay 'islas de serie' por ahí para que pueda manejar de manera más efectiva 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 para llamar cosas y obtuve estos números en un sistema de 8 núcleos (AMD Ryzen 7 2700X) que ejecuta Fedora 29 compilado con clang versión 7.0.1 y libc ++ (no libstdc ++):
Y nativo, en mi MacBook Pro de 15 "(Intel (R) Core (TM) i7-7820HQ CPU @ 2.90GHz) con
Apple LLVM version 10.0.0 (clang-1000.10.44.4)
OSX 10.13.6, obtengo esto:Para el subproceso de trabajo, inicié un subproceso, luego usé una cola sin bloqueo para enviar solicitudes a otro subproceso y luego esperé a que se enviara una respuesta "Listo".
El "No hacer nada" es solo para probar la parte superior del arnés de prueba.
Está claro que la sobrecarga de lanzar un hilo es enorme. E incluso el subproceso de trabajo con la cola entre subprocesos ralentiza las cosas en un factor de 20 aproximadamente en Fedora 25 en una máquina virtual, y en aproximadamente 8 en OS X nativo.
Creé un proyecto de Bitbucket con el código que usé para la prueba de rendimiento. Se puede encontrar aquí: https://bitbucket.org/omnifarious/launch_thread_performance
fuente