C++11 Потокобезопасность генераторов случайных чисел

В C++11 есть несколько новых движков генератора случайных чисел и функций распределения. Они потокобезопасны? Если вы разделяете одно случайное распределение и механизм среди нескольких потоков, безопасно ли это, и вы все равно будете получать случайные числа? Сценарий, который я смотрю, похож на

void foo() {
    std::mt19937_64 engine(static_cast<uint64_t> (system_clock::to_time_t(system_clock::now())));
    std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
#pragma omp parallel for
    for (int i = 0; i < 1000; i++) {
        double a = zeroToOne(engine);
    }
}

используя OpenMP или

void foo() {
    std::mt19937_64 engine(static_cast<uint64_t> (system_clock::to_time_t(system_clock::now())));
    std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
    dispatch_apply(1000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) {
        double a = zeroToOne(engine);
    });
}

используя libdispatch.

3 ответа

Решение

Стандартная библиотека C++11 в целом ориентирована на многопоточность. Гарантии безопасности потока на объектах PRNG такие же, как на контейнерах. Более конкретно, поскольку все классы PRNG являются псевдослучайными, то есть они генерируют детерминированную последовательность, основанную на определенном текущем состоянии, на самом деле нет места, чтобы заглядывать или тыкать во что-либо за пределами содержащегося в нем состояния (что также видно для пользователя).

Так же, как контейнеры нуждаются в блокировках, чтобы сделать их безопасными для совместного использования, вам придется блокировать объект PRNG. Это сделало бы это медленным и недетерминированным. Один объект на поток был бы лучше.

§17.6.5.9 [res.on.data.races]:

1 В этом разделе определяются требования, которым должны соответствовать реализации, чтобы предотвратить гонки данных (1.10). Каждая стандартная библиотечная функция должна отвечать каждому требованию, если не указано иное. Реализации могут предотвратить скачки данных в случаях, отличных от указанных ниже.

2 Функция стандартной библиотеки C++ не должна прямо или косвенно обращаться к объектам (1.10), доступным для потоков, отличных от текущего потока, если к объектам не обращаются прямо или косвенно через аргументы функции, включая это.

3 Функция стандартной библиотеки C++ не должна прямо или косвенно изменять объекты (1.10), доступные для потоков, отличных от текущего потока, если к объектам не обращаются прямо или косвенно через неконстантные аргументы функции, включая this.

4 [Примечание: это означает, например, что реализации не могут использовать статический объект для внутренних целей без синхронизации, потому что это может вызвать гонку данных даже в программах, которые не разделяют объекты явно между потоками. -endnote]

5. Стандартная библиотечная функция C++ не должна обращаться к объектам, косвенно доступным через ее аргументы или через элементы своих аргументов контейнера, за исключением вызова функций, требуемых ее спецификацией для этих элементов контейнера.

6 Операции над итераторами, получаемые путем вызова стандартного библиотечного контейнера или функции-члена строки, могут обращаться к базовому контейнеру, но не должны изменять его. [Примечание: В частности, контейнерные операции, которые делают недействительными итераторы, конфликтуют с операциями на итераторах, связанных с этим контейнером. - конец примечания]

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

8 Если не указано иное, стандартные функции библиотеки C++ должны выполнять все операции исключительно внутри текущего потока, если эти операции имеют видимые для пользователей эффекты (1.10).

9 [Примечание: это позволяет реализациям распараллеливать операции, если нет видимых побочных эффектов. - конец примечания]

Стандарт (хорошо N3242), кажется, не упоминают о том, что генерация случайных чисел свободна от гонки (за исключением того, что rand нет), так что нет (если я что-то пропустил). Кроме того, в действительности нет никакого смысла в том, чтобы они сохраняли потоки, так как это повлекло бы за собой относительно большие издержки (по крайней мере, по сравнению с генерацией самих чисел), без реального выигрыша.

Более того, я не вижу преимущества в том, чтобы иметь один общий генератор случайных чисел, вместо того, чтобы иметь один генератор для каждого потока, каждый из которых слегка инициализируется по-разному (например, из результатов другого генератора или текущего идентификатора потока). В конце концов, вы, вероятно, в любом случае не полагаетесь на генератор, генерирующий определенную последовательность при каждом запуске. Поэтому я бы переписал ваш код как-то так (для openmpбез понятия о libdispatch):

void foo() {
    #pragma omp parallel
    {
    //just an example, not sure if that is a good way too seed the generation
    //but the principle should be clear
    std::mt19937_64 engine((omp_get_thread_num() + 1) * static_cast<uint64_t>(system_clock::to_time_t(system_clock::now())));
    std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
    #pragma omp for
        for (int i = 0; i < 1000; i++) {
            double a = zeroToOne(engine);
        }
    }
}

В документации не упоминается о безопасности потоков, поэтому я предполагаю, что они не являются безопасными для потоков.

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