多线程C++时成员变量自行改变

Member variables changing on their own when multithreading c++

这是我第一次尝试对我的代码进行多线程处理。

该代码包含一个 class Simulation,它创建单独的 Simulation 对象。因为我需要 运行 其中的几个,所以我想 运行 它们跨多个线程并行。该代码在串行中运行得非常好,但是当将每个模拟对象方法分配给不同的线程时,我在不同的时间(通常很早)遇到分段错误,我认为这是由于某种数据竞争的发生。深入挖掘我发现一些成员变量似乎被重新初始化或只是改变值(并非在每个 运行 中都是一致的)。我很清楚有些资源正在混淆,但是当我 运行 在一个独立的线程中进行每个模拟时(或者我认为如此)怎么会发生这种情况?

这是代码的简化版本。

模拟class:

   class Simulation{
    public:
    void run(){
        //Complicated stuff;                    
       }
    };

main.cpp:

int main(){
        vector<Simulation> simulations;
        vector<thread> threads;

    for (int i=0; i<nSimulations; i++){
        simulations.push_back(
            Simulation(params));
        threads.push_back(thread(&Simulation::run,
            std::ref(simulations[i])));
    }

    for (int i=0; i<nSimulations; i++){
        threads[i].join();
        simulations[i].saveToFile("test.dat");
    }

return 0;
}

这段代码有什么本质上的错误吗?实际代码相当复杂,所以至少我想知道这是否是将不同对象方法多线程化到不同线程的正确方法。

你应该非常谨慎处理std::vector个元素的地址,当你push_back个元素时它们会改变。

    vector<Simulation> simulations;

for (int i=0; i<nSimulations; i++){
    simulations.push_back(
        Simulation(params));
    threads.push_back(thread(&Simulation::run,
        std::ref(simulations[i])));  // <-- This place !
}

这里将vector元素的地址保存在一个for循环中,之前的地址在vector放大时会失效

解决问题的最小更改是在启动任何线程之前构建所有模拟。

int main(){
    vector<Simulation> simulations;
    for (int i=0; i<nSimulations; i++){
        simulations.push_back(Simulation(params)); // or emplace_back(params)
    }
    // or vector<Simulation> simulations(nSimulations, Simulation(params));

    vector<thread> threads;   
    for (int i=0; i<nSimulations; i++){
        threads.push_back(thread(&Simulation::run, std::ref(simulations[i])));
    }

    for (int i=0; i<nSimulations; i++){
        threads[i].join();
        simulations[i].saveToFile("test.dat");
    }

    return 0;
}

现有答案解决了要求的简单案例,我们提前知道模拟次数。解决方案是简单地在模拟向量中保留足够的 space 以防止重新分配。

但是如果不知道模拟的数量,或者必须临时添加模拟怎么办?

一个答案可能是将模拟存储在 std::list 而不是 std::vector 中。然而,我们随后失去了随机访问模拟的能力。

我们可以通过根据 handle/body 习语实施模拟来解决这个问题。句柄是可移动的,并控制实际实现的生命周期。

我是一个例子(我在其中也给出了 运行 模拟的概念 class):

#include <memory>
#include <thread>
#include <vector>

struct SimulationParams {};

struct Simulation
{
    // noncopyable
    Simulation(Simulation const&) = delete;
    Simulation& operator=(Simulation const&) = delete;

    Simulation(SimulationParams params);

    void run()
    {
        // complicated stuff
    }

    void saveToFile(std::string const& path);
};

class SimulationHandle
{
    using impl_class = Simulation;
    using impl_type = std::unique_ptr<impl_class>;
    impl_type impl_;

public:

    SimulationHandle(SimulationParams params)
    : impl_(std::make_unique<impl_class>(std::move(params)))
    {}

    auto saveToFile(std::string const& path) -> decltype(auto)
    {
        return implementation().saveToFile(path);        
    }

    auto runInThread() -> std::thread
    {
        return std::thread { 
            [&sim = this->implementation()]
            {
                sim.run();
            }
        };
    }

    auto implementation() -> impl_class& 
    {
        return *impl_;
    }
};

struct RunningSimulation
{
    RunningSimulation(SimulationParams params)
    : simHandle_{ std::move(params) }
    , thread_ { simHandle_.runInThread() }
    {

    }

    void join()
    {
        if (thread_.joinable())
            thread_.join();
    }

    void saveToFile(std::string const& path)
    {
        join();
        simHandle_.saveToFile(path);
    }

private:
    // DEPENDENCY: ORDER
    //     During constructor, thread_ depends on simHandle_ being constructed
    SimulationHandle simHandle_;
    std::thread thread_;
};

extern int nSimulations;

int main(){
    using std::vector;

    vector<RunningSimulation> simulations;

    for (int i=0; i<nSimulations; i++)
        simulations.emplace_back(SimulationParams());

    for(auto&& rs : simulations)
        rs.saveToFile("test.dat");

    return 0;
}

进一步增强:

当前句柄是根据unique_ptr实现的——这意味着只有一个句柄可以拥有一个模拟。我们可能希望以多种方式为模拟编制索引,这将需要多个句柄。

一个可能的解决方案是简单地将 unique_ptr 替换为 shared_ptr,以共享所有权。另一个可能是拥有生命周期控制句柄(使用 shared_ptr 实现)和生命周期监视器(使用 weak_ptr 实现)的概念。