HttpContextAccessor를 통해 이해하는 AsyncLocal<T>
HttpContext.Current가 동기 모델에 적합했다면,
HttpContext.Current를 통해 이해하는 CallContext와 ExecutionContext
; https://www.sysnet.pe.kr/2/0/1608
ASP.NET Core의 HttpContextAccessor는 비동기 모델을 위해 새롭게 나온 타입입니다.
ASP.NET의 HttpContext.Current 구현에 대응하는 ASP.NET Core의 IHttpContextAccessor/HttpContextAccessor 사용법
; https://www.sysnet.pe.kr/2/0/11440
그리고, HttpContextAccessor는 내부적으로 AsyncLocal<T>의 래퍼 클래스에 불과합니다.
AsyncLocal<T> Class
; https://learn.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1
[API Proposal]: Api handle Activity.Current value changes
; [API Proposal]: Api handle Activity.Current value changes
; https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-4/#added-new-tar-apis#observability
실제로 .NET Reflector 등의 도구로 HttpContextAccessor를 보면 다음과 같습니다.
// C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\3.1.8\Microsoft.AspNetCore.Http.dll
using System;
using System.Threading;
namespace Microsoft.AspNetCore.Http
{
// Token: 0x0200000F RID: 15
public class HttpContextAccessor : IHttpContextAccessor
{
public HttpContext HttpContext
{
get
{
HttpContextAccessor.HttpContextHolder value = HttpContextAccessor._httpContextCurrent.Value;
if (value == null)
{
return null;
}
return value.Context;
}
set
{
HttpContextAccessor.HttpContextHolder value2 = HttpContextAccessor._httpContextCurrent.Value;
if (value2 != null)
{
value2.Context = null;
}
if (value != null)
{
HttpContextAccessor._httpContextCurrent.Value = new HttpContextAccessor.HttpContextHolder
{
Context = value
};
}
}
}
private static AsyncLocal<HttpContextAccessor.HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextAccessor.HttpContextHolder>();
private class HttpContextHolder
{
public HttpContext Context;
}
}
public sealed class DefaultHttpContext : HttpContext
{
// ...[생략]...
}
public abstract class HttpContext
{
// ...[생략]...
}
}
스레드를 넘나드는 정보를 다루기 때문에 당연히 AsyncLocal은 내부적으로 ExecutionContext의 처리 과정을 래핑합니다.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace System.Threading
{
// Token: 0x020001F1 RID: 497
[NullableContext(1)]
[Nullable(0)]
public sealed class AsyncLocal<[Nullable(2)] T> : IAsyncLocal
{
// Token: 0x06001DED RID: 7661 RVA: 0x000A8969 File Offset: 0x000A7769
public AsyncLocal()
{
}
// Token: 0x06001DEE RID: 7662 RVA: 0x00118FF7 File Offset: 0x00117DF7
public AsyncLocal([Nullable(new byte[]
{
2,
0,
1
})] Action<AsyncLocalValueChangedArgs<T>> valueChangedHandler)
{
this.m_valueChangedHandler = valueChangedHandler;
}
// Token: 0x17000663 RID: 1635
// (get) Token: 0x06001DEF RID: 7663 RVA: 0x00119008 File Offset: 0x00117E08
// (set) Token: 0x06001DF0 RID: 7664 RVA: 0x0011902F File Offset: 0x00117E2F
public T Value
{
[return: MaybeNull]
get
{
object localValue = ExecutionContext.GetLocalValue(this);
if (localValue != null)
{
return (T)((object)localValue);
}
return default(T);
}
set
{
ExecutionContext.SetLocalValue(this, value, this.m_valueChangedHandler != null);
}
}
// Token: 0x06001DF1 RID: 7665 RVA: 0x00119048 File Offset: 0x00117E48
void IAsyncLocal.OnValueChanged(object previousValueObj, object currentValueObj, bool contextChanged)
{
T previousValue = (previousValueObj == null) ? default(T) : ((T)((object)previousValueObj));
T currentValue = (currentValueObj == null) ? default(T) : ((T)((object)currentValueObj));
this.m_valueChangedHandler(new AsyncLocalValueChangedArgs<T>(previousValue, currentValue, contextChanged));
}
// Token: 0x0400070A RID: 1802
private readonly Action<AsyncLocalValueChangedArgs<T>> m_valueChangedHandler;
}
}
따라서, 우리도 HttpContextAccessor처럼 AsyncLocal<T>를 사용해 스레드 간의 문맥 정보 전달을 할 수 있습니다. 다음은 이것을 테스트한 간단한 예제 코드입니다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Context1
{
public class MyRefType
{
public string Name;
public int Age;
public override string ToString()
{
return $"{Name}: {Age}";
}
}
class Program
{
static AsyncLocal<string> s_asyncText = new AsyncLocal<string>();
static AsyncLocal<int> s_asyncInt = new AsyncLocal<int>();
static AsyncLocal<MyRefType> s_asyncRef = new AsyncLocal<MyRefType>();
static async Task Main(string[] args)
{
int count = 3;
s_asyncRef.Value = new MyRefType { Name = $"User#{count}", Age = count };
while (count-- > 0)
{
s_asyncText.Value = $"TEST#{count}";
s_asyncInt.Value = count;
await AsyncMethodFirst();
OutputAsyncContext("AsyncMethodFirst - step4");
ThreadPool.QueueUserWorkItem((obj) =>
{
OutputAsyncContext("QueueUserWorkItem");
});
ThreadPool.UnsafeQueueUserWorkItem((obj) =>
{
OutputAsyncContext("UnsafeQueueUserWorkItem");
}, null);
Thread t = new Thread(() =>
{
OutputAsyncContext("new Thread");
});
t.Start();
Console.WriteLine();
s_asyncRef.Value = null;
}
Console.ReadLine();
}
private static async Task AsyncMethodFirst()
{
OutputAsyncContext("AsyncMethodFirst - step1");
await Task.Delay(1000);
OutputAsyncContext("AsyncMethodFirst - step2");
await Task.Delay(1000);
OutputAsyncContext("AsyncMethodFirst - step3");
await Task.Factory.StartNew(() => {
OutputAsyncContext("Task.Factory.StartNew");
});
}
private static void OutputAsyncContext(string title)
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {title}: {s_asyncText.Value} {s_asyncInt.Value}, {s_asyncRef.Value}");
}
}
}
/* 출력 결과
[1] AsyncMethodFirst - step1: TEST#2 2, User#3: 3
[4] AsyncMethodFirst - step2: TEST#2 2, User#3: 3
[4] AsyncMethodFirst - step3: TEST#2 2, User#3: 3
[4] Task.Factory.StartNew: TEST#2 2, User#3: 3
[4] AsyncMethodFirst - step4: TEST#2 2, User#3: 3
[5] QueueUserWorkItem: TEST#2 2, User#3: 3
[6] UnsafeQueueUserWorkItem: 0,
[4] AsyncMethodFirst - step1: TEST#1 1,
[8] new Thread: TEST#2 2, User#3: 3
[7] AsyncMethodFirst - step2: TEST#1 1,
[5] AsyncMethodFirst - step3: TEST#1 1,
[5] Task.Factory.StartNew: TEST#1 1,
[5] AsyncMethodFirst - step4: TEST#1 1,
[7] UnsafeQueueUserWorkItem: 0,
[4] QueueUserWorkItem: TEST#1 1,
[5] AsyncMethodFirst - step1: TEST#0 0,
[9] new Thread: TEST#1 1,
[6] AsyncMethodFirst - step2: TEST#0 0,
[4] AsyncMethodFirst - step3: TEST#0 0,
[7] Task.Factory.StartNew: TEST#0 0,
[7] AsyncMethodFirst - step4: TEST#0 0,
[5] UnsafeQueueUserWorkItem: 0,
[4] QueueUserWorkItem: TEST#0 0,
[10] new Thread: TEST#0 0,
*/
QueueUserWorkItem, UnsafeQueueUserWorkItem과 Thread에서 보이는 결과에 따르면, AsyncLocal은 LogicalCallContext로 구현된 듯합니다. .NET 초기 시절에, 과연 LogicalCallContext가 향후 async/await에서 유용하게 사용할 거라는 것을 누가 예상했을까요? ^^
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]