У меня есть параллельный код, который выполняет некоторые вычисления, а затем добавляет двойное значение к двойной переменной вне цикла. Я пробовал использовать 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;
ерунда явно отпустить. У меня вопрос, как я могу решить эту проблему, не делая код серийным?





Я считаю, что для исключения частичной записи в память в неатомарной переменной требуется мьютекс, я не уверен, что это единственный способ гарантировать отсутствие конфликта записи, но это достигается следующим образом
#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 тактов.
Мораль: использовать с осторожностью.
Если читаемая переменная является разделяемой памятью, почему параллельное чтение требует защиты? Это потому, что существует вероятность того, что другой поток начнет запись, пока поток находится в середине чтения?
Чтение из неатомарной переменной во время записи в нее другого потока не допускается стандартом C++. Это технически неопределенное поведение.
Типичный способ атомарного выполнения арифметических операций над типом с плавающей запятой - это цикл сравнения и замены (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 В этом нет ничего плохого, и даже кажется, что это проще, потому что вы избавляетесь от нагрузки. Проблема в том, что теперь CAS нужно выполнять дважды, что является пессимизацией по сравнению с одной загрузкой + CAS.
Если в вашем векторе много элементов на поток, вам следует подумать о реализации сокращения, а не об использовании атомарной операции для каждого элемента. Атомарные операции намного дороже обычных магазинов.
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, предоставляют шаблоны параллельного сокращения, которые внутренне делают правильные вещи.
Вы правы, что
std:mutexможет защитить переменную, к которой выполняется одновременный доступ для чтения / записи, но часть чтения также нуждается в защите, а не только запись