Microsoft MVP성태의 닷넷 이야기
.NET Framework: 989. HttpContextAccessor를 통해 이해하는 AsyncLocal<T> [링크 복사], [링크+제목 복사]
조회: 1465
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

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://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1

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

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




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 7/9/2021]

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

비밀번호

댓글 쓴 사람
 



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

1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12650정성태5/17/2021470기타: 82. OpenTabletDriver의 버튼에 더블 클릭을 매핑 및 게임에서의 지원 방법
12649정성태5/16/2021612.NET Framework: 1059. 세대 별 GC(Garbage Collection) 방식에서 Card table의 사용 의미
12648정성태5/16/2021519[예약]파일 다운로드1
12647정성태5/15/2021606.NET Framework: 1058. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용파일 다운로드1
12646정성태5/15/2021639[예약]파일 다운로드1
12645정성태5/14/2021626[예약]파일 다운로드1
12644정성태5/14/2021548오류 유형: 719. 윈도우 - 제어판의 "프로그램 및 기능" / "Windows 기능 켜기/끄기" 오류 0x800736B3
12643정성태5/14/2021469오류 유형: 718. 서버 유형의 COM+ 사용 시 0x80080005(Server execution failed) 오류 발생
12642정성태5/14/2021568오류 유형: 717. The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine.
12641정성태5/13/2021582디버깅 기술: 179. 윈도우용 .NET Core 3 이상에서 Windbg의 sos 사용법
12640정성태5/13/2021846오류 유형: 716. RDP 연결 - Because of a protocol error (code: 0x112f), the remote session will be disconnected. [1]
12639정성태5/12/2021566오류 유형: 715. Arduino: Open Serial Monitor - The module '...\detection.node' was compiled against a different Node.js version using NODE_MODULE_VERSION
12638정성태5/12/2021608사물인터넷: 63. NodeMCU v1 ESP8266 - 펌웨어 내 파일 시스템(SPIFFS, LittleFS) 및 EEPROM 활용
12637정성태5/10/2021607사물인터넷: 62. NodeMCU v1 ESP8266 보드의 A0 핀에 다중 아날로그 센서 연결 [1]
12636정성태5/10/2021670사물인터넷: 61. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - FSR-402 아날로그 압력 센서 연동파일 다운로드1
12635정성태5/9/2021553기타: 81. OpenTabletDriver를 (관리자 권한으로 실행하지 않고도) 관리자 권한의 프로그램에서 동작하게 만드는 방법
12634정성태5/9/2021603개발 환경 구성: 572. .NET에서의 신뢰도 등급 조정 - 외부 Manifest 파일을 두는 방법파일 다운로드1
12633정성태5/7/2021622개발 환경 구성: 571. UAC - 관리자 권한 없이 UIPI 제약을 없애는 방법
12632정성태5/7/2021704기타: 80. (WACOM도 지원하는) Tablet 공통 디바이스 드라이버 - OpenTabletDriver
12631정성태5/5/2021630사물인터넷: 60. ThingSpeak 사물인터넷 플랫폼에 ESP8266 NodeMCU v1 + 조도 센서 장비 연동파일 다운로드1
12630정성태5/5/2021682사물인터넷: 59. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - CdS Cell(GL3526) 조도 센서 연동파일 다운로드1
12629정성태5/5/2021847.NET Framework: 1057. C# - CoAP 서버 및 클라이언트 제작 (UDP 소켓 통신)파일 다운로드1
12628정성태5/4/2021590Linux: 39. Eclipse 원격 디버깅 - Cannot run program "gdb": Launching failed
12627정성태5/4/2021649Linux: 38. 라즈베리 파이 제로 용 프로그램 개발을 위한 Eclipse C/C++ 윈도우 환경 설정
12626정성태5/3/2021656.NET Framework: 1056. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상 (2)파일 다운로드1
12625정성태5/3/2021689오류 유형: 714. error CS5001: Program does not contain a static 'Main' method suitable for an entry point
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...