Добавление двойника в параллельный цикл - std :: atomic <double>

У меня есть параллельный код, который выполняет некоторые вычисления, а затем добавляет двойное значение к двойной переменной вне цикла. Я пробовал использовать std :: atomic, но он не поддерживает арифметические операции с переменными std :: atomic <double>.

double dResCross = 0.0;
std::atomic<double> dResCrossAT = 0.0;

Concurrency::parallel_for(0, iExperimentalVectorLength, [&](size_t m)
{
     double value;
     //some computation of the double value
     atomic_fetch_add(&dResCrossAT, value);
});
dResCross += dResCrossAT;

Просто писать

dResCross += value;

ерунда явно отпустить. У меня вопрос, как я могу решить эту проблему, не делая код серийным?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
473
3

Ответы 3

Я считаю, что для исключения частичной записи в память в неатомарной переменной требуется мьютекс, я не уверен, что это единственный способ гарантировать отсутствие конфликта записи, но это достигается следующим образом

#include <mutex>
#include <thread>

std::mutex mtx;

void threadFunction(double* d){
    while (*d < 100) {
        mtx.lock();
        *d += 1.0;
        mtx.unlock();
    }
}

int main() {
    double* d = new double(0);
    std::thread thread(threadFunction, d);
    while (true) {
        if (*d == 100) {
            break;
        }
    }
    thread.join();
}

Это добавит 1.0 к d 100 раз потокобезопасным способом. Блокировка и разблокировка мьютекса гарантирует, что только один поток обращается к d в данный момент. Однако это значительно медленнее, чем аналог atomic, потому что блокировка и разблокировка настолько дороги - я слышал разные вещи в зависимости от операционной системы и конкретного процессора, а также того, что блокируется или разблокируется, но в этом примере это около 50 тактов. , но для этого может потребоваться системный вызов, который больше похож на 2000 тактов.

Мораль: использовать с осторожностью.

Вы правы, что std:mutex может защитить переменную, к которой выполняется одновременный доступ для чтения / записи, но часть чтения также нуждается в защите, а не только запись

LWimsey 03.12.2018 22:08

Если читаемая переменная является разделяемой памятью, почему параллельное чтение требует защиты? Это потому, что существует вероятность того, что другой поток начнет запись, пока поток находится в середине чтения?

user10737101 03.12.2018 22:24

Чтение из неатомарной переменной во время записи в нее другого потока не допускается стандартом C++. Это технически неопределенное поведение.

LWimsey 03.12.2018 22:32

Типичный способ атомарного выполнения арифметических операций над типом с плавающей запятой - это цикл сравнения и замены (CAS).

 double value;
 //some computation of the double value

 double expected = atomic_load(&dResCrossAT);

 while (!atomic_compare_exchange_weak(&dResCrossAT, &expected, expected + value));

Подробное объяснение этого класса операций можно найти в Статья Джеффа Прешинга.

Я видел аналогичный код с пустым (или нулевым) expected; Затем CAS передает это ... Разве это не проще?

Tootsie 04.12.2018 22:37

@Tootsie В этом нет ничего плохого, и даже кажется, что это проще, потому что вы избавляетесь от нагрузки. Проблема в том, что теперь CAS нужно выполнять дважды, что является пессимизацией по сравнению с одной загрузкой + CAS.

LWimsey 04.12.2018 23:03

Если в вашем векторе много элементов на поток, вам следует подумать о реализации сокращения, а не об использовании атомарной операции для каждого элемента. Атомарные операции намного дороже обычных магазинов.

double global_value{0.0};
std::vector<double> private_values(num_threads,0.0);
parallel_for(size_t k=0; k<n; ++k) {
    private_values[my_thread] += ...;
}
if (my_thread==0) {
    for (int t=0; t<num_threads; ++t) {
         global_value += private_values[t];
    }
}

Этот алгоритм не требует атомарных операций и во многих случаях будет быстрее. Вы можете заменить вторую фазу деревом или атомиками, если количество потоков очень велико (например, на графическом процессоре).

Обе библиотеки параллелизма, такие как TBB и Kokkos, предоставляют шаблоны параллельного сокращения, которые внутренне делают правильные вещи.

Другие вопросы по теме