C++多线程蒙特卡洛求π代码的数据竞争问题求助
多线程PI计算程序的数据竞争问题解决
问题描述
我遇到了多线程PI计算程序的数据竞争问题,用Valgrind检测到main函数的第一个for循环存在竞争,推测是因为把循环变量i以引用形式传入线程函数导致的,但不知道怎么解决。以下是我的代码:
void pi(size_t& id, size_t nThread, size_t N, double& pigreco) { size_t begin = id* N / nThread; size_t end = (id+ 1) * N / nThread; random_device rd; // Object to create random seed mt19937 generator(rd()); // Mersenne Twister seeded with rd() uniform_real_distribution<double> distribution(-1.0, 1.0); for (size_t i = begin; i < end; i++) { double x = distribution(generator); double y = distribution(generator); if (sqrt(x*x + y*y) < 1.0) pigreco += 4.0 / double(N); } } int main(int argc, char* argv[]) { if (argc != 2) { cerr << "Usage: ./pi <number of threads>" << endl; exit(EXIT_FAILURE); } size_t nThread = (size_t)atoi(argv[1]); size_t N = 100000000; cout << "Generating " << N << " random values using " << nThread << " thread(s)." << endl; atomic<double> pigreco = 0.0; // create threads vector<thread> threads(nThread); for (size_t i = 0; i < nThread; i++) threads[i] = thread(pi, ref(i), nThread, N, ref(pigreco)); for_each(threads.begin(), threads.end(), mem_fn(&thread::join)); cout << "Estimated value for pi: " << pigreco << endl; exit(EXIT_SUCCESS); }
问题分析
- 循环变量的数据竞争:主线程创建线程时传递
ref(i),循环变量i是主线程和所有子线程共享的变量。主线程的循环会快速递增i,而子线程启动后读取i的时机不确定,可能拿到错误的线程ID,同时多线程读写同一个非原子变量i,明确触发数据竞争。 - 原子操作的性能问题:虽然
pigreco是atomic<double>,但循环内频繁执行pigreco += ...会触发大量原子读-修改-写操作,不仅性能低下,也存在不必要的同步开销。 - 随机数生成的性能瓶颈:每个线程都创建
random_device,而random_device通常依赖系统调用,多线程并发调用会导致性能下降。
解决方案
1. 解决循环变量竞争
将线程函数的id参数从引用改为值传递,主线程创建线程时直接传递i的副本,而非引用。这样每个线程拿到独立的ID值,彻底避免对主线程循环变量的竞争。
2. 优化原子操作
让每个线程先计算局部命中次数,最后再将局部结果合并到全局变量pigreco,只需要一次原子加法操作,大幅减少同步开销。
3. 优化随机数生成
结合线程ID和random_device生成种子,既保证随机性,又避免多线程下random_device的性能问题;同时去掉sqrt计算,直接比较平方和,减少浮点运算量。
修改后的代码
#include <iostream> #include <vector> #include <thread> #include <algorithm> #include <functional> #include <random> #include <cmath> #include <atomic> #include <cstdlib> void pi(size_t id, size_t nThread, size_t N, std::atomic<double>& pigreco) { size_t begin = id * N / nThread; size_t end = (id + 1) * N / nThread; size_t local_count = 0; // 结合线程ID生成随机种子,平衡随机性与性能 std::mt19937 generator(id * 1000 + std::random_device{}()); std::uniform_real_distribution<double> distribution(-1.0, 1.0); for (size_t i = begin; i < end; ++i) { double x = distribution(generator); double y = distribution(generator); // 去掉sqrt,直接比较平方和,结果一致且计算更快 if (x*x + y*y < 1.0) { ++local_count; } } // 仅一次原子操作合并结果 pigreco += 4.0 * local_count / static_cast<double>(N); } int main(int argc, char* argv[]) { if (argc != 2) { std::cerr << "Usage: ./pi <number of threads>" << std::endl; exit(EXIT_FAILURE); } size_t nThread = static_cast<size_t>(atoi(argv[1])); size_t N = 100000000; std::cout << "Generating " << N << " random values using " << nThread << " thread(s)." << std::endl; std::atomic<double> pigreco = 0.0; std::vector<std::thread> threads(nThread); // 传递i的值而非引用,避免数据竞争 for (size_t i = 0; i < nThread; ++i) { threads[i] = std::thread(pi, i, nThread, N, std::ref(pigreco)); } // 简化线程join的写法 for (auto& thread : threads) { thread.join(); } std::cout << "Estimated value for pi: " << pigreco << std::endl; exit(EXIT_SUCCESS); }
关键修改说明
- 线程ID传递方式:从
ref(i)改为直接传i,每个线程获取独立的ID副本,消除循环变量的数据竞争。 - 局部结果合并:用
local_count统计当前线程的命中次数,最后仅做一次原子加法,减少同步开销,提升性能。 - 计算优化:去掉
sqrt调用,直接比较平方和,降低浮点运算量。 - 随机数生成优化:结合线程ID生成种子,避免多线程下
random_device的性能瓶颈。
内容的提问来源于stack exchange,提问作者LordGrim
相关产品推荐
相关产品推荐

