Haskell中的随机数生成器是否支持多线程?

15

全局随机数生成器是共享的吗?还是每个线程都有自己的随机数生成器?

如果是共享的,如何确保线程安全?在《Real World Haskell》的"单子"章节中描述的使用getStdGensetStdGen的方法似乎不安全。

如果每个线程都有独立的生成器,那么在快速连续启动两个线程时,它们的生成器会具有不同的种子吗?(例如,如果种子是以秒为单位的时间,则不会,但毫秒可能可以。我不知道如何从Data.Time中获取毫秒分辨率的时间。)

4个回答

14

有一个名为newStdGen的函数,每次调用该函数都会提供一个新的std. gen。 它的实现使用了atomicModifyIORef,因此是线程安全的。

newStdGen不仅在线程安全方面比get/setStdGen更好,还可以防止潜在的单线程错误,例如:let rnd = (fst . randomR (1,5)) <$> getStdGen in (==) <$> rnd <*> rnd.

此外,如果考虑newStdGengetStdGen/setStdGen之间的语义,前者可能非常简单:您只需获得具有随机状态的新std. gen,选择非确定性。另一方面,如果使用get/set配对,则无法抽象出全局程序状态,这对多个原因都很糟糕。


请注意,在底层,这使用与FUZxxl的答案相同的技术; newStdGen的文档指出:“[它]将分裂应用于当前全局随机生成器,使用其中一个结果更新它,并返回另一个”,其实现仅为atomicModifyIORef theStdGen split - Antal Spector-Zabusky

10

我建议你只在主线程中使用getStdGen一次,然后使用split函数生成新的生成器。我会这样做:

创建一个包含生成器的MVar。每当一个线程需要一个新的生成器时,它从MVar中取出当前值,调用split,并将新生成器放回。由于MVar的功能,这应该是线程安全的。


1
实际上,Rotsor的回答中描述的newStdGen操作正是这样做的;它的文档说明:“[它]将split应用于当前全局随机生成器,使用其中一个结果更新它,并返回另一个”,其实现只是atomicModifyIORef theStdGen split - Antal Spector-Zabusky
哇,我不知道有这样的功能。 - fuz

3
< p >单独使用getStdGensetStdGen在某种意义上不是线程安全的。假设两个线程都执行此操作:
do ...
   g <- getStdGen
   (v, g') <- someRandOperation g
   setStdGen g'

线程在另一个线程到达“setStdGen”之前可能会同时运行“g <- getStdGen”这一行,因此它们都可能获得完全相同的生成器。(我错了吗?)
如果它们都获取了相同版本的生成器,并在同一函数中使用它,则会获得相同的“随机”结果。因此,在处理随机数生成和多线程时需要更加小心。有许多解决方案;其中一个解决方案是有一个单独的专用随机数生成器线程,它产生一系列随机数,其他线程可以以线程安全的方式消耗这些随机数。像FUZxxl建议的那样将生成器放入MVar中可能是最简单和最直接的解决方案。
当然,我鼓励您检查代码并确保有必要在多个线程中生成随机数。

2

你可以像FUZxxl的回答一样使用split。但是,不要使用MVar,每当调用forkIO时,只需让分叉线程的IO操作关闭其中一个生成器,并将另一个生成器保留在原始线程中。这样每个线程都有自己的生成器。

正如Dan Burton所说,检查您的代码,看看是否真的需要在多个线程中使用RNG。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接