我有一个并行代码,它进行一些计算,然后向循环外的double变量添加一个double。我尝试使用std::atomic,但它不支持对std::原子<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;
显然不会胡说八道。我的问题是,如何在不使代码串行的情况下解决这个问题?
在浮点类型上原子地执行算术运算的典型方法是使用compare-and-swap(CAS(循环。
double value;
//some computation of the double value
double expected = atomic_load(&dResCrossAT);
while (!atomic_compare_exchange_weak(&dResCrossAT, &expected, expected + value));
在Jeff Preshing关于这类操作的文章中可以找到详细的解释。
我认为排除非原子变量中的部分内存写入需要互斥,我不确定这是确保没有写入冲突的唯一方法,但它是像一样完成的
#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个时钟周期的系统调用
道德:谨慎使用。
如果您的向量每个线程有许多元素,那么您应该考虑实现减少,而不是对每个元素使用原子操作。原子能操作比普通商店要贵得多。
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];
}
}
该算法不需要原子操作,并且在许多情况下会更快。如果线程数非常高(例如在GPU上(,则可以用树或原子替换第二阶段。
像TBB和Kokkos这样的并发库都提供并行的reduce模板,它们在内部做正确的事情。