C #의 Random.Next()
메서드 스레드는 안전합니까?
C #의 Random.Next()
메서드 스레드는 안전합니까?
답변:
Next
스레드 안전성을 달성하기 위해 메서드 에서 특별히 수행 된 것은 없습니다 . 그러나 인스턴스 메서드입니다. Random
여러 스레드에서 인스턴스를 공유하지 않는 경우 인스턴스 내의 상태 손상에 대해 걱정할 필요가 없습니다. Random
일종의 배타적 잠금을 유지하지 않고 다른 스레드 에서 단일 인스턴스를 사용하지 마십시오 .
Jon Skeet은이 주제에 대해 몇 가지 멋진 게시물을 올렸습니다.
일부 해설자들이 언급했듯이, Random
스레드 배타적이지만 동일하게 시드 된 다른 인스턴스를 사용하는 데 또 다른 잠재적 인 문제 가 있습니다. 서로의 근접. 이 문제를 완화하는 한 가지 방법은 마스터 Random
인스턴스 (단일 스레드에 의해 잠김)를 사용하여 임의의 시드를 생성 Random
하고 사용할 다른 모든 스레드에 대해 새 인스턴스를 초기화 하는 것입니다.
Random
생성 된 방식 으로 인해 두 개의 개별 인스턴스가 Random
거의 동시에 두 개의 개별 스레드에서 생성되면 동일한 시드를 갖게되므로 동일한 값을 반환합니다. 해결 방법 은 내 대답 을 참조하십시오 .
Random
인스턴스에 대한 통계적 관계의 직교 문제 는 더 많은주의가 필요합니다.
아니요, 여러 스레드에서 동일한 인스턴스를 사용하면 중단되고 모두 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
있습니다.
ThreadSafeRandom
이것을 사용 하기 위해를 만들어야하는 방식이 마음에 들지 않습니다 . 현재 생성자 코드를 포함하는 게으른 게터와 함께 정적 속성을 사용하지 않는 이유는 무엇입니까? 여기에서 아이디어 : confluence.jetbrains.com/display/ReSharper/… 그러면 전체 클래스가 정적 일 수 있습니다.
IRandom
런타임에 호출을 static으로 리디렉션하는 클래스를 추가 하면 모의가 허용됩니다. 나를 위해 모든 멤버가 정적이라는 것은 정적 클래스를 가져야 함을 의미하며 사용자에게 더 많은 것을 알려줍니다. 각 인스턴스는 별도의 난수 시퀀스가 아니라 공유됩니다. 이것은 정적 프레임 워크 클래스를 모의해야 할 때 이전에 취한 접근 방식입니다.
_local
생성자에서 인스턴스화 할 수 없습니다.
Microsoft의 공식적인 답변은 매우 강력 하지 않습니다 . 에서 http://msdn.microsoft.com/en-us/library/system.random.aspx#8 :
임의의 개체는 스레드로부터 안전하지 않습니다. 앱이 여러 스레드에서 Random 메서드를 호출하는 경우 동기화 개체를 사용하여 한 번에 하나의 스레드 만 난수 생성기에 액세스 할 수 있도록해야합니다. 스레드로부터 안전한 방식으로 Random 개체에 액세스 할 수없는 경우 난수를 반환하는 메서드를 호출하면 0이 반환됩니다.
문서에 설명 된대로 동일한 Random 개체가 여러 스레드에서 사용될 때 발생할 수있는 매우 불쾌한 부작용이 있습니다.
(즉, 트리거 될 때 'random.Next ....'메서드의 반환 값이 모든 후속 호출에 대해 0이되는 경쟁 조건이 있습니다.)
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.
아니요, 스레드로부터 안전하지 않습니다. 다른 스레드에서 동일한 인스턴스를 사용해야하는 경우 사용을 동기화해야합니다.
그래도 왜 필요한지 모르겠습니다. 각 스레드가 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)));
}
적은 수의 스레드에서 작동합니다.
++SeedCount
경쟁 조건을 소개합니다. Interlocked.Increment
대신 사용하십시오 .
때문에 Random
스레드 안전하지 않습니다, 당신은 스레드 당 하나가 아닌 전역 인스턴스가 있어야합니다. 이러한 여러 Random
클래스가 동시에 시드되는 것이 걱정된다면 (예 :에 의해 DateTime.Now.Ticks
) Guid
s를 사용 하여 각 클래스를 시드 할 수 있습니다. .NET Guid
생성기는 반복 불가능한 결과를 보장하기 위해 상당한 길이로 진행됩니다.
var rnd = new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0))
NewGuid
는 실제로 고유 한 것으로 보장되지만 해당 GUID의 처음 4 바이트 (모두 확인 BitConverter.ToInt32
)는 그렇지 않습니다. 일반적으로 GUID의 하위 문자열을 고유 한 것으로 취급하는 것은 끔찍한 생각 입니다.
Guid.NewGuid
, 적어도 Windows 에서는 대부분 무작위로 생성되는 버전 4 GUID 를 사용한다는 것입니다. 특히 처음 32 비트는 무작위로 생성되므로 본질적으로 Random
인스턴스에 20 억분의 1의 충돌 확률로 (아마도 암호 학적으로?) 난수를 시드 하는 것입니다. 하지만이 를 확인하는 데 몇 시간 이 걸렸지 만 여전히 .NET Core가 NewGuid()
비 Windows OS에서 어떻게 작동 하는지 전혀 모릅니다 .
그 가치가 무엇인지, 여기에 스레드로부터 안전한 암호화로 강력한 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();
?
다음을 사용하여 BlueRaja의 답변을 다시 구현합니다 ThreadLocal
.
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
. 😃
Random
... 같은 시간에
Lazy<T>
이 경우 클래스 이벤트 아무것도 없습니다.
문서 당
이 형식의 모든 공용 정적 (Visual Basic에서 공유) 멤버는 스레드로부터 안전합니다. 모든 인스턴스 멤버는 스레드 안전이 보장되지 않습니다.
Random
정적 멤버가 없기 때문에 인용 된 문서는 모든 멤버가 "스레드 안전이 보장되지 않는다" 고 효과적으로 설명합니다 . 당신은 이것이 틀렸다고 말한 다음 ... Random
스레드로부터 안전하지 않다고 말함으로써 이것을 백업하십시오 . 그것은 문서가 말한 것과 거의 동일합니다!
스레드 안전 난수 생성기의 경우 RNGCryptoServiceProvider를 참조하십시오 . 문서에서 :
스레드 안전성
이 유형은 스레드로부터 안전합니다.
시드에 대한 잠금없는 알고리즘을 사용하여 기존 스레드 로컬 스토리지 접근 방식 을 개선 할 수 있습니다. 다음은 자바의 알고리즘에서 뻔뻔스럽게 도난당했습니다 (아마도 개선 될 수도 있습니다).
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;
}
}
int
는 이것의 요점을 패배시킵니다. 자바Random
는으로 시드 long
되지만 C #은 int를 취하고 더 나쁘게도 서명 된 int 의 절대 값 을 시드로 사용하므로 사실상 2 ^ 31 개의 개별 시드 만 있습니다. 좋은 long
종자 생성 을 갖는 것은 당신이 대부분의 비트를 버리고 있다면 일종의 낭비입니다 long
. 임의의 시드가 완벽하게 무작위 인 경우에도 C #의 임의 시드를 무작위로 시드하면 약 20 억 분의 1의 충돌 가능성이 남습니다.