Состояние гонки или повреждение памяти в 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 проблемы:
Один поток работает быстрее, чем 2, 3 или 4 потока, и дает лучшие результаты (лучшая оптимизация в результате запуска генетического алгоритма в 1 потоке), я читал, что я могу быть ограничен пропускной способностью памяти, но где это происходит? как я могу убедиться, что это так?
Два или более потока: результаты становятся очень плохими, что-то повреждается или искажается по пути. Однако я не могу точно определить это. я имею
cout
редактируется из разных мест в коде, и каждый поток выполняет ровно один унаследованный классexecute()
т.е. каждый поток запускаетexecute()
изB, C or D
и не прыгает и не мешает другим. Момент я положилm_parent.mutex.lock()
а такжеm_parent.mutex.unlock()
вокругa.execute();
эффективно делая многопоточный код однопоточным, результаты снова становятся правильными.
Я пытался:
- удалить указатели в
B, C and D
которые могут стать висящими после нажатияWorkers
обратно вContainer
вектор. Теперь я передаю копиюpush_back
, - использование
emplace_back
вместоpush_back
но это не имело никакого значения - использование
vector.reserve()
чтобы избежать перераспределения и потери ссылки, но без разницы - использование
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
,