每个线程是否都有自己的 `rand()` prng 副本?

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

我正在使用一些简单的 C++ 代码来解决有关线程的考试问题,其中线程正在调用

rand()
(是的,我知道有充分的理由使用其他生成器,但希望保持考试问题简单且切中要害)。代码在创建线程之前使用
srand()
来播种 prng,但我惊讶地发现每个线程都从 prng (独立)获取相同的数字序列,就好像它没有被播种(或者更确切地说是播种)默认为
1
)。直到我开始在每个线程中播种 prng 时,顺序才发生变化。

我知道

rand()
不是线程安全的,所以我对线程都获得相同的序列并不感到惊讶(因为我想象生成器需要一些缓存同步来避免这种情况),但令我惊讶的是在创建线程之前在主机进程中播种 prng 似乎对序列没有任何影响。

例如,在对此问题的评论中: 线程,如何独立播种随机数生成器? 据说您不应该“不要在线程中为生成器播种。在启动任何线程之前对其进行播种。您与 rand() 和 srand() 一起使用的生成器对于整个程序来说是唯一的。”

所以我的问题是这样的: 每个线程是否获得生成器的单独副本,或者为什么将其播种到主机进程中不会影响序列?

重现该问题的最小代码示例如下。 我知道这些不是好的种子或最佳实践,但它证明了我在说什么。

当我运行这个时(根本没有播种):

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>

using namespace std;

void test(int id, mutex &channel)
{
    // srand(id);

    channel.lock();
    cout << id << ": " << rand() % 1000 << endl;
    channel.unlock();

    this_thread::sleep_for(chrono::milliseconds(100));

    channel.lock();
    cout << id << ": " << rand() % 1000 << endl;
    channel.unlock();
}

int main()
{
    // srand(5);
    cout << rand() % 1000 << endl;
    cout << rand() % 1000 << endl;

    int n = 4;
    thread threads[n];
    mutex output_channel;

    for (int i = 0; i < n; ++i)
    {
        threads[i] = thread(test, i, ref(output_channel));
    }

    for (int i = 0; i < n; ++i)
    {
        threads[i].join();
    }

    return 0;
}

我明白了:

41
467
0: 41
1: 41
2: 41
3: 41
3: 467
2: 467
0: 467
1: 467

当我运行此命令时(仅播种一次):

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>

using namespace std;

void test(int id, mutex &channel)
{
    // srand(id);

    channel.lock();
    cout << id << ": " << rand() % 1000 << endl;
    channel.unlock();

    this_thread::sleep_for(chrono::milliseconds(100));

    channel.lock();
    cout << id << ": " << rand() % 1000 << endl;
    channel.unlock();
}

int main()
{
    srand(5);
    cout << rand() % 1000 << endl;
    cout << rand() % 1000 << endl;

    int n = 4;
    thread threads[n];
    mutex output_channel;

    for (int i = 0; i < n; ++i)
    {
        threads[i] = thread(test, i, ref(output_channel));
    }

    for (int i = 0; i < n; ++i)
    {
        threads[i].join();
    }

    return 0;
}

我明白了:

54
693
0: 41
1: 41
2: 41
3: 41
2: 467
3: 467
0: 467
1: 467

这让我感到惊讶。 正如您所看到的,线程表现出与没有播种时相同的行为。

当我运行这个(在每个线程中播种)时:

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>

using namespace std;

void test(int id, mutex &channel)
{
    srand(id);

    channel.lock();
    cout << id << ": " << rand() % 1000 << endl;
    channel.unlock();

    this_thread::sleep_for(chrono::milliseconds(100));

    channel.lock();
    cout << id << ": " << rand() % 1000 << endl;
    channel.unlock();
}

int main()
{
    // srand(5);
    cout << rand() % 1000 << endl;
    cout << rand() % 1000 << endl;

    int n = 4;
    thread threads[n];
    mutex output_channel;

    for (int i = 0; i < n; ++i)
    {
        threads[i] = thread(test, i, ref(output_channel));
    }

    for (int i = 0; i < n; ++i)
    {
        threads[i].join();
    }

    return 0;
}

我明白了:

41
467
0: 38
1: 41
2: 45
3: 48
2: 216
3: 196
0: 719
1: 467

这在线程中实现了独立的随机化(并且可以很容易地产生更好的种子等),但我想了解上面目睹的行为的原因。

如果相关的话,我在双核机器(Intel(R) Core(TM) i7-7600U CPU)上的 Windows 10 上运行此程序,并使用 g++ 进行编译。

c++ multithreading random random-seed srand
3个回答
1
投票

有一个方面您忽略了:硬件就是魔法。

您已在一个线程中播种

srand
,并且运行该线程的 CPU 更新了全局变量,这些全局变量已写入该 CPU 的私有 L1 缓存。

然后你的代码产生了新的线程,每个线程都尝试从相同的全局变量中读取。来自内存。从而在写入之前从before读取值。

使用锁进行读取和写入,或

atomic
将使用特殊 CPU 指令强制写入共享缓存/RAM,并使用特殊 CPU 指令强制从共享缓存/RAM 读取数据,但由于您的
rand
实现不是线程安全的(因此这是未定义的行为),编译器没有发出这些指令,因此不同线程读取和写入的值不一致。


0
投票

C 标准中的措辞有点不清楚,但我们似乎不需要

srand()
具有立即的线程间效果。 请注意, [c.math.rand] p2
rand()
srand()
的语义委托给 C,并且 C 表示:

srand 函数使用参数作为新伪随机数序列的种子 通过后续调用 rand 返回。

- 7.22.2.2,第 2 段

后续是一个非正式术语,目前尚不清楚这是否指的是在

调用
rand()之后顺序调用(即在同一线程上后续),还是指
发生在
调用之后的调用srand()(即线程之间的后续)。

srand()

rand()
“不需要避免数据竞争”这一事实来看,可以肯定地说,如果标准库选择不使
srand()
线程安全,
rand()
也不需要有一定的线程间播种效果。
实际上,如果您在同一线程上调用 

srand()

srand(5)
,编译器可能会假设
rand()
将具有
rand()
的种子,而不实际读取 PRNG 状态。 对 PRNG 状态的写入可能会被推迟很长时间。 也有可能对
5
的调用确实写入了所有线程共享的 PRNG 状态,但其他线程在很长一段时间内使用自己的 PRNG 状态缓存值(CPU 缓存或软件缓存),而不是最近写的种子。
简而言之,当线程之间没有同步并且它们共享状态时,事情会变得非常奇怪。


-1
投票
srand(5)

的 LCG 状态。

如果您想避免单一全局状态,请改用 C++ 

rand()

库。您可以将这些生成器对象放置在线程本地存储中。

    

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