Случайный механизм по умолчанию для PRNG в C++ генерирует одинаковые выходные данные для каждого экземпляра класса - правильное начальное число?
Я не имел опыта с генерацией псевдослучайных чисел (PRNG), но недавно я задумался над этим, потому что я хочу протестировать некоторые вещи, и ручная генерация данных трудна и, откровенно говоря, весьма подвержена ошибкам.
У меня есть следующий класс:
#include <QObject>
#include <QList>
#include <QVector3D>
#include <random>
#include <functional>
// TaskCommData is part of a Task instance (a QRunnable).
// It contains all the data required for partially controlling the runnable
// and what it processes inside its run() method
class TaskCommData : public QObject
{
friend class Task;
Q_OBJECT
// Property is used to abort the run() of the Task and also signal the TaskManager that the Task has changed its running status
Q_PROPERTY(bool running
READ isRunning
WRITE setRunningStatus
NOTIFY signalRunningStatusChanged)
public:
QString getId() const; // Task ID
bool isRunning() const;
signals:
void signalRunningStatusChanged(QString id, bool running);
public slots:
void slotAbort();
private:
bool running;
QList<QVector3D> data; // Some data in the form of a list of 3D vectors
QString id;
// PRNG related members
std::default_random_engine* engine;
std::uniform_int_distribution<>* distribution;
std::function<int()> dice;
// Private constructor (don't allow creation of TaskCommData outside the Task class which instantiates the class as its class member
explicit TaskCommData(QString id, QObject *parent = 0);
void setRunningStatus(bool running);
QList<QVector3D>* getData();
void generateData();
};
Этот объект создан и прикреплен к набору QRunnable
s в приложении на основе Qt 5.7. Важные части перечислены ниже:
#include <QDebug>
#include "TaskCommData.h"
// ...
TaskCommData::TaskCommData(QString _id, QObject *parent)
: QObject(parent),
running(false),
id(_id)
{
this->engine = new std::default_random_engine();
this->distribution = new std::uniform_int_distribution<int>(0, 1);
this->dice = std::bind(*this->distribution, *this->engine);
generateData();
}
// ...
void TaskCommData::generateData()
{
QString s;
s += QString("Task %1: Generated data [").arg(this->id);
for(int i = 0; i < 10; ++i) {
this->data.append(QVector3D(dice(), dice(), dice())); // PROBLEM occurs here but it's probably just the aftermath
s += "[" + QString::number(this->data.at(i).x()) + ","
+ QString::number(this->data.at(i).y()) + ","
+ QString::number(this->data.at(i).z()) + "]";
}
s += "]";
qDebug() << s;
}
При инициализации я получаю следующий вывод из qDebug()
(Я создаю 10 экземпляров Task
который создает TaskCommData
- по одному на задание):
"Task task_0: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_0" (sleep: 0)
"Task task_1: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_1" (sleep: 1315)
"Task task_2: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_2" (sleep: 7556)
"Task task_3: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_3" (sleep: 4586)
"Task task_4: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_4" (sleep: 5328)
"Task task_5: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_5" (sleep: 2189)
"Task task_6: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_6" (sleep: 470)
"Task task_7: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_7" (sleep: 6789)
"Task task_8: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_8" (sleep: 6793)
"Task task_9: Generated data [[1,0,0][0,1,0][1,1,0][1,0,1][0,0,1][0,1,1][0,0,0][1,1,1][0,1,1][1,0,1]]"
Added task "task_9" (sleep: 9347)
Как вы могли догадаться, глядя на результаты, я хотел бы иметь больше разнообразия (очевидно, что такое разнообразие невозможно из-за того, что один кусок данных (QVector3D
) содержит 3 двоичных значения) и здесь явно что-то пошло не так.
Вы могли также заметить (sleep: ...)
на выходе. Это выход из моего TaskManager
класс, который создает кучу Task
с и их соответствующие TaskCommData
s:
void TaskManager::initData()
{
// Setup PRNG
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(0,10000); // Between 0 and 10000ms
auto dice = std::bind(distribution, generator);
this->tasks.reserve(this->taskCount);
qDebug() << "Adding" << this->taskCount << "tasks...";
int msPauseBetweenChunks = 0;
for(int taskIdx = 0; taskIdx < this->taskCount; ++taskIdx) {
msPauseBetweenChunks = dice();
Task* task = new Task("task_" + QString::number(taskIdx), msPauseBetweenChunks);
task->setAutoDelete(false);
const TaskCommData *taskCommData = task->getCommData();
// Manage connections
connect(taskCommData, SIGNAL(signalRunningStatusChanged(QString, bool)),
this, SLOT(slotRunningStatusChanged(QString, bool)));
connect(this, SIGNAL(signalAbort()),
taskCommData, SLOT(slotAbort()));
this->tasks.insert(task->getCommData()->getId(), task);
qDebug() << "Added task " << task->getCommData()->getId() << " (sleep: " << msPauseBetweenChunks << ")";
}
emit signalCurrentlyRunningTasks(this->tasksRunning, this->taskCount);
}
Здесь у меня есть то же самое (хотя и не как член класса), и это работает (диапазон отличается, но все же).
Первоначально у меня был тот же фрагмент кода (тот, который связан с генерацией случайных чисел; TaskManager::initData()
) внутри моего void TaskCommData::generateData()
то есть движок, дистрибутив и таймер были в стеке и уничтожены, когда они вышли за пределы области видимости. Результат был тем же - повторять один и тот же набор случайных чисел снова и снова.
Тогда я решил, что проблема исходит от семени (отсутствие может быть более подходящим в качестве описания здесь). Поэтому я изменил свой код на:
// ...
std::chrono::nanoseconds nanoseed = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch());
qDebug() << "Setting PRNG engine to seed" << nanoseed.count();
this->engine = new std::default_random_engine();
this->engine->seed(nanoseed.count());
this->distribution = new std::uniform_int_distribution<int>(0, 1);
this->dice = std::bind(*this->distribution, *this->engine);
generateData();
// ...
Я получаю немного лучший результат:
Setting PRNG engine to seed 1473233571281947000
"Task task_0: Generated data [[1,0,0][0,1,1][0,0,0][0,1,1][1,0,0][1,0,0][0,0,1][1,1,1][1,0,0][1,0,0]]"
Added task "task_0" (sleep: 0 )
Setting PRNG engine to seed 1473233571282947700
"Task task_1: Generated data [[1,0,1][1,0,0][1,0,1][0,0,1][1,1,0][0,0,1][0,0,1][0,1,0][0,1,0][0,1,0]]"
Added task "task_1" (sleep: 1315 )
Setting PRNG engine to seed 1473233571282947700
"Task task_2: Generated data [[1,0,1][1,0,0][1,0,1][0,0,1][1,1,0][0,0,1][0,0,1][0,1,0][0,1,0][0,1,0]]"
Added task "task_2" (sleep: 7556 )
Setting PRNG engine to seed 1473233571283948400
"Task task_3: Generated data [[0,0,1][1,0,1][0,1,1][1,1,1][1,0,0][0,0,0][0,0,1][1,1,0][0,1,1][0,0,1]]"
Added task "task_3" (sleep: 4586 )
Setting PRNG engine to seed 1473233571283948400
"Task task_4: Generated data [[0,0,1][1,0,1][0,1,1][1,1,1][1,0,0][0,0,0][0,0,1][1,1,0][0,1,1][0,0,1]]"
Added task "task_4" (sleep: 5328 )
Setting PRNG engine to seed 1473233571284950700
"Task task_5: Generated data [[0,0,0][1,1,0][0,0,1][0,0,1][0,1,1][1,0,0][1,0,0][1,0,1][0,0,0][0,0,0]]"
Added task "task_5" (sleep: 2189 )
Setting PRNG engine to seed 1473233571284950700
"Task task_6: Generated data [[0,0,0][1,1,0][0,0,1][0,0,1][0,1,1][1,0,0][1,0,0][1,0,1][0,0,0][0,0,0]]"
Added task "task_6" (sleep: 470 )
Setting PRNG engine to seed 1473233571285950800
"Task task_7: Generated data [[0,0,0][1,0,0][0,1,1][1,0,0][1,0,1][0,1,0][1,0,1][0,1,0][1,1,0][0,0,1]]"
Added task "task_7" (sleep: 6789 )
Setting PRNG engine to seed 1473233571285950800
"Task task_8: Generated data [[0,0,0][1,0,0][0,1,1][1,0,0][1,0,1][0,1,0][1,0,1][0,1,0][1,1,0][0,0,1]]"
Added task "task_8" (sleep: 6793 )
Setting PRNG engine to seed 1473233571286950900
"Task task_9: Generated data [[1,0,1][1,1,1][1,0,0][1,1,0][0,1,1][0,0,0][1,0,1][1,0,1][0,0,0][1,0,1]]"
Added task "task_9" (sleep: 9347 )
хотя повторений еще слишком много (создается впечатление, что генерируются одни и те же пары данных). Это также имеет огромный недостаток, связанный с тем, насколько быстро TaskCommData
Объект создан и что происходит между созданием двух экземпляров этого класса. Чем быстрее создание, тем меньше разница, измеренная с std::chrono::system_clock::now()
). Это не похоже на хороший способ создать семя (конечно, я могу ошибаться:D).
Есть идеи, как решить эту проблему? Даже если проблема в семени, я до сих пор не понимаю, почему в TaskManager::initData()
все работает просто отлично, а здесь не так много.
1 ответ
Итак, да, первый случай верен: если вы заполняете все PRNG одинаковыми (по умолчанию) начальными значениями, они должны создавать одинаковую последовательность чисел. Вот для чего они предназначены.
Во втором случае, когда вы используете начальное время, вы замечаете, что это тоже не работает, потому что вы на самом деле получаете только три различных начальных значения, и вы также замечаете, что это не удивительно, так как разные семена образуются примерно в одно и то же время. Итак, это еще одна иллюстрация того, почему семена на основе времени плохие, часто. Я, честно говоря, не знаю, почему мы до сих пор этому учим. Случаи, когда посев на основе времени является хорошей идеей, на самом деле очень редки… если я об этом думаю, как только вам нужно что-то, что на самом деле непредсказуемо со стороны. Если вам не нужно быть фактически непредсказуемым, подойдет любое статическое начальное число.
Итак, вот и все: как насчет простого использования номера вашей задачи в качестве начального числа? Таким образом, вы гарантированно получите столько разных последовательностей PRN, сколько у вас есть задач. Если вам нужно иметь разные значения в разных запусках, вы все равно можете сначала взять одно случайное число, основанное на времени (или, что еще лучше: спросите у вашей ОС случайное число!) И добавить к нему номера задач, снова давая вам гарантированно- последовательности, которые должны быть разными.
¹ основанный на времени посев был проблемой безопасности, стоящей за множеством несанкционированных обращений. Типичный пример: некоторые системы управления процессами, подключенные к Интернету, имеют веб-интерфейс, в который необходимо войти. Затем вы получаете cookie с секретным идентификатором сессии. Единственная проблема состоит в том, что этот идентификатор сеанса является просто случайным числом, подпадающим под известный "строковый преобразователь", и RNG был засеян со временем в момент, когда фактический пользователь вошел в систему. Поскольку зачастую легко определить время устройства и легко угадайте временной интервал, в котором мог произойти такой вход в систему, этот идентификатор сеанса далек от секрета и часто мог быть взломан с помощью очень небольшого количества попыток.