Programming/C#

[.NET / C#] Random의 스레드 안정성

xxx123 2021. 5. 9. 18:59

 

.NET 5.0까지의 Random

기본적으로 현재 최신버전인 .NET 5.0의 Random클래스는 ThreadSafe하지 않다.

구현 내용을 보면 다음과 같이 멤버 변수로 ImplBase타입을 가지고 있는데,

public partial class Random
{
        private readonly ImplBase _impl;
        
 .....생략

 

ImplBase를 상속 받은 클래스 또한 멤버 변수를 가지고 있고, 이 멤버변수를 이용해 Random관련 연산을 진행하게 된다.

public partial class Random
{ 
    internal sealed class XoshiroImpl : ImplBase
    {
        private uint _s0, _s1, _s2, _s3;  // 멤버 변수를 이용해 연산

.....생략

        internal uint NextUInt32()
        {
            uint s0 = _s0, s1 = _s1, s2 = _s2, s3 = _s3;

            uint result = BitOperations.RotateLeft(s1 * 5, 7) * 9;
            uint t = s1 << 9;

            s2 ^= s0;
            s3 ^= s1;
            s1 ^= s2;
            s0 ^= s3;

            s2 ^= t;
            s3 = BitOperations.RotateLeft(s3, 11);

            _s0 = s0;
            _s1 = s1;
            _s2 = s2;
            _s3 = s3;

            return result;
         }

.....생략

 

위와 같은 구현이기에 당연히 다중 스레드 상황에서 하나의 Random 객체를 공유하여 사용하면 문제가 발생하게 되고,

우리는 다중 스레드에 안전하게 사용하기 위해 Random 클래스를 한번 더 Wapping한 클래스를 만들어 사용하게 된다.

그 방식은 크게 두가지 방법 중 선택하게 된다.

 

  1. 공유 하는 Random객체의 Next() 호출 시 lock을 거는 방법.
  2. TLS(Thread Local Storage)를 통해 Thread 마다 별도의 Random 클래스를 생성하여 사용하는 방법.

 

1번의 경우 빈번하게 Next() 호출 시 부담이 될 수 있기에 일반적으로 2번과 같은 TLS 방식이 선호되어 왔다.

2번 방식의 Wapper클래스 구현은 다음과 같다.

public static class  ThreadSafeRandom
{
    public static int Next(int min, int max)
    {
        if (min > max)
        {
            throw new ArgumentException($"[RandomWapper::Next] min:{min} max:{max}");
        }

        return TlsRandom.Instance.Next(min, max); // 스레드마다 각각 가지고 있는 Random 객체를 사용.
    }

.....생략

    private static class TlsRandom
    {
        private static readonly ThreadLocal<System.Random> Random = new ThreadLocal<System.Random>(() =>
        {
            int seed = Environment.TickCount;
            return new System.Random(seed);
        });

        internal static System.Random Instance => Random.Value;
    }
}

 

 

 

.NET 6.0에서의 Random

위와 같이 번거롭게 Warpper클래스를 만드는 수고를 .NET 6.0에서는 조금 덜 수 있을 것 같다.

다음은 .NET 6.0에서의 Random 클래스이다.

public partial class Random
{

.....생략

    public static Random Shared { get; } = new ThreadSafeRandom();
    
.....생략

    private sealed class ThreadSafeRandom : Random
    {
        [ThreadStatic]
        private static XoshiroImpl? t_random; // TLS로 random 값을 가진다.

        public ThreadSafeRandom() : base(isThreadSafeRandom: true) { }

        private static XoshiroImpl LocalRandom => t_random ?? Create();

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static XoshiroImpl Create() => t_random = new();

        public override int Next(int minValue, int maxValue)
        {
            if (minValue > maxValue)
            {
                ThrowMinMaxValueSwapped();
            }

            int result = LocalRandom.Next(minValue, maxValue);
            AssertInRange(result, minValue, maxValue);
            return result;
        }
    

Random클래스의 Shared 호출 시 ThreadSafeRandom의 객체를 사용하게 되고 내부적으로는 스레드마다 각각 가지고 있는 LocalRandom값을 사용하여 서로 다른 Random클래스의 Next()함수를 호출하게 된다.

즉, 우리가 일반적으로 만들어 사용하던 TLS 방식을 내장해준 것이다.

 

static void Main(string[] args)
{
     int result = Random.Shared.Next(0, 1000);
}

 

사용법 또한 위와 같이 매우 간단해질 것이다.

 

 

[참고]

.NET 6.0 (preview)