【问题标题】:Member variables changing on their own when multithreading c++多线程c ++时成员变量自行更改
【发布时间】:2019-03-13 00:40:26
【问题描述】:

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

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

这是代码的简化版本。

模拟类:

   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::ref(simulations[i]) 作为参数传递给线程,因为模拟是vector,并且在调用push_back 方法时,它的元素被重新分配,因此线程对象中指向模拟对象的指针无效。
  • 您可以通过保留向量空间轻松防止重新分配:simulations.reserve(nSimulations);。顺便说一句,使用simulations.back()simulations[i] 更清楚地表达了您的意图。另外,请考虑将emplace_back 与两个向量一起使用。
  • 您的 Simulation 对象没有任何可变成员 - 只有 run()run() 是否以某种方式发生了变化,或者您的示例是否不完整且不可重现?我建议您创建一个 minimal reproducible example 来演示该问题。
  • 不知道push_back 重新分配向量。 @DanielLangr 使用 simulations.reserve(nSimulations) 解决了这个问题。是否有一种更优雅的方式可以总体上实现相同的结果?
  • 如果您的Simulation 对象是可复制的,您只需std::vector&lt;Simulation&gt; simulations(n, { params }); 将它们全部初始化为向量构造函数中的副本。

标签: c++ multithreading class c++11 stdthread


【解决方案1】:

在处理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 !
}

这里将向量元素的地址保存在for循环中,向量放大时之前的地址将失效。

【讨论】:

  • 怎么做会更好
  • 在这种情况下如何实现预期的行为?
  • .push_back() 仅在容量不足时重新分配。在循环修复它之前对.reserve() 的简单调用。
【解决方案2】:

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

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;
}

【讨论】:

  • 如果您想要一个最小的修复,请致电.capacity() 一次。
  • @Deduplicator 你的意思是reserve(nSimulations)
【解决方案3】:

现有答案解决了所要求的简单案例,我们提前知道了模拟的数量。解决方案是简单地在模拟向量中保留足够的空间,以便永远不会发生重新分配。

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

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

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

我的例子(在这个例子中我也给出了运行模拟的概念它自己的类):

#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 实现)的概念。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-30
    • 1970-01-01
    • 2019-08-10
    • 2018-09-27
    • 2012-09-13
    • 1970-01-01
    相关资源
    最近更新 更多