C#的Random.Next()
方法是线程安全的吗?
在Next
方法中没有什么特别的做法来实现线程安全性。但是,它是一个实例方法。如果不跨不同线程共享Random
实例,则不必担心实例中的状态损坏。不要在不同的线程中使用Random
的单个实例而不保留某种类型的独占锁。
Jon Skeet在这个主题上有几个不错的帖子:
StaticRandom
Revisiting randomness
正如一些评论家所指出的那样,使用Random
的不同实例存在另一个潜在的问题,这些实例是线程排他的,但是种子相同,因此会产生相同的伪随机数序列,因为它们可能同时或在关闭时创建彼此的时间接近。缓解该问题的一种方法是使用主Random
实例(由单个线程锁定)生成一些随机种子并为每个其他线程初始化新的Random
实例。
更新:事实并非如此。您需要在每次连续调用时重用Random实例,并在调用.Next()方法时锁定一些“信号量”对象,或者在每次调用时使用带有保证随机种子的新实例。正如Yassir建议的那样,你可以在.NET中使用加密技术来获得有保证的不同种子。
传统的thread local storage approach可以通过对种子使用无锁算法来改进。以下是从Java的算法(可能甚至improving)无耻地被盗:
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;
}
}
不,使用来自多个线程的相同实例可能导致它中断并返回所有0。但是,创建一个线程安全的版本(每次调用Next()
时都不需要讨厌的锁)很简单。改编自this article的想法:
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
来证明这两个问题。
微软的官方回答非常强烈。来自http://msdn.microsoft.com/en-us/library/system.random.aspx#8:
随机对象不是线程安全的。如果您的应用程序从多个线程调用随机方法,则必须使用同步对象以确保一次只有一个线程可以访问随机数生成器。如果不确保以线程安全的方式访问Random对象,则对返回随机数的方法的调用将返回0。
如文档中所述,当多个线程使用相同的Random对象时,可能会发生非常令人讨厌的副作用:它只是停止工作。
(即存在竞争条件,当触发时,'random.Next ....'方法的返回值对于所有后续调用将为0。)
不,这不是线程安全的。如果需要使用来自不同线程的相同实例,则必须同步使用。
不过,我真的看不出你为什么会这么做的原因。每个线程拥有自己的Random类实例会更有效。
另一种线程安全的方法是使用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)));
}
适用于少量线程。
由于Random
不是线程安全的,因此每个线程应该有一个,而不是全局实例。如果你担心这些多个Random
类同时播种(即通过DateTime.Now.Ticks
等),你可以使用Guid
s为它们中的每一个播种。 .NET Guid
生成器需要相当长的时间来确保不可重复的结果,因此:
var rnd = new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0))
值得一提的是,这是一个线程安全,加密强大的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;
}
}
用于文档
此类型的任何公共静态(在Visual Basic中为Shared)成员都是线程安全的。任何实例成员都不保证是线程安全的。
对于线程安全的随机数生成器,请查看RNGCryptoServiceProvider。来自文档:
Thread Safety
这种类型是线程安全的。