多线程c程序中的随机函数

Random function in multi-threaded c program

请查看整个问题

我知道 srand() 应该只调用一次,但我的第二个代码段显示这并不能解决问题!!!!

我编写的程序正在为我提供输出,但我不太明白为什么会这样。代码段的不同改变给出不同的输出。

Objective 代码:
该代码使用 omp 简单地 运行 一段代码用于 3 个线程。每个线程必须使用 rand() 函数打印 3 个随机值。因此,总共会产生 9 个输出。线程 0 是主线程/主程序的 运行 流程。 Thread 1 和 Thread 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



问题和我的疑惑:

所以,

请帮助我理解这整件事..

原因是 time() 具有秒精度,因此每个线程都使用相同的种子调用 srand(),从而导致相同的伪随机数序列。

只需在程序开始时调用 srand() 一次,而不是在每个线程中,这将使程序的每个 运行 生成 3 个不同的序列,每个线程一个。

已更新:插入了对 OP 列举问题的直接回答。

What is actually happening here?

虽然某些版本的rand()函数在某种意义上可能是"thread safe",但没有理由相信或期望在没有任何外部存储器同步的情况下,多个rand() 由不同线程执行的调用将与由一个线程执行的相同数量的调用返回的一组值相同。特别是,rand() 维护在每次调用时修改的内部状态,并且在没有任何内存同步的情况下,一个线程看不到其他线程对该内部状态执行的更新是完全合理的。在这种情况下,两个或多个线程可能会生成部分或全部相同的数字序列。

How come the placement of the srand() function make a difference only to the main thread (thread 0)?

唯一可以肯定的是,如果srand()在并行块之外,那么它只被主线程执行,而如果在并行块内部,则由每个线程单独执行线。由于您的代码未正确同步,因此无法从源代码中预测每种情况的影响,因此我接下来的评论大多是推测性的。

假设 time(),其(仅)一秒的精度,returns 每个线程中的相同值,将 srand() 放在并行区域中确保每个线程看到相同的初始随机数种子。如果他们随后看不到彼此的更新,那么他们将生成相同的伪随机数序列。但是请注意,您既不能安全地依赖线程看到彼此的更新,也不能安全地依赖它们而不是看到彼此的更新。

但是,如果将 srand() 放在并行区域之外,使其仅由主线程执行,则还有其他可能性。如果OMP维护了一个线程池,其成员在你进入并行段之前就已经启动了,那么可能是线程1和2根本看不到线程0的srand()调用的效果,因此都继续进行默认种子。还有其他的可能性。

Why is it that either ways the the other 2 new threads always output same random number for the respective call to rand()?

无法肯定地说。然而,我倾向于猜测,none 个相关线程看到彼此对 rand() 内部状态的更新。

How is this srand() and rand() even linked, to cause this abnormality?

这两个功能密切相关。 srand() 的目的是修改 rand() 的内部状态(到 "seed" 它,因此 "srand" 中的 "s"),以启动伪造- 它在不同的(但仍然是确定性的)点生成的随机数序列。


这个问题的解决方法与任何涉及多线程访问共享变量的问题的解决方法相同:通过应用同步。在这种情况下,最直接的同步形式可能是使用互斥体保护 rand() 调用。由于这是 OMP 代码,您最好的选择可能是使用 OMP 锁来实现互斥锁,因为将显式 pthreads 对象与 OMP 声明混合似乎很冒险。

看起来,在您的平台上,rand() 是线程安全的,因为每个线程都有自己的 PRNG,该 PRNG 在创建线程时播种。您可以通过首先为每个线程生成一个种子然后让每个线程在调用 rand 之前使用其种子调用 srand 来让这个平台执行您想要的操作。但这可能会在其他具有不同行为的平台上中断,所以你不应该使用 rand.

随机数生成器实际上并不是那么随机。他们采用一些内部状态("seed"),确定性地从该状态中提取一个整数,并确定性地改变状态,以便在下一次调用时它会有所不同。

通常,涉及的计算是复杂的位操作,旨在保证输出序列 "looks" 随机,在可能的范围内均匀分布,并满足其他要求。但从根本上说,它是全局内部状态的确定性函数。如果没有复杂的计算,它与此没有太大区别:

# File: not_so_random.c
static unsigned seed = 1;
void srand(unsigned newseed) { seed = newseed; }
int  rand(void)              { return seed++; }

([注1])

很容易看出如果它在并行线程中执行会如何产生竞争条件。

您可以通过使 seed 原子化来使此 "kind of" 多线程安全。即使突变比原子增量更复杂,使访问原子化将确保下一个种子是 some 调用 rand 所产生的突变的结果。尽管如此,竞争条件仍然是可能的:两个线程可以同时获取种子,然后它们将收到相同的随机数。其他奇怪的行为也是可能的,包括一个线程两次获得相同的随机数,甚至更早的一个。如果 srandrand 同时被调用,则可能会出现特别奇怪的行为,因为这始终是竞争条件。

另一方面,您可以使用互斥锁保护对 randsrand 的所有调用,只要在线程开始之前调用 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;
}

备注:

  1. 虽然该示例大致与该主题上著名的 xkcd 一样随机,但以下是直接从 rand 实施(稍作编辑)的示例C 标准(§7.22.2 para.5),被判断为 rand 的 "sufficiently-random" 实现。与我的示例的相似之处显而易见。

    /* 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; }
    
  2. C 标准和 Posix 都不要求 rand 是线程安全的,但也不禁止。标准 C 库互斥锁的 Gnu 实现保护 rand()srand()。但显然这不是 OP 使用的 rand 实现,因为 glibc 的 rand() 产生了更大的随机数。

  3. 如果您的系统没有rand_r,您可以对上面注释1中的示例代码进行简单修改:

    int rand_r(unsigned *seedp) {
        *seedp = *seedp * 1103515245 + 12345;
        return((unsigned)(*seedp/65536) % 32768);
    }
    
  4. 如果你的OS没有提供/dev/urandom,那么很有可能你的OS是Windows,这样的话你可以使用rand_s生成一次性种子。