用于初始化“thread_local”变量的函数调用是否保证是线程安全的?

问题描述 投票:0回答:1

考虑以下

rand_double()
的实现,其中有一个
thread_local
函数变量
seed
,在每个线程中使用
std::random_device{}()
进行初始化:

auto rand_double(double min, double max) {
    thread_local uint64_t seed = std::random_device{}();
    // ^^ Are the calls to std::random_device guaranteed to not interleave?

    /* Generate random `double` and return it... */
}

我了解到静态函数变量的初始化保证是线程安全的。但是,我还没有找到一个来源来说明当使用 function 初始化

static
函数变量时,对该函数的调用是否不会交错。 有人可以确认使用函数来初始化
static
(特别是
thread_local
- 有区别吗?)函数变量是否是线程安全的?

c++ multithreading random static thread-safety
1个回答
0
投票

让我们看看你在做什么:

  1. 您创建一个
    std::random_device
    作为自动(函数局部)变量。这是线程安全的。这是 C++ 标准的标准保证,请参阅第 16.5.5.10 节数据竞争避免
    [res.on.data.races]
    :

2 C++ 标准库函数不得直接或间接访问当前线程以外的线程可访问的对象 (6.9.2),除非通过函数的参数直接或间接访问对象,包括

this

3 C++ 标准库函数不得直接或间接修改当前线程以外的线程可访问的对象 (6.9.2),除非通过函数的非常量参数直接或间接访问对象,包括

this

4 [注意:这意味着,例如,在没有同步的情况下,实现不能将静态对象用于内部目的,因为即使在线程之间未显式共享对象的程序中,它也可能导致数据争用。 — 尾注]

  1. 当前线程使用该随机设备来检索一个种子值。出于同样的原因,这是线程安全的。是的,当随机设备访问操作系统的共享熵池时,在幕后会发生一些同步,但这是库的工作,而不是您的工作。

  2. 您从该种子初始化一个线程本地对象。当然安全啦

  3. 你摧毁了随机设备。和上面一样

您的使用是线程安全的,因为每个线程都使用自己的

std::random_device

现在的问题是,这是一种明智的做事方式吗?我看到了两个问题:

  1. 每个线程使用一个随机设备是相当昂贵的。 那些东西有一些开销
  2. 它让你面临生日问题。两个线程最终可能具有相同的种子值。机会很低(除非您创建大量线程)但存在

这是基于该链接博客文章中的建议的替代方案,即为线程使用顺序种子值。

auto rand_double(double min, double max) {
    static std::atomic<std::uint64_t> process_seed = std::random_device{}();
    thread_local std::uint64_t seed =
          process_seed.fetch_add(1, std::memory_order_relaxed);
}

或者通过不检查线程本地种子之前的

process_seed
来节省大约两条指令和每次调用一次跳转:

auto next_seed()
{
    static std::atomic<std::uint64_t> process_seed = std::random_device{}();
    return process_seed.fetch_add(1, std::memory_order_relaxed);
}
auto rand_double(double min, double max) {
    thread_local std::uint64_t seed = next_seed();
    
}

这应该可以消除生日问题,并且还会降低每个新线程的成本。如果您运行多重处理(例如 MPI),您还应该将进程索引号纳入种子中。

此代码的一个潜在缺点是,使用一个线程的状态,您可以猜测相邻线程的随机状态。这在某些服务器应用程序中可能是一个问题,但对于其他应用程序(例如蒙特卡洛计算)来说,这似乎更合适。

旁注:

std::random_device::result_type
是一个简单的
unsigned int
。你是零扩展的。如果您想要真正的 64 位种子,则需要进行多次调用。像这样的东西:

std::uint64_t random_device_seed()
{
    std::uniform_int_distribution<std::uint64_t> distr; // full range
    std::random_device rng;
    return distr(rng);
}

并且,是的,只是为了澄清:出于与上面讨论的相同原因,像

static auto seed = random_device_seed()
thread_local auto seed = random_device_seed()
这样的东西将是线程安全的。

© www.soinside.com 2019 - 2024. All rights reserved.