You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 20:55:09