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

    2021. 5. 9.

    by. xxx123

     

    .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)

    댓글