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

105

2
此类型的任何公共静态成员(在 Visual Basic 中为 Shared)都是线程安全的。任何实例成员都不能保证是线程安全的。(来自 System.Random 文档)。好吧,公平地说:人们似乎遇到的伪随机数最常见的问题也在那里解释了,但他们仍然继续提问。 - Joey
8
从.NET 6开始,您可以使用Random.Shared属性来获取线程安全的随机数生成器实例。请注意,在翻译过程中,我保持了原文的意思和信息,并尽力使翻译更加通俗易懂。 - Angel Cloudwalker
17个回答

101
不可以在多个线程中使用相同的实例,否则会导致其返回所有0的结果。但是创建一个线程安全版本(不需要在每次调用Next()时使用锁)很简单。这个想法改编自这篇文章
public class ThreadSafeRandom
{
    private static readonly Random _global = new Random();
    [ThreadStatic] private static Random _local;

    public int Next()
    {
        if (_local == null)
        {
            int seed;
            lock (_global)
            {
                seed = _global.Next();
            }
            _local = new Random(seed);
        }

        return _local.Next();
    }
}

这个想法是为每个线程保留一个单独的 static Random 变量。然而,显而易见的方式会失败,因为 Random 存在另一个问题 - 如果在(大约15毫秒内)创建多个实例,则它们将返回相同的值!为了解决这个问题,我们创建了一个全局静态 Random 实例来生成每个线程使用的种子。

顺便说一下,上面的文章中有代码展示了 Random 的这两个问题。


12
不错,但不喜欢需要创建一个ThreadSafeRandom来使用它的方式。为什么不使用带有延迟加载器的静态属性,该属性包含当前构造器的代码。这个想法来自于这里:http://confluence.jetbrains.com/display/ReSharper/'ThreadStaticAttribute'+usage,然后整个类可以是静态的。 - weston
3
当需要时,您始终可以添加一个间接层。例如,添加一个“IRandom”接口和一个类,在运行时将调用重定向到静态对象,这将允许模拟。对于我来说,如果所有成员都是静态的,那就意味着我应该有一个静态类,它会告诉用户更多信息,即每个实例不是独立的随机数序列,而是共享的。当我需要模拟一个静态框架类时,我以前采用过这种方法。 - weston
6
如果您从多个线程实际使用代码,则代码将无法工作,因为每次访问_local时都不会创建它:NullRefereceException。 - Laie
3
使用这种方法需要谨慎,Random类只提供了大约20亿个可能的随机数种子(确切地说,是2^31 + 1,因为Random构造器使用带符号的整数,但在使用前将符号丢弃)。对于某些应用程序而言,即使不需要加密安全的随机性,两个线程生成相同的“随机”序列的概率为20亿分之一可能是不可取的。与在此处随机生成种子相比,我认为更好的一般做法是递增静态存储的先前种子。 - Mark Amery
3
如早前评论所述,当从多个线程使用时,这种方法实际上不起作用。 _local 无法在构造函数中实例化。 - Alex
显示剩余16条评论

34

Next方法中没有特别处理以实现线程安全性,但它是一个实例方法。如果您不在不同线程之间共享Random实例,您就不必担心实例内部状态的破坏。不要在没有某种独占锁的情况下跨不同线程使用单个Random实例。

Jon Skeet有几篇关于这个主题的好文章:

StaticRandom
重新思考随机性

正如一些评论员所指出的,使用不同的、线程专用的Random实例存在另一个潜在问题,即它们被相同的种子初始化,从而产生相同的伪随机数序列,因为它们可能在同一时间或彼此接近的时间内创建。缓解这个问题的一种方法是使用一个主Random实例(由单个线程锁定)生成一些随机种子,并为每个其他线程使用初始化新的Random实例。


18
如果您没有在不同的线程之间共享 Random 实例,那么您就不用担心太多。这是错误的。由于创建 Random 的方式,如果在两个不同的线程上几乎同时创建了两个单独的 Random 实例,它们将具有相同的种子(因此返回相同的值)。请参见我的答案以获取解决方法。 - BlueRaja - Danny Pflughoeft
7
@BlueRaja,我特别关注单个实例内的国家腐败问题。当然,正如你所提到的,两个不同Random实例之间的统计关系的正交问题需要更加小心处理。 - Mehrdad Afshari
31
我不知道为什么这个被标记为答案!问:Random.Next是线程安全的吗?答:如果你只在一个线程中使用它,那么它是线程安全的...最差的答案! - Mick
你能做的最糟糕的事情就是喜欢引用外部文章来回答问题,而实际上并没有包含答案...尤其是当答案像这样简单时。 - Anthony Nichols
2
它已经在 .Net Core 中修复了,它们将拥有相同的种子。 - Magnus

26
微软官方的回答是坚决不行。在http://msdn.microsoft.com/en-us/library/system.random.aspx#8中写道:

Random对象不是线程安全的。如果您的应用程序从多个线程调用Random方法,您必须使用同步对象来确保只有一个线程可以同时访问随机数生成器。如果您不确保以线程安全的方式访问Random对象,则返回随机数的方法调用会返回0。

如文档所述,当多个线程使用相同的Random对象时,可能会发生非常严重的副作用:它就停止工作了。
也就是说,存在一种竞争条件,当触发时,“random.Next....”方法的返回值将为所有后续调用返回0。

3
使用不同实例的随机对象会有一个更加恶劣的副作用,即对于多个线程返回相同的生成数字。从同一篇文章中得出:我们建议您创建一个单独的 Random 实例来生成应用程序所需的所有随机数,而不是实例化各自的 Random 对象。然而,Random 对象不是线程安全的。 - AaA
1
这真是太棒了。我很幸运地从Random对象中获得了零结果 - Agustin Garzon

14

不,它不是线程安全的。如果你需要从不同的线程使用相同的实例,你必须同步使用。

虽然我确实看不出你为什么需要这样做。对于每个线程拥有自己的 Random 类的实例会更有效率。


如果你有一个单元测试对象并想一次生成大量的测试对象,这可能会让你很烦恼。原因是许多人将Random作为全局对象以方便使用。我刚刚这样做了,但rand.Next()一直生成0作为值。 - JSWork
1
@JSWork:我不是很明白你的意思。当你说“这个”时,你指的是什么?如果我正确理解了你的最后一句话,那么你在没有同步对象的情况下跨线程访问了它,这可以解释结果。 - Guffa
1
你是正确的。很抱歉 - 我表达得不好。不按照你所提到的去做可能会让你吃亏。作为一个警示,读者在每个线程中创建新的随机对象时也应该小心 - 随机对象使用当前时间作为它们的种子。种子改变之间有10毫秒的间隔。 - JSWork
3
如果线程同时启动,就会出现时间问题。你可以在主线程中使用一个 Random 对象来提供用于创建线程的 Random 对象的种子,以解决这个问题。 - Guffa
1
如果您要为每个线程创建单独的随机数,请使用更独特于每个线程的种子,例如线程ID或时间+线程ID等。 - apokryfos
这应该是答案。 - Mick

14
C#的Random.Next()方法是线程安全的吗?
正如之前所述,答案是否定的。然而,从.NET6开始,我们有了开箱即用的线程安全替代方案:Random.Shared.Next()。 在此处查看详细信息

10

另一种线程安全的方法是使用ThreadLocal<T>,具体操作如下:

new ThreadLocal<Random>(() => new Random(GenerateSeed()));

GenerateSeed()方法需要在每次调用时返回一个唯一的值,以确保在每个线程中随机数序列都是唯一的。

static int SeedCount = 0;
static int GenerateSeed() { 
    return (int) ((DateTime.Now.Ticks << 4) + 
                   (Interlocked.Increment(ref SeedCount))); 
}

适用于少量线程。


1
但在这种情况下,这是不是确保该方法不需要支持多线程安全性呢?每个线程将访问其自己的对象副本。 - gap
5
++SeedCount引入了竞态条件。请改用Interlocked.Increment。 - Edward Brey
1
正如 OP 所指出的那样,这将适用于有限数量的线程,这很可能不是 ASP.NET 中的最佳选择。 - Chris Marisic
2
我认为你可以用 Guid.NewGuid().GetHashCode() 替换 Random 构造函数中对 GenerateSeed() 的调用。 - Siavash Mortazavi

5

更新 从 .NET 6 开始,Random.Shared 提供了一个内置的线程安全 Random 类型(使用 ThreadStatic 在幕后 进行同步)。

原始答案 使用 ThreadLocal 重新实现 BlueRaja 的答案:

public static class ThreadSafeRandom
{
    private static readonly System.Random GlobalRandom = new Random();
    private static readonly ThreadLocal<Random> LocalRandom = new ThreadLocal<Random>(() => 
    {
        lock (GlobalRandom)
        {
            return new Random(GlobalRandom.Next());
        }
    });

    public static int Next(int min = 0, int max = Int32.MaxValue)
    {
        return LocalRandom.Value.Next(min, max);
    }
}

为了增加代码的风格,你可以使用 Lazy<Random> GlobalRandom 来避免显式的 lock - Theodor Zoulias
1
@TheodorZoulias 不确定我是否理解了,问题不仅在于初始化 - 如果我们放弃全局锁定,我们可能会同时访问全局的 Random 的2个线程... - Ohad Schneider
1
哦哈德,你是对的。我不知道我在想什么。在这种情况下,Lazy<T>类没有任何作用。 - Theodor Zoulias
这是什么神奇的东西。我用它替换了我的锁定发生器号码,CPU风扇试图离家出走。处理速度从每秒4500次提高到117000次以上,而且还在增加,我不明白为什么,似乎每秒钟都比较快而不是慢。我会就此提出问题,因为我不理解我正在检索的数据,似乎类似于我的1/30倍方法。 - Leandro Bardelli
1
@LeandroBardelli 这个答案已经过时了,请查看更新。 - Ohad Schneider
1
谢谢Ohad!!!我试过了,结果完全相同且性能也相同。 - Leandro Bardelli

4

以下是一个线程安全的、具有密码学强度的随机数生成器,它继承了Random

该实现包括静态入口点以便于使用,它们的名称与公共实例方法相同,但前缀为"Get"。

调用RNGCryptoServiceProvider.GetBytes是一项相对昂贵的操作。通过使用内部缓冲区或"池"来降低使用RNGCryptoServiceProvider的频率并提高效率,这种情况得到了缓解。如果应用程序域中的生成次数很少,则可能被视为开销。

using System;
using System.Security.Cryptography;

public class SafeRandom : Random
{
    private const int PoolSize = 2048;

    private static readonly Lazy<RandomNumberGenerator> Rng =
        new Lazy<RandomNumberGenerator>(() => new RNGCryptoServiceProvider());

    private static readonly Lazy<object> PositionLock =
        new Lazy<object>(() => new object());

    private static readonly Lazy<byte[]> Pool =
        new Lazy<byte[]>(() => GeneratePool(new byte[PoolSize]));

    private static int bufferPosition;

    public static int GetNext()
    {
        while (true)
        {
            var result = (int)(GetRandomUInt32() & int.MaxValue);

            if (result != int.MaxValue)
            {
                return result;
            }
        }
    }

    public static int GetNext(int maxValue)
    {
        if (maxValue < 1)
        {
            throw new ArgumentException(
                "Must be greater than zero.",
                "maxValue");
        }
        return GetNext(0, maxValue);
    }

    public static int GetNext(int minValue, int maxValue)
    {
        const long Max = 1 + (long)uint.MaxValue;

        if (minValue >= maxValue)
        {
            throw new ArgumentException(
                "minValue is greater than or equal to maxValue");
        }

        long diff = maxValue - minValue;
        var limit = Max - (Max % diff);

        while (true)
        {
            var rand = GetRandomUInt32();
            if (rand < limit)
            {
                return (int)(minValue + (rand % diff));
            }
        }
    }

    public static void GetNextBytes(byte[] buffer)
    {
        if (buffer == null)
        {
            throw new ArgumentNullException("buffer");
        }

        if (buffer.Length < PoolSize)
        {
            lock (PositionLock.Value)
            {
                if ((PoolSize - bufferPosition) < buffer.Length)
                {
                    GeneratePool(Pool.Value);
                }

                Buffer.BlockCopy(
                    Pool.Value,
                    bufferPosition,
                    buffer,
                    0,
                    buffer.Length);
                bufferPosition += buffer.Length;
            }
        }
        else
        {
            Rng.Value.GetBytes(buffer);
        }
    }

    public static double GetNextDouble()
    {
        return GetRandomUInt32() / (1.0 + uint.MaxValue);
    }

    public override int Next()
    {
        return GetNext();
    }

    public override int Next(int maxValue)
    {
        return GetNext(0, maxValue);
    }

    public override int Next(int minValue, int maxValue)
    {
        return GetNext(minValue, maxValue);
    }

    public override void NextBytes(byte[] buffer)
    {
        GetNextBytes(buffer);
    }

    public override double NextDouble()
    {
        return GetNextDouble();
    }

    private static byte[] GeneratePool(byte[] buffer)
    {
        bufferPosition = 0;
        Rng.Value.GetBytes(buffer);
        return buffer;
    }

    private static uint GetRandomUInt32()
    {
        uint result;
        lock (PositionLock.Value)
        {
            if ((PoolSize - bufferPosition) < sizeof(uint))
            {
                GeneratePool(Pool.Value)
            }

            result = BitConverter.ToUInt32(
                Pool.Value,
                bufferPosition);
            bufferPosition+= sizeof(uint);
        }

        return result;
    }
}

PositionLock = new Lazy<object>(() => new object()); 的目的是什么?这难道不应该只是 SyncRoot = new object(); 吗? - Chris Marisic
@ChrisMarisic,我一直在遵循下面链接的模式。然而,懒惰实例化锁的好处非常有限,如果有的话,所以你的建议似乎是合理的。http://csharpindepth.com/articles/general/singleton.aspx#lazy - Jodrell
这看起来是一个很好的解决方案,但我有一些问题:为什么要使用BitConverter.ToUInt32而不是BitConverter.ToInt32?它会清理GetNext()吗?为什么要使池静态?这可能会避免多个池,但在具有许多SafeRandom实例的并发系统中,它也可能成为瓶颈。如何种子化RNGCryptoServiceProvider? - Wouter

4

由于Random不是线程安全的,您应该在每个线程中拥有一个而不是全局实例。如果您担心这些多个Random类同时被种子化(即通过DateTime.Now.Ticks等方式),您可以使用Guid来为它们中的每一个提供种子。 .NET Guid生成器会花费相当长的时间来确保结果不可重复,因此:

var rnd = new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0))

3
使用NewGuid生成的GUID几乎可以保证是唯一的,但是这些GUID的前4个字节(仅被BitConverter.ToInt32使用)不是唯一的。一般原则是,将GUID的子字符串视为唯一的是一个糟糕的想法 - Mark Amery
3
在这种特定情况下,唯一可以弥补这种方法的是,至少在Windows操作系统上,.NET的Guid.NewGuid使用的是版本4 GUID(请参见https://dev59.com/CHE85IYBdhLWcg3wejfn#2757969),它们大多数是随机生成的。特别地,前32位是随机生成的,所以你基本上只是用一个(可能具有密码学安全性)随机数种子来初始化你的`Random`实例,并且有20亿分之1的碰撞机会。我花费了*几个小时*的时间来确定这一点,但我仍然不知道.NET Core的NewGuid()在非Windows操作系统上的行为。 - Mark Amery
@MarkAmery OP没有指定是否需要加密质量,因此我认为这个答案仍然有用,可以作为一行代码在非关键情况下快速编码。根据您的第一个评论,我修改了代码以避免前四个字节。 - Glenn Slayden
1
使用第二个4字节而不是第一个4字节并没有什么帮助;当我说GUID的前4个字节不能保证唯一时,我指的是GUID的任何4个字节都不应该是唯一的;整个16字节的GUID是唯一的,但是它的任何一个更小的部分都不是。你的修改实际上使情况变得更糟,因为对于版本4 GUID(由Windows上的.NET使用),第二个4字节包括4个非随机位和固定值;你的编辑已将可能的种子值减少到了数亿个。 - Mark Amery
好的,谢谢。我已经撤销了更改,如果有人对您提出的问题有疑虑,他们可以注意您的评论。 - Glenn Slayden
1
你可以用Guid.NewGuid().GetHashCode()替换BitConverter。 - Wouter

2
这里有一个简单的解决方案,不需要创建类。将所有内容隐藏在一个即席的lambda中:
private static Func<int, int, int> GetRandomFunc()
{
    Random random = new Random();
    object lockObject = new object();

    return (min, max) =>
    {
        lock (lockObject)
        {
            return random.Next(min, max);
        }
    };
}

在任何需要线程安全的随机生成器的类中,定义:
private static readonly Func<int, int, int> GetRandomNext = GetRandomFunc();

然后在你的类内自由使用它:

int nextRandomValue = GetRandomNext(0, 10);

随机函数的签名可以根据需要而不同,例如:

private static Func<int> GetRandomFunc()
{
    Random random = new Random();
    object lockObject = new object();

    return () =>
    {
        lock (lockObject)
        {
            return random.Next();
        }
    };
}

点赞这个想法,但我认为它通常不太实用。本质上,您构造一个只有一个方法的对象,然后返回该方法。 - Theodor Zoulias

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