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#' 카테고리의 다른 글
ValueTask / ValueTask<T> (0) | 2024.05.02 |
---|---|
Task.Delay (vs Thread.Sleep) (2) | 2024.04.10 |
동시성 개요 (0) | 2024.04.01 |
struct 와 in, readonly (1) | 2024.03.20 |
가비지 없이 foreach 사용하기 (0) | 2024.01.21 |