Состояние гонки или повреждение памяти в C++ std::thread

У меня проблемы с определением точного источника состояния гонки или повреждения памяти. Мои попытки решить проблему показаны после кода.

У меня есть следующая структура:

class A
{
protected:
   // various variables
   // 1. vector that is assigned value on B, C, D constructor and not 
   //   modified while in thread
   // 2. various ints
   // 3. double array that is accessed by B, C, D
   // here that are used by B, C and D
public:
   virtual void execute() = 0;
};

class B : A
{
public:
   B(...){};
   bool isFinished();
   void execute(); //execute does a very expensive loop (genetic algorithm)
}

class C : A
{
public:
   C(...){};
   bool isFinished();
   void execute();
}

class D : A
{
public:
   D(...){};
   bool isFinished();
   void execute();
}

class Worker
{
private:
   A& m_a;
   Container& m_parent;
public:
   // Worker needs a reference to parent container to control a mutex 
   // in the sync version of this code (not shown here)
   Worker(A& aa, Container& parent) : m_a(aa), m_parent(parent) {}
   executeAsynchronous();
}

class Container
{
private:
   std::vector<Worker> wVec;
public:
   addWorker(Worker w); //this does wVec.push_back(w)
   start();
}

void Worker::executeAsynchronous(){
    while(!a.isFinished())
        m_a.execute();
}

void Container::start(){
    std::thread threads[3];
    for (int i=0; i<wVec.size(); i++){
        threads[i] = std::thread(&Worker::executeAsynchronous,
                                                std::ref(wVec[i]));
    }
    for (int i=0; i<wVec.size(); i++){
        threads[i].join();
    }
}

Чтобы запустить код, я бы сделал:

Container container;
B b(...);
C c(...);
D d(...);
Worker worker1(b, container);
Worker worker2(c, container);
Worker worker3(d, container);
container.addWorker(worker1);
container.addWorker(worker2);
container.addWorker(worker3);
container.start();

Код должен порождать потоки для запуска execute() асинхронно, однако, у меня есть следующие 2 проблемы:

  1. Один поток работает быстрее, чем 2, 3 или 4 потока, и дает лучшие результаты (лучшая оптимизация в результате запуска генетического алгоритма в 1 потоке), я читал, что я могу быть ограничен пропускной способностью памяти, но где это происходит? как я могу убедиться, что это так?

  2. Два или более потока: результаты становятся очень плохими, что-то повреждается или искажается по пути. Однако я не могу точно определить это. я имею cout редактируется из разных мест в коде, и каждый поток выполняет ровно один унаследованный класс execute() т.е. каждый поток запускает execute() из B, C or D и не прыгает и не мешает другим. Момент я положил m_parent.mutex.lock() а также m_parent.mutex.unlock() вокруг a.execute(); эффективно делая многопоточный код однопоточным, результаты снова становятся правильными.

Я пытался:

  1. удалить указатели в B, C and D которые могут стать висящими после нажатия Workers обратно в Container вектор. Теперь я передаю копию push_back,
  2. использование emplace_back вместо push_back но это не имело никакого значения
  3. использование vector.reserve() чтобы избежать перераспределения и потери ссылки, но без разницы
  4. использование std::ref() потому что я обнаружил, что std::thread делает копию, и я хочу элемент wVec[i] чтобы быть измененным, ранее я просто проходил wVec[i] в тему.

Я полагаю, что, выполнив 1-4 выше, они не имеют никакого значения, и запустив однопоточный код, и он прекрасно работает, что это не случай, когда что-то выходит за рамки. Также нет обмена данными между потоками или контейнером, я знаю std::vector не потокобезопасен.

Я был бы признателен, если бы вы нашли время, чтобы помочь мне понять это.

РЕДАКТИРОВАТЬ1: Согласно замечанию Константина Пана, вот мой класс RandomNumberGenerator, это статический класс, я называю его, используя RandomNumberGenerator::getDouble(a,b)

//rng.h
class RandomNumberGenerator
{
private:
    static std::mt19937 rng;

public:
    static void initRNG();
    static int getInt(int min, int max);
    static double getDouble(double min, double max);
};

//rng.cpp
std::mt19937 RandomNumberGenerator::rng;

void RandomNumberGenerator::initRNG()
{
    rng.seed(std::random_device()());
}

int RandomNumberGenerator::getInt(int min, int max)
{
    std::uniform_int_distribution<std::mt19937::result_type> udist(min, max);
    return udist(rng);
}

double RandomNumberGenerator::getDouble(double min, double max)
{
    std::uniform_real_distribution<> udist(min, max);
    return udist(rng);
}

EDIT2: я решил проблему коррупции. Это был вызов функции, не поддерживающей потоки, которую я пропустил (функция оценки). Что касается медлительности, программа все еще медленная при запуске в потоках. Я управлял Вальгриндом callgrind и графически результаты, используя gprof2dot и, похоже, предложение M4rc верно. Существует много вызовов контейнера STL, вместо этого я попытаюсь динамически распределять массивы.

РЕДАКТИРОВАТЬ 3: Похоже, что класс RNG был виновником, как указал Константин Пан. Профилированные с использованием gprof

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
 17.97     70.09    70.09  1734468     0.00     0.00  std::mersenne_twister_engine //SYNC
 18.33     64.98    64.98  1803194     0.00     0.00  std::mersenne_twister_engine //ASYNC
  6.19     63.41     8.93  1185214     0.00     0.00  std::mersenne_twister_engine //Single thread

EDIT4: Deque контейнер тоже виноват - M4rc

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
14.15     28.60    28.60 799662660     0.00     0.00  std::_Deque_iterator

1 ответ

Решение

Поскольку существует генетический алгоритм, убедитесь, что генератор случайных чисел является потокобезопасным. Я ударил это (замедление и неправильные результаты) сам в прошлом с rand() от cstdlib,

Другие вопросы по тегам