Переменные-члены изменяются самостоятельно при многопоточности c ++ - PullRequest
0 голосов
/ 08 октября 2018

Это моя первая попытка многопоточности моего кода.

Код состоит из класса Simulation, который создает отдельные объекты Simulation.Поскольку мне нужно запустить несколько из них, я хотел запустить их параллельно в нескольких потоках.Код прекрасно работает в последовательном режиме, но при назначении каждого метода объекта моделирования в другой поток я сталкиваюсь с ошибками сегментации в разное время (обычно очень рано), что, как я полагаю, происходит из-за некоторой гонки данных.Углубившись немного глубже, я обнаружил, что некоторые переменные-члены, похоже, переинициализируются или просто меняют значения (не всегда при каждом запуске).Мне ясно, что некоторые ресурсы перепутаны, но как это может произойти, когда я запускаю каждую симуляцию в независимом потоке (или я так думаю)?

Вот упрощенная версия кода.

Класс моделирования:

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

Что-то не так с этим фрагментом кода?Реальный код довольно сложен, поэтому, по крайней мере, я хотел бы знать, является ли это правильным способом многопоточности различных методов объекта для разных потоков.

Ответы [ 3 ]

0 голосов
/ 08 октября 2018

Наименьшее изменение, которое решает вашу проблему, состоит в том, чтобы построить все симуляции, прежде чем запускать какие-либо потоки.

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;
}
0 голосов
/ 08 октября 2018

Существующие ответы решают простой запрошенный случай, в котором мы заранее знаем количество симуляций.Решение состоит в том, чтобы просто зарезервировать достаточно места в векторе имитации, чтобы такое перераспределение никогда не происходило.

Но что, если число симуляций не было известно, или имитации должны быть добавлены ad-hoc?

Одним из ответов может быть сохранение симуляции в std::list, а не std::vector.Однако затем мы теряем возможность случайного доступа к симуляциям.

Мы могли бы решить эту проблему, внедрив симуляцию в терминах идиомы дескриптор / тело.Дескриптор является подвижным и управляет временем жизни фактической реализации.

Пример Am (в котором я также дал концепцию запущенного моделирования своего собственного класса):

#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).

0 голосов
/ 08 октября 2018

Вы должны быть очень осторожны при работе с адресом 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, предыдущие адреса будут недействительными при увеличении вектора.

...