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


78

C#的Random.Next()方法线程安全吗?


1
“此类型的任何公共静态(在Visual Basic中为Shared)成员都是线程安全的。不保证任何实例成员都是线程安全的。” (来自System.Random上的文档)。[好吧,公平的是,人们似乎对人们似乎拥有的伪随机数最常见的问题进行了解释,他们仍然在问。]
Joey

Answers:


32

Next实现线程安全性的方法中,没有做任何特殊的事情。但是,这是一个实例方法。如果您不共享Random不同线程之间的实例,则不必担心实例内的状态损坏。在未Random持有某种排他锁的情况下,请勿在不同线程中使用单个实例。

乔恩·斯基特(Jon Skeet)在此主题上有几篇不错的文章:

StaticRandom
重访随机性

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


13
“如果不在不同线程之间共享Random实例,则不必担心太多。” -这是错误的。由于Random创建方式不同,如果Random几乎同时在两个单独的线程上创建两个单独的实例,则它们将具有相同的种子(并因此返回相同的值)。解决方法请参阅我的答案
BlueRaja-Danny Pflughoeft 2012年

5
@BlueRaja我专门针对单个实例中的状态损坏问题。当然,正如您提到的,两个不同Random实例之间的统计关系正交问题需要进一步注意。
Mehrdad Afshari

23
我不知道为什么将此标记为答案!问:“ Random.Next线程安全吗?” 答:“如果仅从一个线程使用它,那么是的,它是线程安全的”。
米克

您能做的最糟糕的事情是喜欢在外部文章中给出答案,而实际上不包括答案...尤其是当它如此简单时。
安东尼·尼科尔斯

“它们将具有相同的种子”。.NetCore中已修复此问题。
马格努斯

85

不可以,在多个线程中使用同一实例可能会导致它中断并返回全0。但是,创建线程安全版本(无需在每次调用时都设置讨厌的锁Next())非常简单。改编自本文的想法:

public class ThreadSafeRandom
{
    private static readonly Random _global = new Random();
    [ThreadStatic] private static Random _local;

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

        return _local.Next();
    }
}

这个想法是static Random为每个线程保留一个单独的变量。但是,由于存在另一个问题Random-如果几乎同时(在大约15ms内)创建多个实例,则这样做显然会失败,它们都将返回相同的值!为了解决这个问题,我们创建了一个全局静态Random实例来生成每个线程使用的种子。

顺便说一下,以上文章的代码演示了的这两个问题Random


12
很好,但是不喜欢您创建一个ThreadSafeRandom使用它的方式。为什么不将静态属性与包含当前construtors代码的惰性getter一起使用。这里的想法是:confluence.jetbrains.com/display/ReSharper / ...然后整个类可以是静态的。
weston,

2
@weston:通常被认为是反模式。除了失去所有OOP的好处,主要的原因是静态对象不能被嘲笑,这是单元测试使用这个任何类是至关重要的。
BlueRaja-Danny Pflughoeft13年

3
您总是可以在需要时添加一个间接层,例如,添加一个IRandom,以及一个在运行时将调用重定向到静态的类,这样可以进行模拟。对我来说,所有成员都是静态的,这意味着我应该有一个静态类,它告诉用户更多信息,它说每个实例不是独立的随机数序列,而是共享的。这是我之前需要模拟静态框架类时所采用的方法。
weston,

6
如果实际上从多个线程使用代码,则该代码将无法工作,因为_local不会在每次访问它时创建:NullRefereceException。
Laie 2015年

3
如前面的评论中所述,从多个线程中使用此方法实际上不起作用。_local不能在构造函数中实例化。
亚历克斯

23

从微软的官方答案是非常强的没有。从http://msdn.microsoft.com/en-us/library/system.random.aspx#8

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

如文档中所述,当多个线程使用同一个Random对象时,会发生非常讨厌的副作用:它只是停止工作。

(即,存在一种竞争条件,当触发该竞争条件时,所有后续调用的“ random.Next ....”方法的返回值将为0。)


1
使用随机对象的不同实例会带来更糟的副作用。它为多个线程返回相同的生成编号。来自同一篇文章:Instead of instantiating individual Random objects, we recommend that you create a single Random instance to generate all the random numbers needed by your app. However, Random objects are not thread safe.
AaA

1
这是黄金。我很幸运能从Random对象
Agustin Garzon

14

不,这不是线程安全的。如果需要使用来自不同线程的同一实例,则必须同步使用。

不过,我真的看不到您为什么需要它的任何原因。每个线程拥有自己的Random类实例将更加高效。


如果您有一个单元测试对象并想一次生成大量的测试对象,这可能会咬人。原因是许多人将Random设为易于使用的全局对象。我只是这样做了,rand.Next()继续生成0作为aa值。
JSWork 2011年

1
@JSWork:我不是真的按照你的意思。您说“这个”时指的是什么?如果我正确理解了您的最后一句话,则可以跨线程访问该对象而不进行同步,这可以解释结果。
Guffa 2011年

1
你是对的。我很抱歉-这句话说得不好。不做你提到的事情会咬你的屁股。请注意,在为每个线程创建新的随机对象时,读者也应小心-随机对象使用当前时间作为种子。种子变化之间有10ms的间隔。
JSWork 2011年

3
@JSWork:是的,如果同时启动线程,则存在时序问题。您可以在主线程中使用一个Random对象来提供种子,以创建线程的Random对象以解决该问题。
Guffa 2011年

1
如果要为每个线程单独设置随机数,请为每个线程使用更独特的内容作为种子,例如线程ID或时间+线程ID或类似名称。
apokryfos

9

另一种线程安全的方法是使用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
但是在这种情况下,不必确保该方法不必是线程安全的....每个线程都将访问其自己的对象副本。
缺口

4
++SeedCount介绍比赛条件。使用Interlocked.Increment代替。
爱德华·布雷

1
正如OP所指出的那样,这仅适用于有限数量的线程,这在ASP.NET内部可能不是一个
不错的

1
我认为您可以将对Random构造函数中的GenerateSeed()的调用替换为:Guid.NewGuid()。GetHashCode())
Siavash Mortazavi,

5

由于Random不是线程安全的,因此每个线程应该有一个,而不是全局实例。如果您担心Random同时播种这些多个类(即,按DateTime.Now.Ticks此类),则可以使用Guids为它们中的每一个播种。.NETGuid生成器会花费相当长的时间来确保不可重复的结果,因此:

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

2
-1; NewGuid实际上,使用生成的GUID保证是唯一的,但是这些GUID的前4个字节(仅此而已BitConverter.ToInt32)不是。作为一般原则,将GUID的子字符串视为唯一是一个可怕的想法
马克·阿默里

2
在这种特殊情况下,唯一可以兑现这种方法的是,Guid.NewGuid至少在Windows上,.NET使用版本4 GUID,这些GUID大多是随机生成的。特别是,前32位是随机生成的,因此,您实际上只是在为您的Random实例播种一个(大概是密码?)随机数,发生冲突的几率为20亿分之一。但是,我花了数小时的研究来确定这一点,而且我仍然不知道.NET CoreNewGuid()在非Windows操作系统上的行为。
Mark Amery

@MarkAmery OP没有指定是否需要加密质量,因此我认为答案仍然是非关键情况下快速编码的一种方法。根据您的第一条评论,我修改了代码以避免前四个字节。
格伦Slayden

1
使用后4个字节而不是前4个字节没有帮助;当我说不能保证GUID的前4个字节唯一时,我的意思是说GUID的4个字节都不应该是唯一的。整个16字节的GUID是,但不是其中的任何较小部分。您所做的更改实际上使情况变得更糟,因为对于版本4 GUID(在Windows上由.NET使用),后4个字节包括4个具有固定值的非随机位。您的编辑已将可能的种子值数量减少到了数亿美元。
Mark Amery

好,谢谢。我还原了更改,如果有您提出的问题,人们可以留心您的评论。
Glenn Slayden

4

对于它的价值,这里有一个继承自线程安全,具有加密功能的RNG 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();吗?
克里斯·马里西克

@ChrisMarisic,我遵循下面的链接模式。但是,对锁的延迟实例化(如果有)的好处很小,因此您的建议似乎很合理。csharpindepth.com/articles/general/singleton.aspx#lazy
Jodrell

这看起来是一个很好的解决方案,但我有一些疑问,为什么使用BitConverter.ToUInt32和BitConverter.ToInt32会清理GetNext()?为何将池设为静态?它可能使您免于多个池的使用,但在您有许多SafeRandom实例的并发系统中,它也会成为瓶颈。如何播种RNGCryptoServiceProvider?
Wouter

2

每个文档

此类型的任何公共static(在Visual Basic中为Shared)成员都是线程安全的。不保证任何实例成员都是线程安全的。

http://msdn.microsoft.com/zh-CN/library/system.random.aspx


2
您引用的文档实际上是不正确的。我相信这是机器生成的内容。MSDN上该主题的社区内容包含大量信息,说明为什么Random类型不是线程安全的,以及如何解决此问题(使用密码学或使用“信号量”)

1
@MTG您的意见很困惑。Random没有静态成员,因此引用的文档有效地表明其所有成员“都不保证是线程安全的”。您指出这是不正确的,然后通过说...Random不是线程安全的来对此进行支持。这与文档所说的差不多!
Mark Amery

刘海头在桌子上。是的,您是正确的“文档说的差不多!”
西雅图伦纳德

2

使用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);
    }
}

1

对于线程安全的随机数生成器,请查看RNGCryptoServiceProvider。从文档:

线程安全

此类型是线程安全的。


1
是的,RNGCryptoServiceProvider是黄金。比Random好得多,尽管还有很多工作要做,以使其吐出特定类型的数字(因为它生成随机字节)。
Rangoric 2010年

1
@Rangoric:不,在任何可以使用两者的目的上,它都不比Random好。如果出于加密目的需要随机性,则不能选择Random类,出于您可以选择的任何目的,Random类将更快,更易于使用。
Guffa 2010年

@Guffa的易用性是一回事,尽管因为我已经将它塞入库中了,所以这并不是一个好主意。尽管我宁愿拥有真正的随机性而不是看起来不错的随机性,但更快是一个合理的观点。为此,我也要使其具有线程安全性。尽管现在我打算对此进行测试,以查看它变慢了多少(Random产生双精度并将其转换为您所要求的值,所以它甚至可能完全取决于您需要的数字范围)
Rangoric 2010年

在C#中,我发现使用非常基本的RNGCrypto实现,大约为100:1,具体取决于要查找的确切数字(例如127的性能是128的两倍)。接下来,我计划添加线程,看看它是如何工作的(为什么不:))
Rangoric

更新到上述声明。对于小于256个值的范围,我将其设置为2:1,并且越接近我们想要的数字256,效果会更好。
Rangoric 2010年

-1

更新:不是。您需要在每次调用.Next()方法时锁定一些“信号量”对象,或者在每次连续调用中重用Random实例,或者在每次此类调用中使用带有保证随机种子的新实例。您可以按照Yassir的建议通过在.NET中使用加密来获得有保证的其他种子。


-1

传统的 通过对种子使用无锁算法,可以改进线程本地存储方法。从Java的算法中偷偷偷走了以下内容(甚至可能对其进行了改进):

public static class RandomGen2 
{
    private static readonly ThreadLocal<Random> _rng = 
                       new ThreadLocal<Random>(() => new Random(GetUniqueSeed()));

    public static int Next() 
    { 
        return _rng.Value.Next(); 
    } 

    private const long SeedFactor = 1181783497276652981L;
    private static long _seed = 8682522807148012L;

    public static int GetUniqueSeed()
    {
        long next, current;
        do
        {
            current = Interlocked.Read(ref _seed);
            next = current * SeedFactor;
        } while (Interlocked.CompareExchange(ref _seed, next, current) != current);
        return (int)next ^ Environment.TickCount;
   } 
}

1
这种(不可避免的)int强制失败的观点。Java的Random种子带有a long,但是C#只是需要一个int ...而且更糟糕的是,它使用该有符号的int的绝对值作为种子,这意味着实际上只有2 ^ 31个不同的种子。long如果您要丢掉大部分种子,那么产生好种子就是一种浪费long; 随机地给C#随机种子,即使您的随机种子是完全随机的,仍然使您有大约20亿次碰撞的机会。
Mark Amery
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.