GCD(C++)并行循环计算错误:代码问题还是GCD本身问题?
GCD并行累加结果错误的原因分析
问题背景与代码实现
以下代码读取TSV文件中的二维数值向量,执行多项式计算后累加得到结果,分别采用串行for循环与GCD的dispatch_apply并行循环实现:
TSV示例数据:
0.335832971253701 0.111283951925475 0.28470219633399 0.237865566116303 0.936298227948222 0.00759627336105169 0.441882043347137 0.744594979690941 0.811153553271307 0.906033395660231
C++实现代码:
#include <iostream> #include <fstream> #include <vector> #include <cmath> #include <sstream> #include <dispatch/dispatch.h> void load_problem(const std::string file, std::vector<std::pair<double,double>>& repo) { repo.clear(); std::ifstream ifs(file); if (ifs.is_open()) { std::string line; while(getline(ifs, line)) { double x= std::nan(""); double y= std::nan(""); std::istringstream istr(line); istr >> std::skipws >> x >> y; if (!isnan(x) && !isnan(y)) { repo.push_back({x, y}); }; } ifs.close(); } } double do_work(const std::vector<std::pair<double,double>>& problem,const std::vector<double>& args, const size_t psz, size_t i) { double score = 0.0; for (__block size_t j=0; j < psz; j++) { score += args[j]*pow(problem[i].first,psz - i); } score += args[psz]; return score; } int main() { // n-factor polynomial - test against a given problem // provided as a set of tab-delimited x y values in 2d.txt std::vector<std::pair<double,double>> problem; const std::vector<double> args = {0.653398943958799,0.575258222088993,-5.54870756928019,-3.56273265353563,12.4189944179562,1.53213505629763,-4.09124685229838,5.7925805708932}; load_problem("2d.tsv",problem); // tab-delimited doubles representing x, y. const size_t psz = args.size() - 1; __block double gcd_accumulator = 0.0; dispatch_apply(problem.size(), dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^(size_t i){ gcd_accumulator += do_work(problem, args, psz, i); }); double for_accumulator = 0.0; for (size_t i=0; i < problem.size(); i++) { for_accumulator += do_work(problem, args, psz, i); } std::cout << gcd_accumulator << std::endl; std::cout << for_accumulator << std::endl; }
测试现象
- 数据量较小时(如5行),两种方式计算结果一致:
31.5181 31.5181
- 数据量增至2000行时,GCD并行计算结果始终错误:
9701.44 11589.8
仅修改TSV文件的条目数量就会触发该问题。
调试尝试
- 尝试用
std::atomic解决并发问题,因直接捕获原子变量会触发拷贝构造函数删除错误,改用指针实现:
std::atomic<double> gcd_a = 0; __block std::atomic<double>* gcd_accumulator = &gcd_a; dispatch_apply(problem.size(), dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^(size_t i){ *gcd_accumulator = *gcd_accumulator + do_work(problem, args, psz, i); });
但结果仍错误:
3476.98 <--- GCD结果 11589.8 <--- 正确串行结果
- 改用先将并行计算结果存入
vector再串行累加,结果完全正确:
__block std::vector<double> answers; answers.reserve(problem.size()); dispatch_apply(problem.size(), dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^(size_t i){ answers[i] = do_work(problem, args, psz, i); }); double gcd_accumulator = 0; for (size_t i=0; i < problem.size(); i++) { gcd_accumulator += answers[i]; }
问题咨询
该错误的根源是代码实现问题,还是Grand Central Dispatch本身的问题?
解答
这完全是代码实现问题,和GCD本身没有关系,具体原因如下:
初始并行累加的线程安全问题
gcd_accumulator += value本质是三步操作:读取当前值、计算新值、写入新值。在多线程环境下,多个线程可能同时读取到相同的旧值,计算后覆盖写入,导致部分累加操作被丢失——小数据量时并发线程少,冲突概率低,所以偶尔结果正确;数据量增大后并发线程增多,冲突概率飙升,结果就会明显错误。std::atomic的误用
你用*gcd_accumulator = *gcd_accumulator + value的写法并没有实现原子累加:读取原子变量的值、计算和、再赋值回去,这三步仍然是分开的非原子操作,线程之间依然会出现竞态条件。正确的原子累加应该使用std::atomic的fetch_add方法,它能保证整个累加操作的原子性:
std::atomic<double> gcd_accumulator = 0.0; dispatch_apply(problem.size(), dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^(size_t i){ gcd_accumulator.fetch_add(do_work(problem, args, psz, i), std::memory_order_relaxed); });
(注:std::memory_order_relaxed适合这种纯累加的场景,性能最优)
- 先存vector再累加的正确性
这种方式每个线程只写入vector中属于自己的索引位置,不存在多线程写同一内存地址的情况,完全避免了竞态条件,最后串行累加自然能得到正确结果,这是一种典型的"分治"思路——并行计算各个子任务,最后合并结果。
内容的提问来源于stack exchange,提问作者Konchog
相关产品推荐
相关产品推荐

