es.davy.ai

Preguntas y respuestas de programación confiables

¿Tienes una pregunta?

Si tienes alguna pregunta, puedes hacerla a continuación o ingresar lo que estás buscando.

¿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*>

Tags:  , , ,

Answer

  1. Avatar for 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 usar std::execution::par correctamente y de manera segura.

    En el ejemplo proporcionado, se espera que la asignación arr[0] = 3 ocurra antes que arr[0]++ y que arr[0]++ ocurra antes de la carga de arr[0] en std::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*>

Comments are closed.