在线程不安全的函数上使用锁定和复制

时间:2019-01-15 05:30:22

标签: c multithreading thread-safety

我是C和多线程的初学者,对使用锁和在线程不安全的函数上复制有疑问。 我的课本上写着“ 伪随机数生成器是此类线程不安全函数的简单示例

unsigned int next = 1;
int rand(void)
{
   next = next*1103515245 + 12345;
   return (unsigned int)(next/65536) % 32768;
}

void srand(unsigned int seed)
{
   next = seed;
}

我的教科书还说' 锁定和复制方法不适用于依赖于调用之间静态状态的rand()

我不明白,为什么我们不能将rand()重写为:

int rand(void)
{
   P(&mutex);
   next = next*1103515245 + 12345;
   V(&mutex);
   return (unsigned int)(next/65536) % 32768;
}

其中

void P(sem_t *s); /* Wrapper function for sem_wait */
void V(sem_t *s); /* Wrapper function for sem_post */

以便其他线程不会影响当前线程的下一个静态变量,这使其具有线程安全功能吗?

1 个答案:

答案 0 :(得分:0)

我认为问题是next的值不是特定于线程的,因此返回到单个线程的值现在取决于在其他线程中对rand()的调用的顺序。每次调用都是安全的,但是线程仍然会相互干扰。如果要稍后重播仿真,则PRNG(伪随机数生成器)的确定性可能很重要。因此,您会发现像POSIX drand48()这样的函数集提供了线程安全和线程不安全的变体:

double drand48(void);
double erand48(unsigned short xsubi[3]);
long jrand48(unsigned short xsubi[3]);
void lcong48(unsigned short param[7]);
long lrand48(void);
long mrand48(void);
long nrand48(unsigned short xsubi[3]);
unsigned short *seed48(unsigned short seed16v[3]);
void srand48(long seedval);

其中drand48()lrand48()mrand48()不是线程安全的,因为它们使用通过srand48()seed48()设置的公共保存种子值。

函数erand48()jrand48()nrand48()具有线程安全性,因为将种子值(参数列表中的三个16位值)传递给该函数。

另一个功能lcong48()可以激发一切。它会更改种子,并会更改用于生成随机数的系数,从而跨线程工作。

要恢复您的代码,您需要制作rand()的可重入变体。实际上,POSIX定义(但标记为过时的)可重入rand()-rand_r()

int rand_r(unsigned *seed);

每次使用时,您将指向种子的指针传递到函数中。因此,您可以编写自己的变体-rand_ts()(用于“线程安全”):

int rand_ts(unsigned *next)
{
   *next = *next * 1103515245 + 12345;
   return (unsigned int)(*next / 65536) % 32768;
}

现在,您可以传递包含种子值的变量的地址,并且该变量将在每次调用时进行更新。只要传递具有足够持续时间的线程局部变量(通常是在线程中执行的函数堆栈上的变量),每个线程将能够计算独立的随机数序列,且每个序列的确定性线程。

请注意,这种设计无需使用互斥锁。

备用random()生成器具有更复杂的接口和更好的随机性,但是为不同的线程维护独立的随机值序列(并非不可能,但很麻烦且相对较慢)是很麻烦的。实际上,该页面建议如果需要跨多个线程的独立序列(因为接口要简单得多),则建议使用erand48()jrand48()nrand48()

如果仔细阅读,在POSIX页面上的主要描述之后的基本原理和应用程序使用情况信息将非常有用。