ThreadLocal<T>
ThreadLocal<T>는 쓰레드별로 서로 고유한 값을 갖는 컨테이너이다.
class Program
{
//#1
public static readonly ThreadLocal<System.Random> ThreadLocalRand =
new ThreadLocal<Random>(() => new System.Random(0), false);
public static void Main(string[] args)
{
//#2
using var threadResults = new ThreadLocal<List<int>>(true);
for(int i = 0 ; i < 100; ++i)
ThreadPool.QueueUserWorkItem(SomeWork, threadResults);
Thread.Sleep(1000); //작업이 다 완료될때까지 충분히 대기
WriteResultsToConsole(threadResults);
}
//#3
private static void SomeWork(object state)
{
Thread.Sleep(1);
var resultContainer = state as ThreadLocal<List<int>>;
if (false == resultContainer.IsValueCreated)
resultContainer.Value = new List<int>();
resultContainer.Value.Add(ThreadLocalRand.Value.Next(100));
}
//#4
private static void WriteResultsToConsole(ThreadLocal<List<int>> results)
{
StringBuilder sb = new StringBuilder();
foreach (var result in results.Values)
{
foreach (var elem in result)
{
sb.Append(elem);
sb.Append(' ');
}
Console.WriteLine(sb.ToString());
sb.Clear();
}
}
}
위 코드의 목표는 쓰레드 풀을 이용하여 난수를 생성하고 저장하는데, 그 시퀀스가 모든 쓰레드마다 동일하도록 하는 것이다.
이를 위해서는 쓰레드마다 고유의 난수 생성기와 값을 저장할 컨테이너를 갖게 해야한다.
코드를 하나씩 살펴보자.
#1 ThreadLocal<T> 의 생성1
//#1
public static readonly ThreadLocal<System.Random> ThreadLocalRand =
new ThreadLocal<Random>(() => new System.Random(0), false);
ThreadLocal<T>는 생성자에 최대 2개의 인자를 받을 수 있다. 첫째는 valueFactory, 다른 하나는 trackAllValues이다.
- valueFactory는 T값을 리턴하는 함수이다. ThreadLocal<T>에 새로운 쓰레드가 접근하면 해당 메서드를 실행시켜 쓰레드 고유의 값을 초기화한다. 이 값이 null이라면 따로 값을 초기화해주어야 한다. (Reference Type인 경우). 기본값은 null.
- trackAllValues는 다른 쓰레드에서 자신의 고유 값에 접근할 수 있는지를 나타낸다. 기본값은 false
위 코드는 시드를 0으로 갖는 난수 생성기를 각 쓰레드에 배정하는 것이 의도이다. 다른 쓰레드에서 개별 쓰레드의 난수 생성기에 접근할 일은 없기 때문에 trackAllValues는 false로 둔다.
#2 ThreadLocal<T>의 생성2
public static void Main(string[] args)
{
//#2
using var threadResults = new ThreadLocal<List<int>>(true);
for(int i = 0 ; i < 100; ++i)
ThreadPool.QueueUserWorkItem(SomeWork, threadResults);
Thread.Sleep(1000); //작업이 다 완료될때까지 충분히 대기
WriteResultsToConsole(threadResults);
}
난수 생성기와 다르게 결과 값을 저장할 컨테이너에는 trackAllValues를 true로 지정한다. 이는 맨 아래에서 결과값을 출력할때 필요하다.
코드는 쓰레드 풀에 100개의 작업을 분배한다. 각 작업은 난수생성기로 난수를 생성하여 컨테이너에 저장한다.
#3 활용
//#3
private static void SomeWork(object state)
{
Thread.Sleep(1);
var resultContainer = state as ThreadLocal<List<int>>;
if (false == resultContainer.IsValueCreated)
resultContainer.Value = new List<int>();
resultContainer.Value.Add(ThreadLocalRand.Value.Next(100));
}
ThreadLocal<List<int>>에 우선 값이 있는지 확인한다. 이는 IsValueCreated로 체크할 수 있다. 만약 값이 없다면 새로 초기화해준다.
이 과정은 ThreadLocal에 valueFactory를 넘기는 것으로 생략할 수 있다.
using var threadResults = new ThreadLocal<List<int>>(()=>new List<int>(), true);
그 후에는 static 변수인 ThreadLocalRand에 접근하여 난수를 하나 생성한 뒤, 이를 컨테이너에 저장한다.
#4 활용2
private static void WriteResultsToConsole(ThreadLocal<List<int>> results)
{
StringBuilder sb = new StringBuilder();
foreach (var result in results.Values)
{
foreach (var elem in result)
{
sb.Append(elem);
sb.Append(' ');
}
Console.WriteLine(sb.ToString());
sb.Clear();
}
}
ThreadLocal<T>.values를 각 쓰레드들의 컨테이너를 순회한다. 만약 ThreadLocal의 trackAllValues가 false라면 System.InvalidOperationException이 발생한다.
결과
| 72 81 76 55 20 55 90 44 72 81 76 55 20 55 90 44 72 81 76 55 20 55 90 44 97 72 81 76 55 20 55 90 44 72 81 76 55 20 55 90 44 97 72 81 76 55 20 55 90 44 72 81 76 55 20 55 90 44 97 72 81 76 55 20 55 90 44 72 81 76 55 20 55 90 44 72 81 76 55 20 55 90 44 72 81 76 55 20 55 90 44 97 72 81 76 55 20 55 90 44 |
컨테이너별로 저장된 시퀀스가 동일함을 알 수 있다. 이로 말미암아
- 각 쓰레드가 고유의 난수 생성기를 사용했음
- 각 쓰레드가 고유의 컨테이너를 가지고 있음
을 검증할 수 있다.
AsyncLocal<T>
ThreadLocal<T>가 쓰레드별로 고유한 값을 가진다면, AsyncLocal<T>는 Task별로 고유한 값을 가진다.
class Program
{
public static void Main(string[] args)
{
//#1
AsyncLocal<System.Random> asyncLocalRand = new AsyncLocal<Random>();
AsyncLocal<List<(int, int)>> asyncLocalResults = new AsyncLocal<List<(int, int)>>();
Task[] tasks = new Task[5];
for (int i = 0; i < 5; ++i)
tasks[i] = SomeWork(i+1, asyncLocalRand, asyncLocalResults);
Task.WaitAll(tasks);
}
//#2
private static async Task SomeWork(int taskId, AsyncLocal<System.Random> asyncLocalRand, AsyncLocal<List<(int,int)>> asyncLocalResults)
{
asyncLocalRand.Value = new System.Random(0);
asyncLocalResults.Value = new List<(int,int)>();
for (int i = 0; i < 10; ++i)
await MakeRand(asyncLocalRand, asyncLocalResults).ConfigureAwait(false);
PrintResult(taskId, asyncLocalResults.Value);
}
//#3
private static async Task MakeRand(AsyncLocal<System.Random> asyncLocalRand, AsyncLocal<List<(int,int)>> asyncLocalResults)
{
await Task.Delay(10);
asyncLocalResults.Value.Add((asyncLocalRand.Value.Next(100), Thread.CurrentThread.ManagedThreadId));
}
private static void PrintResult(int taskId, List<(int, int)> results)
{
StringBuilder sb = new StringBuilder();
sb.Append($"TaskID {taskId} results : ");
foreach(var ret in results)
{
sb.Append($"{ret.Item1}({ret.Item2.ToString("D2")})");
sb.Append(' ');
}
Console.WriteLine(sb.ToString());
}
}
위 코드는 5개의 태스크를 생성한다. 그리고 각 태스크는 난수를 만들고 이를 컨테이너에 저장하는 작업을 10번 시작한다.
그리고 태스크별로 저장한 값을 출력한다. 괄호 안은 해당 태스크를 실행시킨 쓰레드의ID이다.
실행 결과는 다음과 같다.
| TaskID 4 results : 72(05) 81(10) 76(06) 55(04) 20(07) 55(04) 90(18) 44(09) 97(08) 27(10) TaskID 2 results : 72(07) 81(04) 76(10) 55(07) 20(06) 55(07) 90(07) 44(18) 97(07) 27(05) TaskID 1 results : 72(08) 81(05) 76(05) 55(10) 20(05) 55(08) 90(10) 44(17) 97(18) 27(07) TaskID 3 results : 72(06) 81(07) 76(04) 55(05) 20(04) 55(10) 90(08) 44(07) 97(09) 27(08) TaskID 5 results : 72(04) 81(06) 76(07) 55(06) 20(10) 55(06) 90(17) 44(10) 97(10) 27(18) |
08번 쓰레드가 생성한 값을 추적해보자. 중간 중간 값을 건너 뛰어 생성하고 있음을 알 수 있다. 즉, ThreadLocal과 달리 Task마다 고유한 값을 갖으며 thread와는 상관이 없다.
'언어 > C#' 카테고리의 다른 글
| C#의 메모리 정렬 (StructLayout) (0) | 2025.04.17 |
|---|---|
| 성능 측정 - DotnetBenchmark 퀵세팅 (0) | 2025.03.11 |
| ValueTask / ValueTask<T> (0) | 2024.05.02 |
| Task.Delay (vs Thread.Sleep) (2) | 2024.04.10 |
| 동시성 개요 (0) | 2024.04.01 |