请看整个问题
我知道srand()
应该只被调用一次,但是我的第二个代码段显示这不能解决问题!!!!
我写的程序给了我输出,我不能弄清楚为什么会这样。代码段的不同更改会产生不同的输出。
代码目标:
代码使用omp
来简单地为3个线程运行一段代码。每个线程必须使用rand()
函数打印3个随机值。因此,共有9项产出。线程0
是主线程/主程序的运行流程。线程1
和线程2
是在线程代码开头创建的新线程
代码:
#include<omp.h>
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
#pragma omp parallel num_threads(3)
{
srand(time(NULL));
int i=0;
for(i=0;i<3;i++)
{
printf("\nRandom number: %d by thread %d", rand(), omp_get_thread_num());
}
}
return 0;
}
输出:
Random number: 17105 by thread 0
Random number: 30076 by thread 0
Random number: 21481 by thread 0
Random number: 17105 by thread 1
Random number: 30076 by thread 1
Random number: 21481 by thread 1
Random number: 17105 by thread 2
Random number: 30076 by thread 2
Random number: 21481 by thread 2
但是如果我在线程代码之前保留srand(time(NULL))
,那么
srand(time(NULL));
#pragma omp parallel num_threads(3)
{
int i=0;
......
......(rest is same)
输出是,
输出:
Random number: 16582 by thread 0
Random number: 14267 by thread 0
Random number: 14030 by thread 0
Random number: 41 by thread 1
Random number: 18467 by thread 1
Random number: 6334 by thread 1
Random number: 41 by thread 2
Random number: 18467 by thread 2
Random number: 6334 by thread 2
问题和我的怀疑:
所以,
请帮我理解这一切......
答案 0 :(得分:6)
已更新:已插入OP的枚举问题的直接答案。
这里到底发生了什么?
虽然某些版本的rand()
函数在某种意义上可能是“线程安全的”,但没有理由相信或期望在没有任何外部内存同步的情况下,多个{{1}返回的值集合由不同线程执行的调用将与由一个线程执行的相同数量的调用返回的值集相同。特别是,rand()
维护在每次调用时修改的内部状态,并且没有任何内存同步,一个线程看不到由其他线程执行的内部状态的更新是完全合理的。在这种情况下,两个或多个线程可能会生成部分或完全相同的数字序列。
为什么
rand()
函数的位置只对主线程(线程srand()
)产生影响?
唯一可以肯定的是,如果0
在并行块之外,则它仅由主线程执行,而如果它在内部则由每个线程单独执行。由于您的代码未正确同步,因此源代码无法预测每种情况的影响,因此我的下一条评论大多是推测性的。
假设srand()
及其(仅)一秒精度在每个线程中返回相同的值,将time()
置于并行区域内可确保每个线程看到相同的初始随机数种子。如果他们没有看到彼此的更新,那么他们将生成相同的伪随机数序列。但请注意,您既不能安全地依赖线程看到彼此的更新,也不能安全地依赖它们而不是看到彼此更新。
但是,如果将srand()
置于并行区域之外,使其仅由主线程执行,则还有其他可能性。如果OMP维护一个线程池,其成员已经在您进入并行部分之前已经启动,则可能是线程1和2根本无法看到线程0的srand()
调用的影响,因此两者都继续默认种子。还有其他可能性。
为什么其他2个新线程总是为相应的
srand()
调用输出相同的随机数?
不可能肯定地说。但是,我倾向于猜测,所涉及的线程都没有看到彼此对rand()
内部状态的更新。
这
rand()
和srand()
如何联系,导致这种异常?
这两个功能密切相关。 rand()
的目的是修改srand()
的内部状态(“种子”它,因此“srand”中的“s”),以便启动它生成的伪随机数序列在另一个(但仍然是确定的)点。
这个问题的解决方法与解决涉及多线程访问共享变量的任何问题的方法相同:通过应用同步。在这种情况下,最直接的同步形式可能是使用互斥锁来保护rand()
个调用。由于这是OMP代码,您最好的选择可能是使用OMP锁来实现互斥锁,因为将显式pthreads对象与OMP声明混合似乎很冒险。
答案 1 :(得分:3)
随机数生成器实际上不是那么随机。它们采用一些内部状态(“种子”),确定性地从该状态中提取一个整数,并确定性地改变状态,以便在下次调用时它会有所不同。
通常,计算涉及复杂的位操作,旨在保证输出序列“看起来”随机,在可能的范围内均匀分布,并满足其他要求。但在基础上,它是全球内部状态的确定性函数。如果没有复杂的计算,它就不会有太大的不同:
# File: not_so_random.c
static unsigned seed = 1;
void srand(unsigned newseed) { seed = newseed; }
int rand(void) { return seed++; }
([注1])
如果它是在并行线程中执行的话,很容易看出它会产生竞争条件。
你可以通过seed
原子使这种“有点”的多线程安全。即使突变比原子增量更复杂,使得访问原子将确保下一个种子是某些调用rand
所做的突变的结果。仍然存在竞争条件:两个线程可以同时拾取种子,然后它们将接收相同的随机数。其他奇怪的行为也是可能的,包括线程获得两次相同的随机数,甚至更早的行为。如果srand
与rand
同时被调用,则可能出现奇怪的行为,因为这始终是竞争条件。
另一方面,您可以使用互斥锁保护对rand
和srand
的所有呼叫,只要在线程开始之前调用srand
,就可以避免所有竞争条件。 (否则,在一个线程中对srand
的任何调用都将重置每个其他线程中的随机数序列。)但是,如果多个线程同时消耗大量随机数,您将看到很多互斥争用并且可能同步文物。 [注2]。
在多处理世界中,依赖于全局状态的库函数不是那么好,并且许多旧接口具有多线程安全的替代方案。 Posix需要rand_r
,它类似于rand
,除了它期望作为参数的种子变量的地址。使用此接口,每个线程可以简单地使用自己的种子,并且线程将有效地具有独立的随机数生成器。 [注3]
当然,这些种子必须以某种方式进行初始化,将它们全部初始化为相同的值显然会适得其反,因为这会导致每个线程获得相同的随机数序列。
在此示例代码中,我使用系统/dev/urandom
设备为每个线程提供一些种子字节。 /dev/urandom
由操作系统实现(或至少by many OSs);它产生一个高度随机的字节流。通常,通过混合随机事件(例如键盘中断的时间)来增强该流的随机性。生成随机数是一种适度昂贵的方法,但它会生成相当好的随机数。因此,这对于为每个线程生成随机种子是完美的:我希望种子是随机的,而且我不需要很多种子。 [注4]
所以这是一个可能的实现:
#define _XOPEN_SOURCE
#include<omp.h>
#include<stdio.h>
#include<stdlib.h>
// This needs to be the maximum number of threads.
// I presume there is a way to find the correct value.
#define THREAD_COUNT 3
// Hand-built alternative to thread-local storage
unsigned int seed[THREAD_COUNT];
int main() {
FILE* r = fopen("/dev/urandom", "r");
if (fread(seed, sizeof seed, 1, r) != 1) exit(1);
fclose(r);
#pragma omp parallel num_threads(3)
{
// Get the address of this thread's RNG seed.
int* seedp = &seed[omp_get_thread_num()];
int i=0;
for(i=0;i<3;i++) {
printf("Random number: %d by thread %d\n",
rand_r(seedp), omp_get_thread_num());
}
}
return 0;
}
虽然该示例与该主题上的着名xkcd大致相同,但以下是直接来自C标准的rand
实现(带有少量编辑)的示例( §7.22.2第5段),被认为是rand
的“足够随机”的实现。与我的例子的相似之处是显而易见的。
/* RAND_MAX assumed to be 32767. */
static unsigned long next = 1;
int rand(void) {
next = next * 1103515245 + 12345;
return((unsigned)(next/65536) % 32768);
}
void srand(unsigned seed) { next = seed; }
C标准和Posix都不要求rand
是线程安全的,但也不禁止。标准C库互斥锁的Gnu实现可以保护rand()
和srand()
。但显然这不是OP使用的rand
实现,因为glibc
的rand()会产生更大的随机数。
如果您的系统没有rand_r
,您可以使用上面注1中的示例代码进行简单修改:
int rand_r(unsigned *seedp) {
*seedp = *seedp * 1103515245 + 12345;
return((unsigned)(*seedp/65536) % 32768);
}
如果您的操作系统未提供/dev/urandom
,则您的操作系统很可能是Windows,在这种情况下,您可以使用rand_s
生成一次性种子。
答案 2 :(得分:2)
原因是time()
具有秒精度,因此每个线程使用相同的种子调用srand()
,导致相同的伪随机数序列。
只需在程序开始时调用srand()
,而不是在每个线程中,这将使程序的每次运行为每个线程生成3个不同的序列。
答案 3 :(得分:1)
看起来,在您的平台上,rand()
是线程安全的,因为每个线程都有自己的PRNG,在创建线程时播种。您可以通过首先为每个线程生成种子然后让每个线程在调用srand
之前使用其种子调用rand
来使此平台完成您想要的操作。但是,这可能会在具有不同行为的其他平台上中断,因此您不应该使用rand
。