我正在使用一些简单的 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++ 进行编译。
有一个方面您忽略了:硬件就是魔法。
您已在一个线程中播种
srand
,并且运行该线程的 CPU 更新了全局变量,这些全局变量已写入该 CPU 的私有 L1 缓存。
然后你的代码产生了新的线程,每个线程都尝试从相同的全局变量中读取。来自内存。从而在写入之前从before读取值。
使用锁进行读取和写入,或
atomic
将使用特殊 CPU 指令强制写入共享缓存/RAM,并使用特殊 CPU 指令强制从共享缓存/RAM 读取数据,但由于您的 rand
实现不是线程安全的(因此这是未定义的行为),编译器没有发出这些指令,因此不同线程读取和写入的值不一致。
C 标准中的措辞有点不清楚,但我们似乎不需要
srand()
具有立即的线程间效果。
请注意, [c.math.rand] p2 将 rand()
和 srand()
的语义委托给 C,并且 C 表示:
srand 函数使用参数作为新伪随机数序列的种子 通过后续调用 rand 返回。
后续是一个非正式术语,目前尚不清楚这是否指的是在
调用
rand()
之后顺序调用(即在同一线程上后续),还是指发生在调用之后的调用
srand()
(即线程之间的后续)。从srand()
和
rand()
“不需要避免数据竞争”这一事实来看,可以肯定地说,如果标准库选择不使srand()
线程安全,rand()
也不需要有一定的线程间播种效果。实际上,如果您在同一线程上调用 srand()
和
srand(5)
,编译器可能会假设 rand()
将具有 rand()
的种子,而不实际读取 PRNG 状态。
对 PRNG 状态的写入可能会被推迟很长时间。
也有可能对 5
的调用确实写入了所有线程共享的 PRNG 状态,但其他线程在很长一段时间内使用自己的 PRNG 状态缓存值(CPU 缓存或软件缓存),而不是最近写的种子。简而言之,当线程之间没有同步并且它们共享状态时,事情会变得非常奇怪。
srand(5)
的 LCG 状态。
如果您想避免单一全局状态,请改用 C++rand()
库。您可以将这些生成器对象放置在线程本地存储中。