Microsoft MVP성태의 닷넷 이야기
.NET Framework: 989. HttpContextAccessor를 통해 이해하는 AsyncLocal<T> [링크 복사], [링크+제목 복사],
조회: 20873
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 5개 있습니다.)
.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: 1075.  C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?
; https://www.sysnet.pe.kr/2/0/12701

.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분
정성태

... 106  107  108  109  110  111  112  113  114  115  116  [117]  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11035정성태8/29/201621667오류 유형: 354. .NET Reflector - PDB 생성 화면에서 "Clear Store"를 하면 "Index and length must refer to a location within the string" 예외 발생
11034정성태8/25/201625732개발 환경 구성: 296. .NET Core 프로젝트를 NuGet Gallery에 배포하는 방법 [2]
11033정성태8/24/201623610오류 유형: 353. coreclr 빌드 시 error C3249: illegal statement or sub-expression for 'constexpr' function
11032정성태8/23/201622830개발 환경 구성: 295. 최신의 Visual C++ 컴파일러 도구를 사용하는 방법 [1]
11031정성태8/23/201618938오류 유형: 352. Error encountered while pushing to the remote repository: Response status code does not indicate success: 403 (Forbidden).
11030정성태8/23/201622120VS.NET IDE: 111. Team Explorer - 추가한 Git Remote 저장소가 Branch에 보이지 않는 경우
11029정성태8/18/201629384.NET Framework: 602. Process.Start의 cmd.exe에서 stdin만 redirect 하는 방법 [1]파일 다운로드1
11028정성태8/15/201622384오류 유형: 351. Octave 설치 시 JRE 경로 문제
11027정성태8/15/201624085.NET Framework: 601. ElementHost 컨트롤의 메모리 누수 현상
11026정성태8/13/201625294Math: 19. 행렬 연산으로 본 해밍코드
11025정성태8/12/201624148개발 환경 구성: 294. .NET Core 프로젝트에서 "Copy to Output Directory" 처리 [1]
11024정성태8/12/201623335오류 유형: 350. "nProtect GameMon" 실행 중에는 Visual Studio 디버깅이 안됩니다! [1]
11023정성태8/10/201624913개발 환경 구성: 293. Azure 구독 후 PaaS 서비스 만들어 보기
11022정성태8/10/201625396개발 환경 구성: 292. Azure Cloud Service 배포시 사용자 정의 작업을 추가하는 방법
11021정성태8/10/201622254오류 유형: 349. System.Runtime.Remoting.RemotingException - Type '..., ..., Version=..., Culture=neutral, PublicKeyToken=null' is not registered for activation [2]
11020정성태8/10/201625291VC++: 98. 원본과 대상 버퍼가 같은 경우 memcpy, wmemcpy 주의점
11019정성태8/10/201642126기타: 60. 도서: 시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지 (2쇄 정오표)
11018정성태8/9/201626295.NET Framework: 600. 단일 메서드 내에서의 할당으로 알아보는 자바와 닷넷의 GC 차이점 [1]
11017정성태8/9/201627640웹: 33. HTTP 쿠키에 한글 값을 설정하는 방법
11016정성태8/7/201625636개발 환경 구성: 291. Windows Server Containers 소개
11015정성태8/7/201623942오류 유형: 348. Windows Server 2016 TP5에서 Windows Containers의 docker run 실행 시 encountered an error during Start failed in Win32
11014정성태8/6/201624688오류 유형: 347. Hyper-V Virtual Machine Management service Account does not have permission to open attachment
11013정성태8/6/201635535개발 환경 구성: 290. Windows 10에서 경험해 보는 Windows Containers와 docker [4]
11012정성태8/6/201625611오류 유형: 346. Windows 10에서 Windows Containers의 docker run 실행 시 encountered an error during CreateContainer failed in Win32 발생
11011정성태8/6/201627121기타: 59. outlook.live.com 메일 서비스의 아웃룩 POP3 설정하는 방법
11010정성태8/6/201624082기타: 58. Outlook에 설정한 SMTP/POP3(예:천리안 메일) 계정 암호를 잊어버린 경우
... 106  107  108  109  110  111  112  113  114  115  116  [117]  118  119  120  ...