Espacios de nombres
Variantes
Acciones

Ejecuciones multihilos y carreras de datos (desde C++11)

De cppreference.com
< cpp‎ | language

Un “hilo de ejecución” es un flujo de control dentro de un programa que comienza con la invocación de una función específica de nivel superior (por std::thread, std::async, std::jthread(desde 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 la ejecuta el nuevo hilo, no el hilo creador.

Cualquier hilo puede acceder potencialmente a cualquier objeto y función del programa:

  • Los objetos con duración de almacenamiento automática y local al hilo pueden ser accedidos por otro hilo a través de un puntero o por referencia.
  • En una implementación hospedada, un programa de C++ puede tener más de un hilo ejecutándose simultáneamente. La ejecución de cada hilo se lleva a cabo según lo definido en el resto de esta página. La ejecución de todo el programa consiste en la ejecución de todos sus hilos.
  • En una implementación independiente, la implementación define si un programa puede tener más de un hilo de ejecución.

Para un controlador de señales que no se ejecuta como resultado de una llamada a std::raise, no se especifica qué hilo de ejecución contiene la invocación del controlador de señal.

Contenido

[editar]Carreras de datos

Siempre se permite que diferentes hilos de ejecución accedan (lean y modifiquen) diferentes ubicación de memoria simultáneamente, sin interferencias ni requisitos de sincronización.

Cuando una evaluación de una expresión modifica una ubicación de memoria y otra evaluación lee o modifica la misma ubicación de memoria, se dice que las expresiones están en conflicto. Un programa que tiene dos evaluaciones en conflicto tiene una carrera de datos a menos que

  • ambas evaluaciones se ejecuten en el mismo hilo o en el mismo controlador de señales, o
  • ambas evaluaciones en conflicto sean operaciones atómicas (véase std::atomic), o
  • una de las evaluaciones en conflicto sucede-antes que otra (véase std::memory_order).

Si se produce una carrera de datos, el comportamiento del programa no está definido.

(En particular, la liberación de un std::mutex se sincroniza-con, y por lo tanto, sucede-antes de la adquisición del mismo mutex por otro hilo, lo que hace posible usar cerrojos de mutex para protegerse contra carreras de datos).

int cnt =0;auto f =[&]{ cnt++;};std::thread t1{f}, t2{f}, t3{f};// comportamiento no definido
std::atomic<int> cnt{0};auto f =[&]{ cnt++;};std::thread t1{f}, t2{f}, t3{f};// de acuerdo

[editar]Carreras de datos en contenedores

Todos los contenedores en la biblioteca estándar excepto std::vector<bool> garantizan que las modificaciones concurrentes en el contenido del objeto contenido en diferentes elementos en el mismo contenedor nunca resultarán en carreras 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};// de acuerdostd::thread t3{f, 2}, t4{f, 2};// comportamiento no definido
std::vector<bool> vec ={false, false};auto f =[&](int index){ vec[index]=true;};std::thread t1{f, 0}, t2{f, 1};// comportamiento no definido

[editar]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. Véase std::memory_order para obtener detalles sobre el orden en el que las escrituras realizadas desde hilos se vuelven visibles para otros hilos.

[editar]Progreso hacia adelante

[editar]Libertad de obstrucción

Cuando sólo un hilo que no está bloqueado en una función de la biblioteca estándar ejecuta una función atómica que no tiene bloqueos, se garantiza que esa ejecución se completará (todas las operaciones sin bloqueos de la biblioteca estándar están libres de obstrucciones).

[editar]Libertad de bloqueo

Cuando una o más funciones atómicas sin bloqueo se ejecutan simultáneamente, se garantiza que al menos una de ellas se complete (todas las operaciones sin bloqueo de la biblioteca estándar son libres de bloqueo. Es trabajo de la implementación asegurar que no puedan ser bloqueadas indefinidamente por otros hilos, como por ejemplo robando continuamente la línea de caché).

[editar]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 la biblioteca.
  • Realiza un acceso a través de un gl-valor volatile.
  • Realiza una operación atómica o una operación de sincronización.
  • Continúa la ejecución de un bucle infinito trivial (ver a continuación).

Se dice que un hilo progresa si realiza 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 sin 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 un comportamiento observable, sin tener que demostrar que eventualmente terminarían porque se puede asumir que ningún hilo de ejecución puede ejecutarse para siempre sin realizar ninguno de estos comportamientos observables. Se hace una excepción para bucles infinitos triviales, que no se pueden eliminar ni reordenar.

[editar]Bucles infinitos triviales

Una instrucción de iteración trivialmente vacía es una instrucción 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 (instrucción-de-inicio condición (opcional); ) ; (5)
for (instrucción-de-inicio condición (opcional); ) { } (6)
1) Una instrucción while cuyo cuerpo del bucle es una instrucción simple vacía.
2) Una instrucción while cuyo cuerpo del bucle es una instrucción compuesta vacía.
3) Una instrucción do-while cuyo cuerpo del bucle es una instrucción simple vacía.
4) Una instrucción do-while cuyo cuerpo del bucle es una instrucción compuesta vacía.
5) Una instrucción for cuyo cuerpo del bucle es una instrucción simple vacía, la instrucción for no tiene una expresión-de-iteración.
6) Una instrucción for cuyo cuerpo del bucle es una instrucción compuesta vacía, la instrucción for no tiene una expresión-de-iteración.

La expresión de control de una declaración de iteración trivialmente vacía es:

1-4)condición.
5,6)condición si está presente, de lo contrario true.

Un bucle infinito trivial es una declaración de iteración trivialmente vacía para la cual la expresión de control convertida es una expresión constante, cuando manifiestamente evaluada de manera constante, y se evalúa como true.

El cuerpo del bucle de un bucle infinito trivial se reemplaza con una llamada a la función std::this_thread::yield. Depende de la implementación si este reemplazo ocurre en implementaciones independientes.

for(;;);// bucle infinito trivial, ben definido a partir de P2809for(;;){int x;}// comportamiento no definido

Progreso concurrente hacia adelante

Si un hilo ofrece garantía de progreso concurrente hacia adelante, "progresará" (como se define arriba) en un tiempo finito, siempre y cuando no haya terminado, independientemente de si otros hilos (si los hay) están progresando.

El estándar alienta, pero no exige, que el hilo principal y los hilos iniciados por std::thready std::jthread(desde C++20) ofrezcan garantía de progreso concurrente hacia adelante.

Progreso paralelo hacia adelante

Si un hilo ofrece progreso paralelo hacia adelante, no se requiere que la implementación garantice que el hilo eventualmente progresará si aún no ha ejecutado ningún paso de ejecución (E/S, volátil, atómico o 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 una reserva de hilos que ejecuta tareas en orden arbitrario).

Progreso débilmente paralelo hacia adelante

Si un hilo ofrece progreso débilmente paralelo hacia adelante, no garantiza que eventualmente se logrará progreso, independientemente de si otros hilos progresan o no.

Aún se puede garantizar que estos hilos progresen si se bloquean con la delegación de garantía de progreso hacia adelante: si un hilo P se bloquea de esta manera al completarse una reserva de hilos S, entonces al menos un hilo en S ofrecerá una garantía de progreso hacia adelante que es igual o más fuerte que P. Una vez que ese hilo se completa, otro hilo en S se fortalecerá de manera similar. Una vez que el conjunto esté vacío, P se desbloqueará.

Los algoritmos paralelos de la biblioteca estándar de C++ se bloquean con la delegación de progreso hacia adelante al completarse una reserva no especificada de hilos gestionados por la biblioteca.

(desde C++17)

[editar]Informes de defectos

Los siguientes informes de defectos de cambio de comportamiento se aplicaron de manera retroactiva a los estándares de C++ publicados anteriormente.

ID Aplicado a Comportamiento según lo publicado Comportamiento correcto
LWG 2200 C++11 No estaba claro si el requisito
de carreras de datos de contenedores solo se aplicaba a los contenedores de secuencias.
Se aplica a todos los contenedores.
P2809R3 C++11 El comportamiento de ejecutar bucles “triviales”[1]
infinitos no estaba definido.
Define apropiadamente los “bucles infinitos triviales”
y el comportamiento está bien definido.
  1. “Trivial” aquí significa que ejecutar el bucle infinito nunca produce ningún progreso.
close