如何制作一个C#线程安全的随机数生成器

5

我在我的代码中有一个循环

Parallel.For(0, Cnts.MosqPopulation, i => { DoWork() });

然而在 DoWork() 函数中,有多个对随机数生成器的调用,其定义如下:

public static class Utils
{
    public static readonly Random random = new Random();

}

这是静态实例,因此仅种子一次即可。我可以在整个代码中使用它。

根据MSDN和其他stackoverflow线程,这不是线程安全的。事实上,我注意到有时我的代码会出错,随机数生成器开始生成全零(根据MSDN文档)。

还有其他stackoverflow线程,但它们相当古老,而且实现速度很慢。由于程序是科学计算并运行数百个模拟,我不能浪费时间来生成数字。

自2.0以来,我就没有使用过.net了,我不确定语言如何发展才能使RNG快速、高效、线程安全。

以下是以前的线程:

C#随机数生成器是否线程安全?

多线程应用程序中使用Random的正确方法

C#的快速线程安全随机数生成器

注意:由于我需要快速实现,所以无法使用相当慢的RNGCryptoServiceProvider。
注意2:我没有最小的工作代码。我甚至不知道从哪里开始,因为我不知道线程安全如何工作,也没有高级的c#知识。因此,看起来像是我正在寻求完整的解决方案。

你为什么不适用你链接的帖子中的解决方案呢?有特殊原因吗? - Dirk Vollmar
1
那这个怎么样:https://dev59.com/0GIk5IYBdhLWcg3wbtyb#19271062 - Dirk Vollmar
@DirkVollmar 评论提到如果线程创建得太快,可能会生成相同的随机数。在我的代码中这是非常可能的,因为DoWork()非常简单 - 只有几个if语句。 - masfenix
我认为该注释无效,因为每个线程都使用自己的种子。 - Dirk Vollmar
1
这个答案怎么样? https://dev59.com/AHE95IYBdhLWcg3wkekf#2261892 它链接到https://codeblog.jonskeet.uk/2009/11/04/revisiting-randomness/ - dbc
显示剩余3条评论
5个回答

8

使用 ThreadStatic 属性和自定义 getter,您可以获得每个线程的单个 Random 实例。如果这不可接受,则使用锁。

public static class Utils
{
    [ThreadStatic]
    private static Random __random;

    public static Random Random => __random??(__random=new Random());
}
ThreadStatic 属性不会在每个线程上运行初始化程序,因此您需要在访问器中负责执行初始化。此外,请考虑您的种子初始化程序,您可以使用类似以下代码:
new Random((int) ((1+Thread.CurrentThread.ManagedThreadId) * DateTime.UtcNow.Ticks) )

2

我会考虑类似这样的东西:

private static int _tracker = 0;

private static ThreadLocal<Random> _random = new ThreadLocal<Random>(() => {
    var seed = (int)(Environment.TickCount & 0xFFFFFF00 | (byte)(Interlocked.Increment(ref _tracker) % 255));
    var random = new Random(seed);
    return random;
});

我现在不是很喜欢使用ThreadStatic。我们有比它更好的工具,可以使用ThreadLocal。只需在并行循环中使用_random.Value,它将为每个线程提供一个新的Random

它结合了原子递增值以及使用Environemnt.TickCount的默认行为。递增值存在的目的是解决两个Random获取相同种子的问题。请注意,此方法仅允许创建255个随机数。如果需要更多,请更改掩码的大小。

正如您已经注意到的,这不能用于安全目的。


2

我的实现结合了其他答案的最佳方法(请参阅类文档中的设计说明)。

/// <summary>
/// DotNet Random is not ThreadSafe so we need ThreadSafeRandom.
/// See also: https://dev59.com/z3A75IYBdhLWcg3w49UR.
/// Design notes:
/// 1. Uses own Random for each thread (thread local).
/// 2. Seed can be set in ThreadSafeRandom ctor. Note: Be careful - one seed for all threads can lead same values for several threads.
/// 3. ThreadSafeRandom implements Random class for simple usage instead ordinary Random.
/// 4. ThreadSafeRandom can be used by global static instance. Example: `int randomInt = ThreadSafeRandom.Global.Next()`.
/// </summary>
public class ThreadSafeRandom : Random
{
    /// <summary>
    /// Gets global static instance.
    /// </summary>
    public static ThreadSafeRandom Global { get; } = new ThreadSafeRandom();

    // Thread local Random is safe to use on that thread.
    private readonly ThreadLocal<Random> _threadLocalRandom;

    /// <summary>
    /// Initializes a new instance of the <see cref="ThreadSafeRandom"/> class.
    /// </summary>
    /// <param name="seed">Optional seed for <see cref="Random"/>. If not provided then random seed will be used.</param>
    public ThreadSafeRandom(int? seed = null)
    {
        _threadLocalRandom = new ThreadLocal<Random>(() => seed != null ? new Random(seed.Value) : new Random());
    }

    /// <inheritdoc />
    public override int Next() => _threadLocalRandom.Value.Next();

    /// <inheritdoc />
    public override int Next(int maxValue) => _threadLocalRandom.Value.Next(maxValue);

    /// <inheritdoc />
    public override int Next(int minValue, int maxValue) => _threadLocalRandom.Value.Next(minValue, maxValue);

    /// <inheritdoc />
    public override void NextBytes(byte[] buffer) => _threadLocalRandom.Value.NextBytes(buffer);

    /// <inheritdoc />
    public override void NextBytes(Span<byte> buffer) => _threadLocalRandom.Value.NextBytes(buffer);

    /// <inheritdoc />
    public override double NextDouble() => _threadLocalRandom.Value.NextDouble();
}

1
这很棒。只有一个小建议,就是在你的开头摘要文档中每行末尾加上<br/>,以使 IntelliSense 弹出窗口更易读。 - wopr_xl

0
如果您知道有多少个线程在并行运行,这可能有效:
Random rand = new Random();
var randomNums = Enumerable.Range(0, Cnts.MosqPopulation)
                           .Select(_ => rand.Next()).ToList();
Parallel.For(0, Cnts.MosqPopulation, i => 
{
    Random localRand = new Random(randomNums[i]);
    DoWork();
});

不确定生成的分布与均匀分布有多相似。


0

您可以从Random继承来构建一个线程安全的随机类。

public class ThreadsafeRandom : Random
{
    private readonly object _lock = new object();

    public ThreadsafeRandom() : base() { }
    public ThreadsafeRandom( int Seed ) : base( Seed ) { }

    public override int Next()
    {
        lock ( _lock )
        {
            return base.Next();
        }
    }

    public override int Next( int maxValue )
    {
        lock ( _lock )
        {
            return base.Next( maxValue );
        }
    }

    public override int Next( int minValue, int maxValue )
    {
        lock ( _lock )
        {
            return base.Next( minValue, maxValue );
        }
    }

    public override void NextBytes( byte[ ] buffer )
    {
        lock ( _lock )
        {
            base.NextBytes( buffer );
        }
    }

    public override double NextDouble()
    {
        lock ( _lock )
        {
            return base.NextDouble();
        }
    }
}

并使用该类的实例

public static class Utils
{
    public static readonly Random random = new ThreadsafeRandom();

}

锁定是最后需要使用的工具,特别是对于这些情况。 - ZOXEXIVO
这没什么问题,如果出于某种原因你想在所有线程中使用相同的随机生成器。至于为什么在多线程环境下要这样做,我不太清楚,但我不评判。 - tekHedd
@tekHedd:为了获得更好的随机性。如果所有线程使用同一个实例,随机分布会更好,更不可预测,因为其他线程会取中间的数字。 - undefined

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