¿Los algoritmos paralelos como for_each se sincronizan con el código circundante?
Esto surgió mientras pensaba en https://stackoverflow.com/questions/70230298/thread-sanitizer-warnings-after-using-parallel-stdfor-each/70271363#70271363.
Algoritmos como std::for_each
con políticas de ejecución en paralelo pueden ejecutar código en hilos de trabajo creados por la implementación. ¿Estos hilos se sincronizan con la llamada y el retorno de for_each
por parte del hilo que realiza la llamada, o algo así? El sentido común sugiere que deberían, pero no puedo encontrar una garantía en el estándar C++20.
Considera el siguiente ejemplo simple (pruébalo en godbolt):
#include <algorithm>
#include <execution>
#include <iostream>
void increment(int &a) {
a++;
}
int main(void) {
constexpr size_t n = 1000;
static int arr[n];
arr[0] = 3;
std::for_each(std::execution::par, arr, arr+n, increment);
std::cout << arr[0] << std::endl;
return 0;
}
Esto está diseñado para siempre mostrar 4
.
La implementación puede llamar a increment(arr[0])
en otro hilo, lo cual hace arr[0]++
. ¿El almacenamiento arr[0] = 3
en el hilo principal ocurre antes de arr[0]++
en el sentido de intro.races p10
? Del mismo modo, ¿arr[0]++
ocurre antes de la carga de arr[0]
en std::cout << arr[0]
? Ingenuamente esperaría que sí, pero no puedo encontrar ninguna forma de probarlo. algorithms.parallel
no parece contener nada sobre la sincronización con el código circundante.
Si no es así, entonces el ejemplo contiene carreras de datos y su comportamiento es indefinido. Esto haría bastante difícil utilizar std::execution::par
correctamente, y me preguntaría si es un defecto.
Sin garantía alguna, la implementación podría hacer algo como lo siguiente:
std::atomic<int*> work = nullptr;
void do_work() {
int *p;
while (!(p = work.load(std::memory_order_relaxed)))
std::this_thread::yield();
(*p)++;
}
// iniciado al inicio del programa
std::thread worker_thread(do_work);
int main() {
// ...
arr[0] = 3;
// for_each hace lo siguiente:
work.store(&arr[0], std::memory_order_relaxed);
worker_thread.join();
// ...
}
Si lo hiciera, realmente tendríamos una carrera de datos.</int*>
davy.ai
El estándar C++20 no proporciona una garantía de que los hilos creados por las políticas de ejecución paralela de algoritmos como
std::for_each
se sincronicen con el hilo de llamada. Por lo tanto, es posible que estos hilos se ejecuten concurrentemente con el hilo de llamada sin ninguna garantía de sincronización, lo que puede llevar a carreras de datos y un comportamiento indefinido. Sin una garantía explícita, es difícil usarstd::execution::par
correctamente y de manera segura.En el ejemplo proporcionado, se espera que la asignación
arr[0] = 3
ocurra antes quearr[0]++
y quearr[0]++
ocurra antes de la carga dearr[0]
enstd::cout << arr[0]
, pero esto no se puede demostrar sin la garantía de sincronización con el hilo de llamada. Sin dicha garantía, la implementación puede ejecutar código en hilos paralelos sin garantías de sincronización, lo que lleva a un comportamiento indefinido.Una posible implementación que podría causar carreras de datos es aquella en la que los hilos son creados por la implementación y trabajan en un
std::atomic<int*>
compartido, sin ninguna sincronización explícita con el hilo de llamada. Por lo tanto, es importante tener precaución al usar políticas de ejecución paralela y asegurarse de que el programa no dependa de ninguna garantía implícita de sincronización.</int*>