Multi-threaded executions and data races (since C++11)
Un hilo de ejecución es un flujo de control dentro de un programa que comienza con la invocación de una función de nivel superior específica (mediante std::thread , std::async , std::jthread (since C++20) u otros medios), e incluye recursivamente cada invocación de función ejecutada posteriormente por el hilo.
- Cuando un hilo crea otro, la llamada inicial a la función de nivel superior del nuevo hilo es ejecutada por el nuevo hilo, no por el hilo creador.
Cualquier hilo puede potencialmente acceder a cualquier objeto y función en el programa:
- Los objetos con duración de almacenamiento automática y local al hilo aún pueden ser accedidos por otro hilo mediante un puntero o por referencia.
- Bajo una implementación hospedada , un programa C++ puede tener más de un hilo ejecutándose concurrentemente. La ejecución de cada hilo procede como se define en el resto de esta página. La ejecución del programa completo consiste en una ejecución de todos sus hilos.
- Bajo una implementación independiente , está definido por la implementación si un programa puede tener más de un hilo de ejecución.
Para un signal handler que no se ejecuta como resultado de una llamada a std::raise , no está especificado qué thread de ejecución contiene la invocación del signal handler.
Contenidos |
Carreras de datos
Diferentes hilos de ejecución siempre pueden acceder (leer y modificar) diferentes memory locations concurrentemente, sin interferencia y sin requisitos de sincronización.
Dos expresiones evaluations conflict si una de ellas modifica una ubicación de memoria o inicia/finaliza el tiempo de vida de un objeto en una ubicación de memoria, y la otra lee o modifica la misma ubicación de memoria o inicia/finaliza el tiempo de vida de un objeto que ocupa almacenamiento que se superpone con la ubicación de memoria.
Un programa que tiene dos evaluaciones conflictivas tiene una data race a menos que
- ambas evaluaciones se ejecutan en el mismo hilo o en el mismo signal handler , o
- ambas evaluaciones conflictivas son operaciones atómicas (ver std::atomic ), o
- una de las evaluaciones conflictivas happens-before otra (ver std::memory_order ).
Si ocurre una condición de carrera de datos, el comportamiento del programa es indefinido.
(En particular, la liberación de un std::mutex está synchronized-with , y por lo tanto, happens-before la adquisición del mismo mutex por otro hilo, lo que hace posible usar bloqueos de mutex para proteger contra carreras de datos.)
int cnt = 0; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // comportamiento indefinido
std::atomic<int> cnt{0}; auto f = [&] { cnt++; }; std::thread t1{f}, t2{f}, t3{f}; // CORRECTO
Carreras de datos en contenedores
Todos los
contenedores
en la biblioteca estándar excepto
std
::
vector
<
bool
>
garantizan que las modificaciones concurrentes en los contenidos del objeto contenido en diferentes elementos del mismo contenedor nunca resultarán en condiciones de carrera de datos.
std::vector<int> vec = {1, 2, 3, 4}; auto f = [&](int index) { vec[index] = 5; }; std::thread t1{f, 0}, t2{f, 1}; // CORRECTO std::thread t3{f, 2}, t4{f, 2}; // comportamiento indefinido
std::vector<bool> vec = {false, false}; auto f = [&](int index) { vec[index] = true; }; std::thread t1{f, 0}, t2{f, 1}; // comportamiento indefinido
Orden de memoria
Cuando un hilo lee un valor de una ubicación de memoria, puede ver el valor inicial, el valor escrito en el mismo hilo, o el valor escrito en otro hilo. Consulte std::memory_order para detalles sobre el orden en que las escrituras realizadas desde hilos se vuelven visibles para otros hilos.
Progreso hacia adelante
Libertad de obstrucción
Cuando solo un hilo que no está bloqueado en una función de la biblioteca estándar ejecuta una función atómica que es lock-free, se garantiza que esa ejecución se completará (todas las operaciones lock-free de la biblioteca estándar son obstruction-free ).
Libertad de bloqueo
Cuando una o más funciones atómicas libres de bloqueo se ejecutan concurrentemente, se garantiza que al menos una de ellas completará (todas las operaciones libres de bloqueo de la biblioteca estándar son lock-free — es responsabilidad de la implementación garantizar que no puedan ser bloqueadas vivas indefinidamente por otros hilos, como mediante el robo continuo de la línea de caché).
Garantía de progreso
En un programa C++ válido, cada hilo eventualmente realiza una de las siguientes acciones:
- Termina.
- Invoca std::this_thread::yield .
- Realiza una llamada a una función de E/S de biblioteca.
- Realiza un acceso a través de un volatile glvalue.
- Realiza una operación atómica o una operación de sincronización.
- Continúa la ejecución de un bucle infinito trivial (ver más abajo).
Se dice que un hilo hace progreso si ejecuta uno de los pasos de ejecución anteriores, se bloquea en una función de la biblioteca estándar, o llama a una función atómica libre de bloqueo que no se completa debido a un hilo concurrente no bloqueado.
Esto permite a los compiladores eliminar, fusionar y reordenar todos los bucles que no tienen comportamiento observable, sin tener que demostrar que eventualmente terminarían porque puede asumir que ningún hilo de ejecución puede ejecutarse para siempre sin realizar ninguno de estos comportamientos observables. Se hace una concesión para los bucles infinitos triviales, que no pueden eliminarse ni reordenarse.
Bucles infinitos triviales
Una sentencia de iteración trivialmente vacía es una sentencia de iteración que coincide con una de las siguientes formas:
while (
condición
) ;
|
(1) | ||||||||
while (
condición
) { }
|
(2) | ||||||||
do ; while (
condición
) ;
|
(3) | ||||||||
do { } while (
condición
) ;
|
(4) | ||||||||
for (
init-statement condición
(opcional)
; ) ;
|
(5) | ||||||||
for (
init-statement condición
(opcional)
; ) { }
|
(6) | ||||||||
La expresión de control de una sentencia de iteración trivialmente vacía es:
Un bucle infinito trivial es una sentencia de iteración trivialmente vacía para la cual la expresión de control convertida es una expresión constante , cuando manifestamente evaluada como constante , y evalúa a true .
El cuerpo del bucle de un bucle infinito trivial se reemplaza con una llamada a la función std::this_thread::yield . Está definido por la implementación si este reemplazo ocurre en implementaciones independientes .
for (;;); // bucle infinito trivial, bien definido según P2809 for (;;) { int x; } // comportamiento indefinido
Progreso concurrente hacia adelanteSi un hilo ofrece garantía de progreso concurrente hacia adelante , este progresará (según se definió anteriormente) en un tiempo finito, siempre que no haya terminado, independientemente de si otros hilos (si los hay) están progresando. El estándar recomienda, pero no requiere, que el hilo principal y los hilos iniciados por std::thread y std::jthread (desde C++20) ofrezcan garantía de progreso concurrente hacia adelante. Progreso paralelo hacia adelanteSi un hilo ofrece garantía de progreso paralelo hacia adelante , la implementación no está obligada a asegurar que el hilo eventualmente progrese si aún no ha ejecutado ningún paso de ejecución (E/S, volátil, atómico o de sincronización), pero una vez que este hilo ha ejecutado un paso, proporciona garantías de progreso concurrente hacia adelante (esta regla describe un hilo en un grupo de hilos que ejecuta tareas en orden arbitrario). Progreso débilmente paralelo hacia adelanteSi un hilo ofrece garantía de progreso débilmente paralelo hacia adelante , no garantiza que eventualmente progrese, independientemente de si otros hilos progresan o no.
A tales hilos aún se les puede garantizar el progreso mediante bloqueo con delegación de garantía de progreso hacia adelante: si un hilo
Los algoritmos paralelos de la biblioteca estándar de C++ se bloquean con delegación de progreso hacia adelante en la finalización de un conjunto no especificado de hilos gestionados por la biblioteca. |
(desde C++17) |
Informes de defectos
Los siguientes informes de defectos que modifican el comportamiento se aplicaron retroactivamente a los estándares de C++ publicados anteriormente.
| DR | Aplicado a | Comportamiento publicado | Comportamiento correcto |
|---|---|---|---|
| CWG 1953 | C++11 |
dos evaluaciones de expresión que inician/finalizan los tiempos de vida
de objetos con almacenamientos superpuestos no entraban en conflicto |
entran en conflicto |
| LWG 2200 | C++11 |
no estaba claro si el requisito de carrera de datos del contenedor
solo se aplicaba a contenedores de secuencia |
se aplica a todos los contenedores |
| P2809R3 | C++11 |
el comportamiento de ejecutar bucles infinitos "triviales"
[1]
no estaba definido |
define adecuadamente "bucles infinitos triviales"
y hace que el comportamiento esté bien definido |
- ↑ "Trivial" aquí significa que ejecutar el bucle infinito nunca produce ningún progreso.