C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법
전에 CallContext를 설명하면서,
HttpContext.Current를 통해 이해하는 CallContext와 ExecutionContext
; https://www.sysnet.pe.kr/2/0/1608
덧글로 잠시 소개를 했는데요,
Implicit Async Context ("AsyncLocal")
; https://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html
위의 글에서는
AsyncLocal 타입을 사용하지 않고 (.NET Core에서는 제공되지 않는) 순수
CallContext만을 이용해 await 호출 간의 문맥을 전달하고 있습니다. 코드를 여기다 그대로 옮겨 볼까요? ^^ 근래의 현실적인 기준으로 볼 때, (.NET Core/5에서 다중
AppDomain을 지원하지 않으므로)
MarshalByRefObject 처리를 없애면 다음과 같이 간단하게 정리할 수 있습니다.
// Install-Package System.Collections.Immutable
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Remoting.Messaging;
public static partial class MyStack
{
private static readonly string name = Guid.NewGuid().ToString("N");
private static ImmutableStack<string> CurrentContext
{
get
{
var ret = CallContext.LogicalGetData(name) as ImmutableStack<string>;
return ret == null ? ImmutableStack.Create<string>() : ret;
}
set
{
CallContext.LogicalSetData(name, value);
}
}
public static IDisposable Push([CallerMemberName] string context = "")
{
CurrentContext = CurrentContext.Push(context);
return new PopWhenDisposed();
}
private static void Pop()
{
CurrentContext = CurrentContext.Pop();
}
private sealed class PopWhenDisposed : IDisposable
{
private bool disposed;
public void Dispose()
{
if (disposed)
return;
Pop();
disposed = true;
}
}
public static string CurrentStack
{
get
{
return string.Join(" ", CurrentContext.Reverse());
}
}
}
이를 활용한 예제 코드는 변경 없이 원 글의 코드를 그대로 옮겨 봅니다.
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
using (MyStack.Push("Main"))
{
Task.WhenAll(SomeWork("1"), SomeWork("2")).Wait();
}
Console.ReadKey();
}
static async Task SomeWork(string stackName)
{
using (MyStack.Push(stackName))
{
Log("<SomeWork>");
await MoreWork("A");
await MoreWork("B");
Log("</SomeWork>");
}
}
static async Task MoreWork(string stackName)
{
using (MyStack.Push(stackName))
{
Log("<MoreWork>");
await Task.Delay(10);
Log("</MoreWork>");
}
}
static void Log(string message)
{
Console.WriteLine(MyStack.CurrentStack + ": " + message);
}
}
실행하면 다음과 같은 결과를 얻을 수 있습니다.
Main 1: <SomeWork>
Main 1 A: <MoreWork>
Main 2: <SomeWork>
Main 2 A: <MoreWork>
Main 2 A: </MoreWork>
Main 1 A: </MoreWork>
Main 2 B: <MoreWork>
Main 1 B: <MoreWork>
Main 2 B: </MoreWork>
Main 2: </SomeWork>
Main 1 B: </MoreWork>
Main 1: </SomeWork>
그런데, 사실 해당 기능을 너무 어렵게 만든 것이 아닌가... 하는 느낌입니다. 왜냐하면, CallContext는 스레드를 넘어가면서 shallow copy가 되는데, 달리 말하면 메서드에 인자를 넘기는 것과 같습니다. 따라서, 굳이 Push/IDisposable/Pop을 사용할 필요 없이 다음과 같은 식으로 바꿔도 무방합니다.
using System;
using System.Runtime.Remoting.Messaging;
public static partial class MyStack
{
private static readonly string idTitle = Guid.NewGuid().ToString("N");
private static string CurrentTitle
{
get
{
object objValue = CallContext.LogicalGetData(idTitle);
if (objValue == null)
{
return "";
}
return objValue as string;
}
set
{
CallContext.LogicalSetData(idTitle, value);
}
}
public static void Push(string title)
{
CurrentTitle = CurrentTitle + " " + title;
}
public static string CurrentStack
{
get
{
return CurrentTitle;
}
}
}
또한 사용 측 코드도 using을 빼고 단순히 Push 메서드만 호출해 주면 됩니다.
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
MyStack.Push("Main");
Task.WhenAll(SomeWork("1"), SomeWork("2")).Wait();
Console.ReadKey();
}
static async Task SomeWork(string stackName)
{
MyStack.Push(stackName);
Log("<SomeWork>");
await MoreWork("A");
await MoreWork("B");
Log("</SomeWork>");
}
static async Task MoreWork(string stackName)
{
MyStack.Push(stackName);
Log("<MoreWork>");
await Task.Delay(10);
Log("</MoreWork>");
}
static void Log(string message)
{
Console.WriteLine(MyStack.CurrentStack + ": " + message);
}
}
당연히 실행 결과는 이전과 다름없이 잘 나옵니다.
마지막으로, 이것을 AsyncLocal을 이용해 똑같이 동작하도록 코딩을 하면 다음과 같이 바뀝니다.
// .NET Framework 4.6 이상, .NET Core/5에서도 사용 가능
using System.Threading;
public static partial class MyStack
{
private static AsyncLocal<string> CurrentTitle = new AsyncLocal<string>();
public static void Push(string title)
{
CurrentTitle.Value = CurrentTitle.Value + " " + title;
}
public static string CurrentStack
{
get
{
return CurrentTitle.Value;
}
}
}
AsyncLocal이 CallContext에 대한 단순한 래퍼에 불과하기 때문에 우리가 기대한 동작이 나오는 건데요, 결과만 보면 어떤 것을 사용해도 무방합니다. 단지,
CallContext 타입이 .NET Framework에서만 허용되고 .NET Core/5에서는 사용할 수 없으므로 어쩔 수 없이 향후에는 AsyncLocal을 쓸 수밖에 없습니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]