现在的位置: 首页 > 综合 > 正文

多线程中生成随机数序列重复问题的解决方法

2018年01月25日 ⁄ 综合 ⁄ 共 3021字 ⁄ 字号 评论关闭

 

使用过随机数的程序员都知道在程序中并不能够实现的真正的完全的随机数函数。随机数函数产生的是通过公式计算出来的一系列伪随机数,这个公式会采用一个种子数计算出一个数,而该数将成为产生下一个数的种子数。基于产生随机数的原理,两次调用随机数后产生的随机数序列将是一样的,显然,这不是我们的期望的结果。

为了解决上述问题,使得随机数函数产生的数尽量随机,编程语言通常提供了设定种子数的功能,而一般情况下,程序员会使用当前时间作为种子数,这样两次调用随机函数产生的随机数序列就会不同,进而达到更加随机的效果。

对于C/C++而言,通常产生的随机数的方法是调用以下两个函数:

 

srand(time(NULL));      // 设定随机数种子, 参数内是使用当前时间作为种子

rand();                 // 产生一个随机数

 

由于一般情况下调用srand(time(NULL))的时间不会一样,所以每次产生的随机数序列也会不同,因而可以达到近似完全随机的效果。但是,如果是在多线程的环境下需要使用随机数,情况将变得有所不同。

以下是一个在线程中生成随机数的示例,为了能将生成的随机数显示出来,测试程序肺采用了MFC对话框,并将窗口句柄传入到了线程中,当线程中产生了一个随机数后,以发消息的形式通知对话框显示产生出来的随机数。

 

/* 产生随机数的线程  */

UINT RandThread(LPVOID p)

{

     if(NULL == p)

         return 1;

 

     PTHREADPARA pt = (PTHREADPARA)p;

 

     // 初始化随机种子

     ::srand(time(NULL));

 

     // 生成随机数,并通过消息显示在界面上

     int nRnd;

     for(int i = 1; i <= 20; i++)

     {

         nRnd = ::rand();

         PostMessage(pt->hWind, WM_USER_RAND, pt->nThreadFlag, nRnd);

     }

 

     // 释放线程参数空间

     if(pt)

     {

         delete pt;

         pt = NULL;

     }

 

     return 0;

}

 

在测试程序中,将上述线程启动5次,可以发现,得到的随机数序列是完全一样的。下图是其中一次运行的结果:

 

 线程生成相同的随机数序列

分析其原因,可能是因为5个线程启动的时间相差无几,而不同线程调用rand()函数时相对是独立的,因而产生的随机数序列也就是相同的。

实际上time(NULL)所取得的时间只能精确到秒,而启动5个线程如果在同一秒内完成(经测试,在1.6G单核机器上测试时一秒内启动10个线程也没有问题,能在同一秒内启动的线程数量与机器当前繁忙程度、CUP性能等有很大关系),就必然形成上述生成同一随机数序列的现象。

通过上述分析,如果能令不同线程设定的随机数种子不同,就应该可以令不同线程产生不同的随机数序列。一个容易想到的办法是在相隔较长的时间里启动不同的线程,但是如果要求所有线程在较短的时间内连续启动呢?那我们就只能寻找可以设定不同种子数的办法。

既然time(NULL)的精度只能到秒级,那么我们可以尝试使用时间精度更高的函数来获得一个种子数。

配合使用结构体timeb与函数ftime可以取得毫秒级的时间,它应当可以使得多线程生成同一序列随机数的现状有所改观。修改初始化种子数的代码如下:

 

struct timeb stb;

ftime(&stb);

srand((unsigned)stb.millitm);)

 

重新运行测试程序,可以发现以毫秒级的时间作为种子数依然感觉不够用,部分线程产生的随机数序列依然相同,也就是说在同一毫秒内也可以启动两个以下的线程。那么我们只能继续寻找精度更高的与时间相关的函数。

经查找资料,函数QueryPerformanceCounter可以返回高精度的计数器值,其精度可达微秒级。通常该函数会与函数QueryPerformanceFrequency配合使用,函数QueryPerformanceFrequency的功能是如果当前机器存在定时器则查询出当前机器定时器的频率,我们可以利用QueryPerformanceFrequency测试当前系统里是否有这个高精度的定时器,如果有,则可以调用QueryPerformanceCounter获得一个精度很高的计数值。

利用QueryPerformanceFrequencyQueryPerformanceCounter设定种子数的代码如下:

 

if(::QueryPerformanceFrequency(&nFrequency))

     {

         LARGE_INTEGER nStartCounter;  

         ::QueryPerformanceCounter(&nStartCounter);

         ::srand((unsigned)nStartCounter.LowPart);

}

 

再次运行测试程序,可以发现,每个线程产生的随机数序列已经完全不一样了。下图是其中一次运行的结果:

 

 线程生成不同的随机数序列

以下列出完整的线程代码,供参考。

 

/* 初始化随机种子 */

void InitRand()

{

     // 如果支持高性能精度计数器,则使用其初始化随机种子(微秒级)

     LARGE_INTEGER nFrequency;  

     if(::QueryPerformanceFrequency(&nFrequency))

     {

         LARGE_INTEGER nStartCounter;  

         ::QueryPerformanceCounter(&nStartCounter);

         ::srand((unsigned)nStartCounter.LowPart);

     }

     else // 否则使用当前系统时间初始化随机种子(毫秒级)

     {

         struct timeb stb;

         ::ftime(&stb);

         ::srand((unsigned)stb.millitm);

     }

}

 

/* 产生随机数的线程  */

UINT RandThread(LPVOID p)

{

     if(NULL == p)

         return 1;

 

     PTHREADPARA pt = (PTHREADPARA)p;

 

     // 初始化随机种子

     //::srand(time(NULL));

     InitRand();

 

     // 生成随机数,并通过消息显示在界面上

     int nRnd;

     for(int i = 1; i <= 20; i++)

     {

         nRnd = ::rand();

         PostMessage(pt->hWind, WM_USER_RAND, pt->nThreadFlag, nRnd);

     }

 

     // 释放线程参数空间

     if(pt)

     {

         delete pt;

         pt = NULL;

     }

 

     return 0;

}

 

 

如果需要测试程序完整代码,可以通过QQ联系我。

 

 

 

欢迎访问梦断酒醒的博客:http://www.yanzhijun.com

 

抱歉!评论已关闭.