Microsoft MVP성태의 닷넷 이야기
.NET Framework: 989. HttpContextAccessor를 통해 이해하는 AsyncLocal<T> [링크 복사], [링크+제목 복사],
조회: 19280
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 4개 있습니다.)
.NET Framework: 412. HttpContext.Current를 통해 이해하는 CallContext와 ExecutionContext
; https://www.sysnet.pe.kr/2/0/1608

.NET Framework: 727. ASP.NET의 HttpContext.Current 구현에 대응하는 ASP.NET Core의 IHttpContextAccessor/HttpContextAccessor 사용법
; https://www.sysnet.pe.kr/2/0/11440

.NET Framework: 989. HttpContextAccessor를 통해 이해하는 AsyncLocal<T>
; https://www.sysnet.pe.kr/2/0/12467

.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법
; https://www.sysnet.pe.kr/2/0/12706




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에서 유용하게 사용할 거라는 것을 누가 예상했을까요? ^^

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/13/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-01-05 10시59분
정성태

... 121  122  123  124  [125]  126  127  128  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
10798정성태5/26/201522095기타: 53. 2015년 6월 10일 밤 10시 온라인 세미나 - 새로운 Windows 10 App을 개발하는 방법
10797정성태5/23/201521734VC++: 91. 자식 스레드에 자동 상속되는 TEB의 SubProcessTag 필드파일 다운로드1
10796정성태5/23/201532556오류 유형: 293. SQL Server Management Studio 실행 시 "Cannot find one or more components" 오류
10795정성태5/23/201530653오류 유형: 292. InstallUtil로 .NET 서비스 등록 시 오류 - Operation is not supported. (Exception from HRESULT: 0x80131515). [3]
10794정성태5/22/201525592개발 환경 구성: 267. (무료) 마이크로소프트 온라인 강좌 소개 - 네트워킹 기초 [1]
2925정성태5/14/201525132디버깅 기술: 73. PDB 기호 파일의 경로 구성 방식파일 다운로드1
2924정성태5/14/201528457VS.NET IDE: 100. 비주얼 스튜디오 원격 디버깅 시 'Unknown function' 콜스택이 나온다면?
2923정성태5/12/201587824기타: 52. 도서: 시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지 [17]
2922정성태5/12/201524674오류 유형: 291. ssindex.cmd 실행 시 '...[tfs_collection_url]...' not found in srcsrv.ini 오류 발생
2921정성태5/9/201531057개발 환경 구성: 266. 인텔에서 구현한 최대 절전 모드 기능 - Intel® Rapid Start Technology
2920정성태5/9/201522137오류 유형: 290. 디스크 관리자의 파티션 축소 시, There is not enough space available on the disk(s) to complete this operation.
2919정성태5/9/201522059오류 유형: 289. Error: this template attempted to load component assembly 'NuGet.VisualStudio.Interop, ...'
2918정성태5/9/201540553Windows: 111. 복구(Recovery) 파티션 삭제하는 방법 [3]
2917정성태5/9/201530979오류 유형: 288. .NET Framework 4.6이 설치된 경우 "Intel® Rapid Storage Technology (Intel® RST) RAID Driver"가 설치 안 되는 문제 [5]
2916정성태5/9/201532073오류 유형: 287. 레지스트리 권한 오류 - Cannot edit [Registry key name]: Error writing the value's new contents.
2915정성태5/9/201531195개발 환경 구성: 265. TrustedInstaller 권한으로 프로그램 실행시키는 방법 [11]
2914정성태5/9/201528507DDK: 7. 정식 인증서가 있는 경우 Device Driver 서명하는 방법 [2]
2913정성태4/30/201526268.NET Framework: 511. Build 2015 행사에서 소개된 (맥/리눅스/윈도우 용 무료) Visual Studio Code 개발 도구 [8]
2912정성태4/29/201521998오류 유형: 286. VirtualBox에 Windows 8/2012 설치 시 "Error Code: 0x000000C4" 오류 발생
2911정성태4/29/201520659오류 유형: 285. Visual Studio 2015를 제거한 경우 Microsoft.VisualStudio.Web.PageInspector.Loader 어셈블리를 못 찾는 문제 [2]
2910정성태4/29/201524563오류 유형: 284. System.TypeLoadException: Could not load type 'System.Reflection.AssemblySignatureKeyAttribute' from assembly [1]
2909정성태4/29/201520705오류 유형: 283. WCF 연결 오류 - Expected record type 'PreambleAck'
2908정성태4/29/201528980오류 유형: 282. 원격에서 SQL 서버는 연결되지만, SQL Express는 연결되지 않는 경우
2907정성태4/29/201519078.NET Framework: 510. 제네릭(Generic) 인자에 대한 메타데이터 등록 확인
2906정성태4/28/201521626오류 유형: 281. DebugView로 인한 System.Diagnostics.Trace.WriteLine 멈춤(Hang) 현상
2905정성태4/27/201521999오류 유형: 280. HttpResponse.Headers.Add에서 "System.PlatformNotSupportedException: This operation requires IIS integrated pipeline mode." 예외 발생
... 121  122  123  124  [125]  126  127  128  129  130  131  132  133  134  135  ...