c++ 一个`std::mt19937`静态函数变量是线程安全的吗?

s71maibg  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(173)

我有一个函数rand_double(),它使用类型为std::mt19937的 *static函数变量 * 生成一个随机浮点值。多个线程可以并发使用这个函数。

#include <random>

auto random_double(double min, double max) {
    static std::mt19937 generator{std::random_device{}()}; // Forgot to seed it, thanks @user4581301
    return std::uniform_real_distribution<>{min, max}(generator);
}

字符串
我的第一个问题是,上面的代码是线程安全的吗?我知道从C++11开始,静态函数变量的 * 初始化 * 总是线程安全的,但是并发使用生成器怎么样?

vptzau2j

vptzau2j1#

为了生成一个随机数,std::uniform_real_distribution调用参数生成器的operator()
mersenne twister的情况下,运算符具有副作用:
“发动机状态提前一个位置”
这不被(任何指示器)认为是线程安全操作。
您可以将其用作thread_local,这将在每个特定线程中产生预期的效果,即static

auto random_double(double min, double max) {
    thread_local std::mt19937 generator(std::random_device{}());
    return std::uniform_real_distribution<>{min, max}(generator);
}

字符串

px9o7tmv

px9o7tmv2#

thread_local是要走的路

OP问:
std::mt19937静态函数变量是线程安全的吗?
正如注解中所指出的,在std::mt19937对象上调用operator()不是线程安全的。
对于需要多次调用std::mt19937的应用程序,最好将引擎声明为thread_local,这样每个线程都将获得自己的随机数引擎示例。

使用随机数分布的param_type

通过使std::uniform_real_distribution thread_local也可以稍微加快速度,这样就避免了每次生成随机数时不必要的构造函数调用。
要做到这一点,在生成随机数时,必须向uniform_real_distribution提供param_type对象以及std::mt19937对象。
在下面的函数中,param_type{ min, max }构造了一个临时param对象,并用minmax将其重命名。

auto random_double(double const min, double const max)
{
    thread_local std::mt19937 mt{ std::random_device{}() };
    thread_local std::uniform_real_distribution<double> dist;
    using param_type = std::uniform_real_distribution<double>::param_type;
    return dist(mt, param_type{ min, max });
}

字符串
这是对OP问题最直接的解决办法。
作为替代方案,您可以通过在调用函数random_double之前构造param对象来加快速度,并将其重用于多次调用。

using param_type = std::uniform_real_distribution<double>::param_type;

auto random_double(param_type const& p)
{
    thread_local std::mt19937 mt{ std::random_device{}() };
    thread_local std::uniform_real_distribution<double> dist;
    return dist(mt, p);  // p is passed by const& straight through to distribution function.
}

void some_function()
{
    // Construct the `param` object once, ...
    param_type p{ -100.0, +100.0 };

    // and use it many times.
    for (int n{ 10 }; n--; )
        std::cout << "   " << random_double(p);
    std::cout.put('\n');
}

如果minmax是常数会怎样?

如果minmax永远不会改变,那么可以通过硬编码来提高效率。这个版本的函数random_double避免了传入参数minmax的开销。对于下面的示例,我将它们分别设置为1.010.0

auto random_double()
{
    thread_local std::mt19937 mt{ std::random_device{}() };
    thread_local std::uniform_real_distribution<double> dist{ 1.0, 10.0 };
    return dist(mt);
}

如果需要闭合区间,请使用std::next_after

C++标准要求std::uniform_real_distribution对象返回的随机数均匀分布在半开区间上,比如[a,b)。因此,它们大于或等于a,小于(但永远不等于)b
要创建闭区间[a,b]上的分布,std::nextafter(b, std::numeric_limits<RealType>::max())可以用作分布对象的构造函数的第二个参数。调用函数paramoperator()调用函数也是如此。

使用std::random_device播种std::mt19937的最佳方法

std::mt19937std::19937_64都有19,968位的状态,所以试图用一个只有32或64位的种子来播种它们是愚蠢的。
为了方便播种所有19,968位的状态,我编写了类seed_seq_rdseed_seq_rd代表 seed-sequence using random-device。这个类模仿std::seed_seq的接口,但使用std::random_device来生成种子。这种类型的对象是种子序列,可以用作随机数引擎中成员函数seed的参数。

// Example: Seed mt19937 with random seeds from std::random_device.
tbx::seed_seq_rd s;
std::mt19937 mt;
mt.seed( s );


seed_seq_rd对象也可以用作随机数引擎的构造函数的参数,以及任何可以使用std:seed_seq对象的地方。

// Example: Seed mt19937_64 at the time of its construction.
tbx::seed_seq_rd s;
std::mt19937_64 mt64{ s };


seed_seq_rd的源代码在single-file header on GitHub中可用。它适用于任何可以用种子序列播种的随机数引擎,包括标准库中的所有引擎。
seed_seq_rd在头文件tbx.cpp14.seed_randomly.h中定义。(它只使用C++14的特性,后来没有使用。因此得名。)使用它来播种std::mt19937,将使您的函数更改为如下内容。

// main.cpp
#include <iostream>
#include <random>
#include "tbx.cpp14.seed_randomly.h"

auto random_double(double const min, double const max)
{
    thread_local tbx::seed_seq_rd s;  // uses only 4 bytes of storage!
    thread_local std::mt19937 mt{ s };
    thread_local std::uniform_real_distribution<double> dist;
    using param_type = std::uniform_real_distribution<double>::param_type;
    return dist(mt, param_type{ min, max });
}

int main()
{
    for (int n{ 10 }; n--; )
        std::cout << "   " << random_double(-5.0, +5.0);
    std::cout.put('\n');
    return 0;
}
// end file: main.cpp


输出量:

-1.95065   4.29782   2.23657   -1.3178   4.73166   -2.22954   -3.14094   4.15405   0.847722   2.52301

相关问题