Microsoft MVP성태의 닷넷 이야기
.NET Framework: 989. HttpContextAccessor를 통해 이해하는 AsyncLocal<T> [링크 복사], [링크+제목 복사],
조회: 20666
글쓴 사람
정성태 (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분
정성태

... 46  47  48  49  [50]  51  52  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12721정성태7/20/202121062오류 유형: 738. The trust relationship between this workstation and the primary domain failed. - 세 번째 이야기
12720정성태7/19/202116379Linux: 43. .NET Core/5+ 응용 프로그램의 Ubuntu (Debian) 패키지 준비
12719정성태7/19/202115367오류 유형: 737. SharePoint 설치 시 "0x800710D8 The object identifier does not represent a valid object." 오류 발생
12718정성태7/19/202114993개발 환경 구성: 581. Windows에서 WSL로 파일 복사 시 root 소유권으로 적용되는 문제파일 다운로드1
12717정성태7/18/202115571Windows: 195. robocopy에서 파일의 ADS(Alternate Data Stream) 정보 복사를 제외하는 방법
12716정성태7/17/202116020개발 환경 구성: 580. msbuild의 Exec Task에 robocopy를 사용하는 방법파일 다운로드1
12715정성태7/17/202123450오류 유형: 736. Windows - MySQL zip 파일 버전의 "mysqld --skip-grant-tables" 실행 시 비정상 종료 [1]
12714정성태7/16/202116613오류 유형: 735. VCRUNTIME140.dll, MSVCP140.dll, VCRUNTIME140.dll, VCRUNTIME140_1.dll이 없어 exe 실행이 안 되는 경우
12713정성태7/16/202118799.NET Framework: 1077. C# - 동기 방식이면서 비동기 규약을 따르게 만드는 Task.FromResult파일 다운로드1
12712정성태7/15/202117390개발 환경 구성: 579. Azure - 리눅스 호스팅의 Site Extension 제작 방법
12711정성태7/15/202116868개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
12710정성태7/15/202120203개발 환경 구성: 577. MQTT - emqx.io 서비스 소개
12709정성태7/14/202115728Linux: 42. 실행 중인 docker 컨테이너에 대한 구동 시점의 docker run 명령어를 확인하는 방법
12708정성태7/14/202120080Linux: 41. 리눅스 환경에서 디스크 용량 부족 시 원인 분석 방법
12707정성태7/14/202186650오류 유형: 734. MySQL - Authentication method 'caching_sha2_password' not supported by any of the available plugins.
12706정성태7/14/202118040.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법 [2]파일 다운로드1
12705정성태7/13/202118811VS.NET IDE: 168. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 - 두 번째 이야기
12704정성태7/12/202117367개발 환경 구성: 576. Azure VM의 서비스를 Azure Web App Service에서만 접근하도록 NSG 설정을 제한하는 방법
12703정성태7/11/202122368개발 환경 구성: 575. Azure VM에 (ICMP) ping을 허용하는 방법
12702정성태7/11/202118624오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/202118205.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/202118688.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/202117336VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/202121415오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/202116004개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/202117397VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
... 46  47  48  49  [50]  51  52  53  54  55  56  57  58  59  60  ...