C # 난수 생성기는 스레드 안전합니까?


79

C #의 Random.Next()메서드 스레드는 안전합니까?


1
“이 유형의 모든 공용 정적 (Visual Basic에서 공유) 멤버는 스레드로부터 안전합니다. 인스턴스 멤버는 스레드로부터 안전하지 않을 수 있습니다. " ( System.Random 문서에서 ). [좋아요, 공정하게 말하면, 사람들이 가지고있는 것처럼 보이는 의사 난수에 대한 가장 일반적인 문제 거기에 설명되어 있고 여전히 계속 묻습니다.]
Joey

답변:


33

Next스레드 안전성을 달성하기 위해 메서드 에서 특별히 수행 된 것은 없습니다 . 그러나 인스턴스 메서드입니다. Random여러 스레드에서 인스턴스를 공유하지 않는 경우 인스턴스 내의 상태 손상에 대해 걱정할 필요가 없습니다. Random일종의 배타적 잠금을 유지하지 않고 다른 스레드 에서 단일 인스턴스를 사용하지 마십시오 .

Jon Skeet은이 주제에 대해 몇 가지 멋진 게시물을 올렸습니다.

StaticRandom
무작위성 재 방문

일부 해설자들이 언급했듯이, Random스레드 배타적이지만 동일하게 시드 된 다른 인스턴스를 사용하는 데 또 다른 잠재적 인 문제 가 있습니다. 서로의 근접. 이 문제를 완화하는 한 가지 방법은 마스터 Random인스턴스 (단일 스레드에 의해 잠김)를 사용하여 임의의 시드를 생성 Random하고 사용할 다른 모든 스레드에 대해 새 인스턴스를 초기화 하는 것입니다.


13
"여러 스레드간에 Random 인스턴스를 공유하지 않으면 걱정할 필요가 없습니다." -이것은 거짓입니다. Random생성 된 방식 으로 인해 두 개의 개별 인스턴스가 Random거의 동시에 두 개의 개별 스레드에서 생성되면 동일한 시드를 갖게되므로 동일한 값을 반환합니다. 해결 방법 은 내 대답 을 참조하십시오 .
BlueRaja - 대니 Pflughoeft

6
@BlueRaja 저는 특히 단일 인스턴스 내에서 상태 부패 문제를 표적으로 삼았습니다. 물론 언급했듯이 두 개의 개별 Random인스턴스에 대한 통계적 관계의 직교 문제 는 더 많은주의가 필요합니다.
MMX

23
왜 이것이 답으로 표시되는지 모르겠습니다! Q : "Random.Next 스레드는 안전한가요?" A : "하나의 스레드에서만 사용하면 스레드로부터 안전합니다.".... 최악의 대답!
Mick

당신이 할 수있는 최악의 일은 답변을 제공하기 위해 외부 기사를 사용하는 것과 같으며 실제로 답변을 포함하지 않는 것입니다. 특히 이렇게 간단 할 때.
Anthony Nichols

"그들은 같은 시드를 가질 것입니다."이것은 .Net Core에서 수정되었습니다.
Magnus

87

아니요, 여러 스레드에서 동일한 인스턴스를 사용하면 중단되고 모두 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이것을 사용 하기 위해를 만들어야하는 방식이 마음에 들지 않습니다 . 현재 생성자 코드를 포함하는 게으른 게터와 함께 정적 속성을 사용하지 않는 이유는 무엇입니까? 여기에서 아이디어 : confluence.jetbrains.com/display/ReSharper/… 그러면 전체 클래스가 정적 일 수 있습니다.
weston

2
@weston : 일반적으로 안티 패턴으로 간주됩니다. 모든 OOP 이점을 잃는 것 외에도, 주된 이유는 정적 객체를 모의 할 수 없다는 것입니다 . 이는이를 사용하는 모든 클래스의 단위 테스트에 필수적입니다.
BlueRaja-Danny Pflughoeft

3
필요할 때 언제든지 간접 레이어를 추가 할 수 있습니다. 예를 들어를 추가하고 IRandom런타임에 호출을 static으로 리디렉션하는 클래스를 추가 하면 모의가 허용됩니다. 나를 위해 모든 멤버가 정적이라는 것은 정적 클래스를 가져야 함을 의미하며 사용자에게 더 많은 것을 알려줍니다. 각 인스턴스는 별도의 난수 시퀀스가 ​​아니라 공유됩니다. 이것은 정적 프레임 워크 클래스를 모의해야 할 때 이전에 취한 접근 방식입니다.
weston

6
_local이 액세스 될 때마다 생성되지 않기 때문에 실제로 여러 스레드에서 코드를 사용하면 코드가 작동하지 않습니다 : NullRefereceException.
Laie 2015-04-21

3
이전 주석에서 언급했듯이이 접근 방식은 여러 스레드에서 사용할 때 실제로 작동하지 않습니다. _local생성자에서 인스턴스화 할 수 없습니다.
Alex

23

Microsoft의 공식적인 답변은 매우 강력 하지 않습니다 . 에서 http://msdn.microsoft.com/en-us/library/system.random.aspx#8 :

임의의 개체는 스레드로부터 안전하지 않습니다. 앱이 여러 스레드에서 Random 메서드를 호출하는 경우 동기화 개체를 사용하여 한 번에 하나의 스레드 만 난수 생성기에 액세스 할 수 있도록해야합니다. 스레드로부터 안전한 방식으로 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 개체에서 ZERO 결과를
Agustin Garzon

14

아니요, 스레드로부터 안전하지 않습니다. 다른 스레드에서 동일한 인스턴스를 사용해야하는 경우 사용을 동기화해야합니다.

그래도 왜 필요한지 모르겠습니다. 각 스레드가 Random 클래스의 자체 인스턴스를 갖는 것이 더 효율적입니다.


단위 테스트 개체가 있고 한 번에 많은 테스트 개체를 생성하려는 경우 이로 인해 엉덩이를 물릴 수 있습니다. 그 이유는 많은 사람들이 사용의 편의를 위해 Random을 전역 개체로 만들기 때문입니다. 나는 방금 그것을했고 rand.Next ()는 0을 값으로 계속 생성했습니다.
JSWork

1
@JSWork : 나는 당신이 의미하는 바를 정말로 따르지 않습니다. "this"라고 할 때 무엇을 언급합니까? 마지막 문장을 올바르게 이해하면 동기화하지 않고 스레드 전체에서 객체에 액세스하여 결과를 설명합니다.
Guffa 2011 년

1
당신이 올바른지. 사과드립니다-나는 이것을 잘못 표현했습니다. 당신이 언급 한 것을하지 않으면 엉덩이를 물 수 있습니다. 주의 사항으로, 독자는 모든 스레드에 대해 새로운 임의의 개체를 만들 때주의해야합니다. 임의의 개체는 현재 시간을 시드로 사용합니다. 시드 변경 사이에는 10ms의 간격이 있습니다.
JSWork

3
@JSWork : 예, 스레드가 동시에 시작되면 타이밍 문제가 있습니다. 메인 스레드에서 하나의 Random 객체를 사용하여 스레드가이를 우회 할 수 있도록 Random 객체를 생성하기위한 시드를 제공 할 수 있습니다.
Guffa 2011 년

1
각 스레드에 대해 별도의 무작위를 만들려면 스레드 ID 또는 시간 + 스레드 ID 또는 이와 유사한 것과 같이 시드에 대해 각 스레드에 더 고유 한 것을 사용하십시오.
apokryfos 2013 년

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
그러나이 경우 메서드가 스레드로부터 안전 할 필요가 없는지 확인하지 않습니다. 각 스레드는 개체의 자체 복사본에 액세스합니다.
gap

4
++SeedCount경쟁 조건을 소개합니다. Interlocked.Increment대신 사용하십시오 .
Edward Brey 2012

1
OP에서 언급했듯이 이것은 제한된 수의 스레드에서 작동합니다. 이것은 ASP.NET 내부에서 좋지 않은 선택
Chris Marisic 2014 년

1
Random 생성자에서 GenerateSeed ()에 대한 호출을 Guid.NewGuid (). GetHashCode ())로 대체 할 수 있다고 생각합니다.
Siavash Mortazavi

5

때문에 Random스레드 안전하지 않습니다, 당신은 스레드 당 하나가 아닌 전역 인스턴스가 있어야합니다. 이러한 여러 Random클래스가 동시에 시드되는 것이 걱정된다면 (예 :에 의해 DateTime.Now.Ticks) Guids를 사용 하여 각 클래스를 시드 할 수 있습니다. .NET Guid생성기는 반복 불가능한 결과를 보장하기 위해 상당한 길이로 진행됩니다.

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

2
-1; 로 생성 된 GUID NewGuid는 실제로 고유 한 것으로 보장되지만 해당 GUID의 처음 4 바이트 (모두 확인 BitConverter.ToInt32)는 그렇지 않습니다. 일반적으로 GUID의 하위 문자열을 고유 한 것으로 취급하는 것은 끔찍한 생각 입니다.
Mark Amery

2
이 특별한 경우에이 접근 방식을 사용하는 유일한 것은 .NET의 Guid.NewGuid, 적어도 Windows 에서는 대부분 무작위로 생성되는 버전 4 GUID 를 사용한다는 것입니다. 특히 처음 32 비트는 무작위로 생성되므로 본질적으로 Random인스턴스에 20 억분의 1의 충돌 확률로 (아마도 암호 학적으로?) 난수를 시드 하는 것입니다. 하지만이 를 확인하는 데 몇 시간 이 걸렸지 만 여전히 .NET Core가 NewGuid()비 Windows OS에서 어떻게 작동 하는지 전혀 모릅니다 .
Mark Amery

@MarkAmery OP는 암호화 품질이 필요한지 여부를 지정하지 않았으므로 그 대답은 중요하지 않은 상황에서 빠른 코딩을위한 한 줄로 여전히 유용하다고 생각합니다. 첫 번째 의견을 바탕으로 처음 4 바이트를 피하기 위해 코드를 수정했습니다.
Glenn Slayden

1
처음 4 개 대신 두 번째 4 바이트를 사용하는 것은 도움이되지 않습니다. 나는 GUID의 처음 4 바이트 고유 보장되지 않습니다 말했을 때, 내가 무엇을 의미하는 것이 었 에는 4 바이트 는 GUID의이 독특한 있어야되지 않습니다; 전체 16 바이트 GUID는 있지만 그보다 작은 부분은 아닙니다. 버전 4 GUID (Windows의 .NET에서 사용됨)의 경우 두 번째 4 바이트에는 고정 값이있는 4 개의 비 무작위 비트가 포함되기 때문에 실제로 변경으로 인해 상황이 악화되었습니다. 당신의 편집은 가능한 시드 값의 수를 수억 이하로 줄였습니다.
Mark Amery

알았어 고마워. 변경 사항을 되돌 렸으며, 귀하가 제기 한 우려 사항이 있으면 사람들이 귀하의 의견에 귀를 기울일 수 있습니다.
Glenn

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();?
Chris Marisic 2014 년

@ChrisMarisic, 나는 아래 링크 된 패턴을 따르고 있었다. 그러나 잠금의 지연 인스턴스화에는 최소한의 이점이 있으므로 제안이 합리적으로 보입니다. csharpindepth.com/articles/general/singleton.aspx#lazy
Jodrell

이것은 훌륭한 솔루션처럼 보이지만 BitConverter.ToUInt32 대 BitConverter.ToInt32를 사용하는 이유에 대해 몇 가지 질문이 있습니다. GetNext ()를 정리할까요? 그리고 왜 풀을 정적으로 만드나요? 여러 풀에서 절약 할 수 있지만 SafeRandom 인스턴스가 많은 동시 시스템에서는 병목 현상이 발생할 수도 있습니다. RNGCryptoServiceProvider를 시드하는 방법은 무엇입니까?
Wouter

3

다음을 사용하여 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. 😃
Theodor Zoulias

1
@TheodorZoulias 확실히 나는 문제는 초기화되지 않습니다, 따르지 - 우리는 글로벌 잠금을 떨어 뜨리면 우리가이 개 스레드 세계에 접근 할 수 Random... 같은 시간에
오핫 슈나이더

1
Ohad 당신이 옳습니다. 내가 무슨 생각을했는지 모르겠다. Lazy<T>이 경우 클래스 이벤트 아무것도 없습니다.
Theodor Zoulias

2

문서 당

이 형식의 모든 공용 정적 (Visual Basic에서 공유) 멤버는 스레드로부터 안전합니다. 모든 인스턴스 멤버는 스레드 안전이 보장되지 않습니다.

http://msdn.microsoft.com/en-us/library/system.random.aspx


2
인용 한 문서는 실제로 올바르지 않습니다. 기계에서 생성 된 콘텐츠라고 생각합니다. MSDN의 해당 주제에 대한 커뮤니티 콘텐츠에는 Random 유형이 스레드로부터 안전하지 않은 이유와이 문제를 해결하는 방법 (암호화 또는 "세마포"사용)에 대한 많은 정보가 포함되어 있습니다.

2
@MTG 귀하의 의견이 혼란 스럽습니다. Random정적 멤버가 없기 때문에 인용 된 문서는 모든 멤버가 "스레드 안전이 보장되지 않는다" 고 효과적으로 설명합니다 . 당신은 이것이 틀렸다고 말한 다음 ... Random스레드로부터 안전하지 않다고 말함으로써 이것을 백업하십시오 . 그것은 문서가 말한 것과 거의 동일합니다!
Mark Amery

책상에 머리를 앞머리. 예, 맞습니다. "문서가 말한 것과 거의 동일합니다!"
Seattle Leonard

1

스레드 안전 난수 생성기의 경우 RNGCryptoServiceProvider를 참조하십시오 . 문서에서 :

스레드 안전성

이 유형은 스레드로부터 안전합니다.


1
예, RNGCryptoServiceProvider는 금입니다. 임의의 바이트를 생성하기 때문에 특정 유형의 숫자를 뱉어내는 데 약간 더 많은 작업이 있지만 Random보다 훨씬 낫습니다.
Rangoric

1
@Rangoric : 아니요, 둘 중 하나를 사용할 수있는 모든 목적에 대해 Random보다 낫지 않습니다. 암호화 목적으로 임의성을 필요로하는 경우 Random 클래스는 옵션이 아닙니다. 어떤 용도로든 선택할 수있는 경우 Random 클래스가 더 빠르고 사용하기 쉽습니다.
Guffa 2010-06-16

@Guffa 사용 용이성은 이미 라이브러리에 넣었으므로 한 번만 사용하므로 실제로 좋은 점은 아닙니다. 더 빠르다는 것이 유효한 포인트이지만, 좋은 무작위성보다는 실제 무작위성을 갖기를 원합니다. 이를 위해 스레드로부터 안전합니다. 지금은 얼마나 느린 지 확인하기 위해 이것을 테스트하려고하지만 (Random은 복식을 생성하고 요청한 것으로 변환하므로 정확히 필요한 숫자 범위에 따라 달라질 수 있습니다)
Rangoric

C #에서는 매우 기본적인 RNGCrypto 구현을 사용하면 찾고있는 정확한 숫자에 따라 약 100 : 1이됩니다 (예를 들어 127은 128보다 두 배나 좋습니다). 다음으로 스레딩을 추가하고 어떻게 작동하는지 볼 계획입니다 (왜 안
되겠습니까

위의 진술로 업데이트하십시오. 256 값 미만의 범위에 대해 2 : 1에 도달했으며 원하는 숫자가 256의 인수에 가까울수록 좋습니다.
Rangoric

-1

업데이트 : 그렇지 않습니다. .Next () 메서드를 호출하는 동안 일부 "세마포어"개체를 잠그고 각 연속 호출에서 Random 인스턴스를 재사용하거나 각 호출에서 보장 된 임의 시드가있는 새 인스턴스를 사용해야합니다. Yassir가 제안한대로 .NET에서 암호화를 사용하여 보장 된 다른 시드를 얻을 수 있습니다.


-1

시드에 대한 잠금없는 알고리즘을 사용하여 기존 스레드 로컬 스토리지 접근 방식 을 개선 할 수 있습니다. 다음은 자바의 알고리즘에서 뻔뻔스럽게 도난당했습니다 (아마도 개선 될 수도 있습니다).

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는 이것의 요점을 패배시킵니다. 자바Random 는으로 시드 long되지만 C #은 int를 취하고 더 나쁘게도 서명 된 int 의 절대 값 을 시드로 사용하므로 사실상 2 ^ 31 개의 개별 시드 만 있습니다. 좋은 long종자 생성 을 갖는 것은 당신이 대부분의 비트를 버리고 있다면 일종의 낭비입니다 long. 임의의 시드가 완벽하게 무작위 인 경우에도 C #의 임의 시드를 무작위로 시드하면 약 20 억 분의 1의 충돌 가능성이 남습니다.
Mark Amery
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.