随机数生成器只生成一个随机数

839

我有如下函数:

//Function to get random number
public static int RandomNumber(int min, int max)
{
    Random random = new Random();
    return random.Next(min, max);
}

我如何称呼它:

byte[] mac = new byte[6];
for (int x = 0; x < 6; ++x)
    mac[x] = (byte)(Misc.RandomNumber((int)0xFFFF, (int)0xFFFFFF) % 256);

如果在运行时使用调试器步进循环,我会得到不同的值(这正是我想要的)。 然而,如果我在该代码下面的两行处设置断点,mac数组的所有成员都具有相等的值。

为什么会这样呢?


27
使用 new Random().Next((int)0xFFFF, (int)0xFFFFFF) % 256); 并不能生成比 .Next(0, 256) 更好的“随机”数。 - bohdan_trotsenko
2
你可能会发现这个NuGet包很有用。它提供了一个静态的Rand.Next(int, int)方法,可以在不锁定或遇到种子重用问题的情况下静态访问随机值。 - ChaseMedallion
15个回答

1144

每次执行new Random()时,都会使用时钟初始化。这意味着在一个紧密的循环中,你会多次得到相同的值。你应该保持一个单一的Random实例,并在同一个实例上继续使用Next

//Function to get a random number 
private static readonly Random random = new Random(); 
private static readonly object syncLock = new object(); 
public static int RandomNumber(int min, int max)
{
    lock(syncLock) { // synchronize
        return random.Next(min, max);
    }
}

编辑(见评论):为什么我们需要在这里使用 lock

基本上,Next 方法将改变 Random 实例的内部状态。如果我们同时从多个线程进行此操作,你可以认为 "我们刚刚使结果更加随机了",但实际上我们可能会破坏内部实现并且也可能从不同的线程获得相同的数字,这可能是一个问题,也可能不是一个问题。然而,内部发生的事情没有任何保证才是更大的问题,因为 Random 并不保证线程安全。因此,有两种有效的方法:

  • 同步以便我们不会从不同的线程同时访问它
  • 对于每个线程使用不同的 Random 实例

两者都可以,但是对单个实例进行互斥调用是在寻找麻烦。

lock 实现了这两种方法中的第一种(也是较为简单的)方法;然而,另一种方法可能是:

private static readonly ThreadLocal<Random> appRandom
     = new ThreadLocal<Random>(() => new Random());

这是针对每个线程的,因此您不需要同步。


26
作为一般规则,所有静态方法都应该被制作成线程安全的,因为很难保证多个线程不会同时调用它。通常并不需要将实例(即非静态)方法制作成线程安全的。 - Marc Gravell
5
@Florin - 在这两者之间,“基于堆栈”的区别并不存在。静态字段同样是“外部状态”,并且绝对会在调用者之间共享。对于实例,不同的线程很有可能拥有不同的实例(这是一个常见的模式)。但是对于静态字段,它们保证全部共享(不包括[ThreadStatic])。 - Marc Gravell
3
为什么不能使用lock(random) - Dan Bechard
6
如果对象从未公开暴露:您可以这样做。 (非常理论的)风险在于,某些其他线程会以您未预期的方式锁定它。 - Marc Gravell
4
很可能你只是在没有锁的情况下使用随机数。锁并不能完全阻止对被锁定对象的访问,它只是确保同一实例上的两个锁语句不会同时运行。因此,只有当所有 random.Next() 调用也在 lock (syncObject) 内时,lock (syncObject) 才会有所帮助。如果即使使用了正确的 lock 语句,你描述的场景仍然发生,那么这种情况在单线程环境下(例如 Random 存在微妙的错误)也极有可能发生。 - Luaan
显示剩余19条评论

135

为了在整个应用程序中方便地重复使用,静态类可能会有所帮助。

public static class StaticRandom
{
    private static int seed;

    private static ThreadLocal<Random> threadLocal = new ThreadLocal<Random>
        (() => new Random(Interlocked.Increment(ref seed)));

    static StaticRandom()
    {
        seed = Environment.TickCount;
    }

    public static Random Instance { get { return threadLocal.Value; } }
}

您可以使用类似以下代码的静态随机实例:

StaticRandom.Instance.Next(1, 100);

68

由于Mark的解决方案需要每次进行同步,因此可能会非常昂贵。

我们可以通过使用线程特定存储模式来避免需要进行同步的情况:


public class RandomNumber : IRandomNumber
{
    private static readonly Random Global = new Random();
    [ThreadStatic] private static Random _local;

    public int Next(int max)
    {
        var localBuffer = _local;
        if (localBuffer == null) 
        {
            int seed;
            lock(Global) seed = Global.Next();
            localBuffer = new Random(seed);
            _local = localBuffer;
        }
        return localBuffer.Next(max);
    }
}

测量这两个实现,你应该会看到显著的差异。


16
当锁没有竞争时,它们非常便宜......即使有竞争,我也预计“现在对数字进行操作”的代码在大多数有趣的场景中会比锁的成本高得多。 - Marc Gravell
4
同意,这解决了锁定问题,但这仍然是一个对于一个微不足道的问题而言过于复杂的解决方案:你需要写“两行”代码来生成随机数,而不是一行。难道为了避免读取一行简单的代码,这真的值得吗? - EMP
5
使用额外的全局 Random 实例来获取种子是个好主意。还要注意,代码可以进一步简化,使用在 .NET 4 中引入的 ThreadLocal<T> 类(正如 Phil 在下面所写的那样)。 - vgru
由于 _localThreadStatic 的,为什么要将它复制到/从 var localBuffer 中?这是一种性能优化吗?也就是说,访问 ThreadStatic 变量的性能是否比访问常规变量显著更昂贵?(如果是这样,在典型情况下,这可能会抵消对 lock 的所谓优势。如果不是,则可以简化代码。) - ToolmakerSteve
@ToolmakerSteve 是的,堆栈比TSS更快。我不担心与锁定相比的成本,因为锁定会引入100到1000个周期。我的解决方案问题在于“If”语句引入的分支可能会导致100多个周期的成本,因为当分支预测器错误时,需要刷新流水线和指令缓存。 - Hans Malherbe
好的。我认为这对于任何ThreadStatic解决方案都是固有的。(或者如果使用ThreadLocal,则必须在内部进行类似的测试以确定是否需要惰性初始化。)一旦我们降到由分支预测器猜测正确性主导的优化级别,那对于我所需的任何目的来说就足够了。谢谢。 - ToolmakerSteve

42

我的回答来自这里

只是重申正确的解决方案

namespace mySpace
{
    public static class Util
    {
        private static Random rnd = new Random();
        public static int GetRandom()
        {
            return rnd.Next();
        }
    }
}

所以你可以调用:

var i = Util.GetRandom();

全程都需要。

如果您严格需要一个真正的无状态静态方法来生成随机数,您可以依赖于 Guid

public static class Util
{
    public static int GetRandom()
    {
        return Guid.NewGuid().GetHashCode();
    }
}

它会慢一点,但可以比Random.Next更随机,至少从我的经验来看。

不是

new Random(Guid.NewGuid().GetHashCode()).Next();

不必要的对象创建会使它在循环下变慢。

而且永远不要

new Random().Next();

不仅它在循环内速度较慢,而且它的随机性... 在我看来并不是很好。


17
我不同意Guid案例。Random类实现了均匀分布,而Guid则不是如此。Guid的目的是为了保证唯一性,而不是均匀分布(它的实现大多基于某些硬件/机器属性,这与随机性相反)。 - Askolein
6
如果无法证明GUID生成的均匀性,则将其用作随机数是错误的(哈希算法也会因此而远离均匀性)。同样,碰撞不是问题:关键是碰撞的均匀性。关于GUID生成不再在硬件上的问题,我会查看文档,我的错误(有任何参考资料吗?) - Askolein
6
“随机”有两种理解:1.缺乏模式或2.缺乏遵循概率分布描述的演化模式(2包含在1中)。您的Guid示例在情况1下是正确的,但在情况2下不正确。相反,“Random”类符合情况2(因此也符合情况1)。只有在不涉及情况2时,您才能将“Random”类的使用替换为您的“Guid+Hash”。情况1可能足以回答问题,然后您的“Guid+Hash”就可以正常工作。但并没有清楚地说明这一点。(注:这个均匀分布 - Askolein
2
@Askolein 仅用于一些测试数据,我运行了几批 RandomGuid.NewGuid().GetHashCode() 通过 Ent (http://www.fourmilab.ch/random/),两者都是类似随机的。`new Random(Guid.NewGuid().GetHashCode())同样有效,使用同步的“主”Random生成“子”Random` 的种子也可以。当然,这取决于您的系统如何生成 Guids - 对于我的系统,它们非常随机,对于其他系统甚至可能是加密随机。所以现在 Windows 或 MS SQL 看起来都很好。但 Mono 和/或移动设备可能会有所不同。 - Luaan
2
@EdB 正如我之前在评论中所说的,虽然 Guid(一个大数字)被认为是唯一的,但 .NET 中 Guid 的 GetHashCode 是从其字符串表示派生的。输出对我来说相当随机。 - nawfal
显示剩余9条评论

28

我更愿意使用以下类来生成随机数:

byte[] random;
System.Security.Cryptography.RNGCryptoServiceProvider prov = new System.Security.Cryptography.RNGCryptoServiceProvider();
prov.GetBytes(random);

32
我不是其中一个对投票者,但请注意标准 PNRG 确实有一种真正的需求——即能够从已知的种子可重复地生成序列。有时候真正的加密 RNG 的成本实在太高了。而有时候加密 RNG 是必要的。不同的情况需要不同的工具,可以这么说。 - Marc Gravell
4
根据文档,这个类是线程安全的,这是它有利的一点。 - Rob Church
使用它,两个随机字符串相同的概率是多少?如果字符串只有3个字符,我猜这种情况很可能发生,但如果长度为255个字符,是否可能出现相同的随机字符串,或者从算法上保证不会发生? - Lyubomir Velchev
@LyubomirVelchev - 在数学上,设计一个可以保证两个独立生成的有限长度字符串永远不相同的函数(或硬件部件甚至理论构造)是不可能的。这是不可能的:因为选择的数量是有限的。假设有n个可能的字符串,则必须存在1/n的概率使得两个独立的字符串相同。 (是的,这意味着任何加密方案都不是100%安全的;但是如果在宇宙的寿命内发生两次的几率足够低...则在实践中就足够好了。) - ToolmakerSteve
Joma的后续答案包含了一个基于RNGCryptoServiceProvider更完整的代码片段。请参见public static int Next(int min, int max) ...。但是为了提高性能,修改他的代码将new移出Next方法 - 参见我的评论。 - ToolmakerSteve

18

1)就像Marc Gravell所说,尽量只使用一个随机数生成器。将以下内容添加到构造函数中效果更佳:System.Environment.TickCount。

2)一个小技巧。假设您要创建100个对象,并且每个对象应该有自己的随机数生成器(如果您需要在很短的时间内计算大量随机数,则非常方便)。如果您想通过循环(生成100个对象)来完成此操作,可以使用以下代码确保完全随机性:

int inMyRandSeed;

for(int i=0;i<100;i++)
{
   inMyRandSeed = System.Environment.TickCount + i;
   .
   .
   .
   myNewObject = new MyNewObject(inMyRandSeed);  
   .
   .
   .
}

// Usage: Random m_rndGen = new Random(inMyRandSeed);

干杯。


4
我会将 System.Environment.TickCount 从循环中移出。如果在迭代过程中其计数器溢出,您将得到两个使用相同种子进行初始化的项。另一个选择是以不同方式组合 tickcount 和 i(例如,System.Environment.TickCount<<8 + i)。 - Dolphin
如果我理解正确:您的意思是,“System.Environment.TickCount + i”可能会产生相同的值吗? - sabiland
当然,循环内不需要使用TickCount。我的错误 :)。 - sabiland
3
默认的 Random() 构造函数无论如何都会调用 Random(Environment.TickCount) - Alsty
@Alsty - 有用的观察 - 如果只创建一个全局随机生成器。但是,如果在同一时刻调用默认的Random()构造函数两次,您将获得两个随机生成器,它们每个都生成完全相同的随机数序列。可能不是您想要的!上述逻辑(#2)使用种子TickCount+0TickCount+1等 - 因此生成器都是不同的 - ToolmakerSteve

10

每次执行时

Random random = new Random (15);

无论您执行多少次,都将使用相同的种子。

如果您使用

Random random = new Random ();

如果黑客猜测了种子并且您的算法与系统安全有关,则您的算法将被破解,并且会获得不同的随机数序列。 如果您执行mult,那么在此构造函数中,种子是由系统时钟指定的,如果在非常短的时间内(毫秒级别)创建了多个实例,则可能会具有相同的种子。

如果您需要安全的随机数字,则必须使用以下类:

System.Security.Cryptography.RNGCryptoServiceProvider

public static int Next(int min, int max)
{
    if(min >= max)
    {
        throw new ArgumentException("Min value is greater or equals than Max value.");
    }
    byte[] intBytes = new byte[4];
    using(RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
    {
        rng.GetNonZeroBytes(intBytes);
    }
    return  min +  Math.Abs(BitConverter.ToInt32(intBytes, 0)) % (max - min + 1);
}

使用方法:

int randomNumber = Next(1,100);

3
除非您自己指定种子,否则无论执行多少次,都将使用相同的种子。 - LarsTech
正如您所说的,如果始终指定相同的种子,将始终生成相同的随机数序列。在我的答案中,我提到了使用构造函数和参数时,如果始终使用相同的种子。Random类仅生成伪随机数。如果有人找出您在算法中使用的种子,可能会危及算法的安全性或随机性。通过RNGCryptoServiceProvider类,您可以安全地拥有随机数。 我已经纠正了,非常感谢您的纠正。 - Joma
在每个 Next 上调用 new RNGCryptoServiceProvider() 是过度的。相反,声明 private static RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); 然后移除 using 包装器;只需在该静态上调用 rng.GetNonZeroBytes(intBytes); - ToolmakerSteve
所有的软件算法都生成伪随机数序列,真正的随机性需要基于某些被认为是“真正随机”的物理现象的硬件。另一方面,加密算法经过精心设计(和测试),以改善所生成序列的统计分布——避免暴力攻击利用较简单的随机生成器中的弱点。尽管对许多用途来说有些过度,但我同意这给出了更优越的统计分布。因此,“Random”类只能生成伪随机数。 - ToolmakerSteve

3

从.NET 6开始,Random类现在配备了一个名为Shared的静态属性:

提供一个线程安全的随机实例,可在任何线程中并发使用。

你可以像这样使用它:

// Function to get random number
public static int RandomNumber(int min, int max)
{
    return Random.Shared.Next(min, max);
}

访问线程安全的对象会有一定开销,因此如果您计划在单个线程上尽可能快地生成数百万个随机数,最好创建一个专用的 Random 实例,而不是依赖于 Shared


1
为什么会发生这种情况?
正如之前所回答的,每次调用new Random()都会得到一个新的Random类副本,它使用相同的时钟进行初始化(因此返回相同的值)。
现在,从.NET 6开始,有一个易于使用且线程安全的替代方法:Random.Shared 在您的示例中,您可以完全删除RandomNumber函数,然后使用以下代码(具有相同的逻辑,但现在可以正确工作):
byte[] mac = new byte[6];
for (int x = 0; x < 6; ++x)
    mac[x] = (byte)(Random.Shared.Next(0, 255));

0
您可以使用像这样的代码:
public static class ThreadSafeRandom
{
    private static readonly Random _global = new Random();
    private static readonly ThreadLocal<Random> _local = new ThreadLocal<Random>(() =>
    {
        int seed;
        lock (_global)
        {
            seed = _global.Next();
        }
        return new Random(seed);
    });

    public static Random Instance => _local.Value;
}

这段代码可以直接使用,或者通过NuGet包ThreadSafeRandomizer使用。

编辑:自从.NET 6.0版本以后,你可以使用Random.Shared.Next()代替。你仍然可以使用上述的包,它会根据预处理指令在上述代码和Random.Shared之间进行选择。


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