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한 클래스를 만들어 사용하게 된다.
그 방식은 크게 두가지 방법 중 선택하게 된다.
- 공유 하는 Random객체의 Next() 호출 시 lock을 거는 방법.
- 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) |