Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 5개 있습니다.)
(시리즈 글이 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




HttpContext.Current를 통해 이해하는 CallContext와 ExecutionContext

ASP.NET의 경우 HttpContext.Current를 사용해서 ASP.NET pipeline에서의 동일한 문맥 정보를 제공합니다. 일례로 요청을 처리하는 스레드에서 실행하는 모든 코드에서는 무조건 HttpContext.Current 속성을 이용하면 현재 문맥을 가져올 수 있습니다.

그런데, 도대체 HttpContext에 어떤 마법이 숨겨져 있는 걸까요? ^^ 이번엔 이에 대해서 간단하게(?) 파악해 보겠습니다.




우선, HttpContext.Current 속성의 소스 코드를 살펴볼까요?

// System.Web.dll
public sealed class HttpContext : IServiceProvider
{
    // ...[생략]...
    public static HttpContext Current
    {
        get
        {
            return (ContextBase.Current as HttpContext);
        }
        set
        {
            ContextBase.Current = value;
        }
    }
}

아하... ContextBase.Current를 형변환 한 것에 불과하군요. 이어서 ContextBase를 가보겠습니다.

// System.Web.dll
internal class ContextBase
{
    // ...[생략]...
    internal static object Current
    {
        get
        {
            return CallContext.HostContext;
        }
        set
        {
            CallContext.HostContext = value;
        }
    }
}

ContextBase.Current의 내부 구현은 결국 CallContext.HostContext임을 알 수 있습니다. 그리고 CallContext의 정적 공용 변수인 HostContext는 Thread마다 고유하게 할당된 ExecutionContext가 포함하고 있는 2개의 CallContext 인스턴스에 값을 읽고 쓰는 것에 불과합니다.

여기에서 CallContext.HostContext의 set을 살펴보면 다음과 같습니다.

public static void set_HostContext(object value)

    ExecutionContext mutableExecutionContext = Thread.CurrentThread.GetMutableExecutionContext();
    if (value is ILogicalThreadAffinative)
    {
        mutableExecutionContext.IllogicalCallContext.HostContext = null;
        mutableExecutionContext.LogicalCallContext.HostContext = value;
    }
    else
    {
        mutableExecutionContext.IllogicalCallContext.HostContext = value;
        mutableExecutionContext.LogicalCallContext.HostContext = null;
    }
}

여기서 재미있는 점이 바로, ExecutionContext가 관리하는 2개의 CallContext입니다. 왜 2개일까요? 이는 객체의 Serializable과 관련이 있습니다.

Identifying the differences between CallContext Data Slots 
; http://dotnetmustard.blogspot.kr/2008/08/identifying-differences-between.html

Thread 객체가 소유한 (Hashtable로 데이터를 관리하는) CallContext의 데이터를 스레드 간에 넘길 것이냐 말 것이냐에 따라 어느 CallContext에 데이터가 get/set 되는지가 결정되기 때문입니다. (보다 정확하게는 Thread 간이라고 볼 수는 없습니다. 왜냐하면 CallContext가 정의된 네임스페이스는 System.Runtime.Remoting.Messaging이고 이는 원격 메서드 호출에 관여하기 때문입니다. 물론 원격 메서드 호출은 확실히 스레드가 달라지긴 하지만.)

Thread가 가지고 있는 2개의 CallContext는 각기 2개의 클래스로 나뉘는데요. 하나는 LogicalCallContext이고, 다른 하나는 IllogicalCallContext입니다.

internal class LogicalCallContext : ISerializable, ICloneable
{
    // ...[생략]...
    private Hashtable m_Datastore;
    private object m_HostContext;
}

internal class IllogicalCallContext : ICloneable
{
    // ...[생략]...
    private Hashtable m_Datastore;
    private object m_HostContext;
}

전자가 직렬화 가능한 객체를 담고, 후자는 직렬화가 지원되지 않는 객체를 담습니다. 각각의 CallContext는 Thread 인스턴스에서 직접 접근할 수는 없고, CallContext의 정적 메서드를 사용해야만 가능합니다.

  • public static object GetData(string name);
  • public static void SetData(string name, object data);
  • public static object LogicalGetData(string name);
  • public static void LogicalSetData(string name, object data);

우선, LogicalSetData는 Thread.ExecutionContext.LogicalCallContext에 값을 set 합니다. 반면, SetData는 인자로 주어진 object data 인스턴스가 ILogicalThreadAffinative 인터페이스를 구현하고 있다면 LogicalCallContext로, 그렇지 않으면 IllogicalCallContext로 보관됩니다.




말로만 설명하면 너무 식상하니 ^^ 코딩을 해보겠습니다. 우선, Thread의 LogicalCallContext에 값을 보관하려면 다음과 같이 해주면 됩니다.

public class LTAObject : ILogicalThreadAffinative
{
    public string Value { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // ILogicalThreadAffinative 인터페이스를 구현하지 않은 string 객체라도
        // 명시적으로 LogicalCallContext에 값을 보관
        CallContext.LogicalSetData("key1", "test");
        Debug.Assert(CallContext.LogicalGetData("key1") as string == "test");

        // ILogicalThreadAffinative 인터페이스를 상속받은 객체를 생성하고,
        LTAObject lta = new LTAObject();
        lta.Value = "test";

        // LogicalCallContext에 값을 보관
        CallContext.SetData("key1", lta);
        Debug.Assert((CallContext.LogicalGetData("key1") as LTAObject).Value == "test");
            
    }
}

반면, Thread의 IllogicalCallContext에 값을 보관하는 코드는 다음과 같습니다.

public class NoLTAObject
{
    public string Value { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // string 객체는 ILogicalThreadAffinative를 상속받지 않았으므로 IllogicalCallContext에 보관됨.
        CallContext.SetData("key1", "test");
        Debug.Assert(CallContext.GetData("key1") as string == "test");
        Debug.Assert(CallContext.LogicalGetData("key1") == null); // LogicalCallContext에 없음.

        // NoLTAObject 객체도 ILogicalThreadAffinative를 상속받지 않았으므로 IllogicalCallContext에 보관됨.
        NoLTAObject noLta = new NoLTAObject();
        noLta.Value = "test";

        CallContext.SetData("key1", noLta);
        Debug.Assert((CallContext.GetData("key1") as NoLTAObject).Value == "test");
        Debug.Assert(CallContext.LogicalGetData("key1") == null); // LogicalCallContext에 없음.
    }
}




단일 스레드로 특정 작업이 처리되는 상황에서는 LogicalCallContext와 IllogicalCallContext 중에서 어디에 값을 보관하느냐는 문제될 것이 없습니다. 하지만, 그 작업과 관련되어 여러 가지 스레드들이 관여하게 된다면 그 스레드들이 공통으로 공유해야 할 문맥 정보가 필요합니다.

위에서 구현되었던 LTAObject와 NoLTAObject가 스레드를 넘어가는 경우의 차이점을 아래의 코드로 직접 확인해 볼 수 있습니다.

public class LTAObject : ILogicalThreadAffinative
{
    public string Value { get; set; }
}

public class NoLTAObject
{
    public string Value { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // LogicalCallContext에 값을 보관
        CallContext.LogicalSetData("key1", "test");

        // IllogicalCallContext에 값을 보관
        CallContext.SetData("key2", "test");

        // ILogicalThreadAffinative 인터페이스를 상속받은 객체를 생성하고,
        LTAObject lta = new LTAObject();
        lta.Value = "test";

        // LogicalCallContext에 값을 보관
        CallContext.SetData("key3", lta);

        // NoLTAObject 객체도 ILogicalThreadAffinative를 상속받지 않았으므로 IllogicalCallContext에 보관됨.
        NoLTAObject noLta = new NoLTAObject();
        noLta.Value = "test";

        CallContext.SetData("key4", noLta);

        Thread thread = new Thread(threadFunc);
        thread.Start();
        thread.Join();

        return;
    }

    static void threadFunc()
    {
        // LogicalCallContext에 보관되었던 데이터들은 스레드 경계를 넘어옴
        Debug.Assert(CallContext.LogicalGetData("key1") as string == "test");
        Debug.Assert((CallContext.LogicalGetData("key3") as LTAObject).Value == "test");

        // IllogicalCallContext에 보관되었던 데이터들은 스레드 경계를 못 넘음.
        Debug.Assert(CallContext.GetData("key2") == null);
        Debug.Assert(CallContext.GetData("key4") == null);
    }
}

보시는 것처럼, LogicalCallContext에 보관되었던 항목들은 스레드 간에 자연스럽게 데이터 이관이 되었지만, IllogicalCallContext에 보관되었던 항목들은 안되었습니다.

그런데, 혹시 LogicalCallContext의 데이터도 스레드 간에 넘어가지 못하도록 막고 싶지는 않을까요? 데이터를 스레드 간에 넘긴다는 것은 그런 작업이 이뤄졌기 때문에 가능한 것이고 이것이 복잡한 경우 자칫 성능에 영향을 미칠 수 있습니다. 굳이 CallContext의 데이터들이 필요치 않다면 이를 넘기는데 괜한 오버헤드를 가져갈 이유가 없습니다.

이를 위해 ExecutionContext의 SuppressFlow / RestoreFlow 정적 메서드를 이용하면 됩니다.

ExecutionContext.SuppressFlow();

Thread thread = new Thread(threadFunc);
thread.Start();
thread.Join();

ExecutionContext.RestoreFlow();

static void threadFunc()
{
    // 모든 CallContext의 데이터들이 스레드 경계를 못 넘음
    Debug.Assert(CallContext.LogicalGetData("key1") == null);
    Debug.Assert(CallContext.GetData("key2") == null);

    Debug.Assert(CallContext.LogicalGetData("key3") == null);
    Debug.Assert(CallContext.GetData("key4") == null);
}

LogicalCallContext와 IllogicalCallContext 인스턴스는 ExecutionContext에 포함되어 있고, 다시 ExecutionContext는 Thread에 포함되어 있습니다. 따라서 관리는 ExecutionContext 측에서 이뤄지는데, 기본 동작이 스레드 간에 LogicalCallContext를 자동으로 넘겨주는 역할을 하고 이를 막으려면 개발자가 수작업으로 SuppressFlow / RestoreFlow를 호출해 주어야 합니다.




여기까지 이해하셨으면, ThreadPool 타입의 QueueUserWorkItem과 UnsafeQueueUserWorkItem 메서드에서도 CallContxt에 대한 차이가 있다는 것을 자연스럽게 파악할 수 있습니다.

즉, QueueUserWorkItem은 LogicalCallContext 데이터를 스레드 풀에서 가져올 스레드에 설정해 주는 반면, UnsafeQueueUserWorkItem은 LogicalCallContext 데이터 이관을 명시적으로 하지 않는다는 차이가 있습니다. 위의 예제 코드와 비교해서 설명해 보면 UnsafeQueueUserWorkItem은 SuppressFlow / RestoreFlow를 호출한 스레드처럼 동작하는 것입니다.

static void Main(string[] args)
{
    CallContext.LogicalSetData("key1", "test");

    ThreadPool.QueueUserWorkItem(threadFunc, null);
    ThreadPool.UnsafeQueueUserWorkItem(threadFunc, null);

    Console.ReadLine();
}

static void threadFunc(object objContext)
{
    object objValue = CallContext.GetData("key1");
    Console.WriteLine(objValue ?? "(null)"); // QueueUserWorkItem인 경우 "test" 출력
                                             // UnsafeQueueUserWorkItem인 경우 "(null)"
}

실제로 테스트해 보면 QueueUserWorkItem인 경우일지라도 그 전에 SuppressFlow를 호출해 주면 마찬가지로 LogicalCallContext 데이터가 이관되지 않는 것을 알 수 있습니다.

static void Main(string[] args)
{
    CallContext.LogicalSetData("key1", "test");

    ExecutionContext.SuppressFlow();
    ThreadPool.QueueUserWorkItem(threadFunc, null);
    ExecutionContext.RestoreFlow();

    ThreadPool.UnsafeQueueUserWorkItem(threadFunc, null);

    Console.ReadLine();
}

static void threadFunc(object objContext)
{
    object objValue = CallContext.GetData("key1");
    Console.WriteLine(objValue ?? "(null)"); // QueueUserWorkItem, UnsafeQueueUserWorkItem 모두 "(null)"
}

SuppressFlow 사용 중에 일부러 LogicalCallContext를 전달하고 싶다면 대신 다른 방법을 사용해야 합니다.

static void Main(string[] args)
{
    CallContext.LogicalSetData("key1", "test");
    ExecutionContext threadContext = ExecutionContext.Capture(); // 미리 capture 해두고,

    ExecutionContext.SuppressFlow(); // suppress를 해도,

 // ThreadPool.UnsafeQueueUserWorkItem(threadFunc, null);
    ExecutionContext.Run(threadContext, threadFunc, null); // capture했던 내용으로 직접 실행하는 것도 가능

    ExecutionContext.RestoreFlow();
 
    Console.ReadLine();
}

static void threadFunc(object objContext)
{
    object objValue = CallContext.GetData("key1");
    Console.WriteLine(objValue); // "test" 값을 출력
}




비동기 호출의 경우, LogicalCallContext의 데이터 이관이 어떻게 되는지 살펴보는 것도 흥미로운데요. 아래의 글을 보면,

Nested multithread operations tracing
; http://stackoverflow.com/questions/2651327/nested-multithread-operations-tracing

다음과 같은 코드 테스트가 나옵니다.

static void Main(string[] args)
{
    string key = "aaa";
    EventWaitHandle asyncStarted = new AutoResetEvent(false);
    IAsyncResult r = null;

    CallContext.LogicalSetData(key, "Root - op 0");
    Console.WriteLine("Initial: {0}", CallContext.LogicalGetData(key));

    Action a = () =>
    {
        CallContext.LogicalSetData(key, "Async - op 0");
        asyncStarted.Set();
    };
    r = a.BeginInvoke(null, null);

    asyncStarted.WaitOne();
    Console.WriteLine("AsyncOp started: {0}", CallContext.LogicalGetData(key));

    CallContext.LogicalSetData(key, "Root - op 1");
    Console.WriteLine("Current changed: {0}", CallContext.LogicalGetData(key));

    a.EndInvoke(r);
    Console.WriteLine("Async ended: {0}", CallContext.LogicalGetData(key));
}

// 출력 결과
Initial: Root - op 0
AsyncOp started: Root - op 0
Current changed: Root - op 1
Async ended: Async - op 0

처음 Main 메서드의 시작에서 key == "aaa"로 CallContext.LogicalSetData에 값을 넣었는데요. 비동기 호출되는 delegate 메서드 내에서 "aaa" 키에 해당하는 LogicalCallContext의 값을 변경하지만, 외부에서는 그 시점에 변화를 알 수 없습니다. 실제로 비동기 메서드 내에서의 LogicalCallContext 저장소가 그것의 부모 스레드의 LogicalCallContext에 병합되는 시점은 EndInvoke이라는 것이 특이합니다.




자, 이제 다시 최초의 HttpContext.Current로 돌아가 보겠습니다. 여기까지 살펴봤으면 이제 웹상에서 자주 나타나는 한 가지 질문에 답할 수 있게 됩니다.

새로 만든 스레드에서 HttpContext.Current를 접근하면 null이 나옵니다. 왜 그런 걸까요?

간단합니다. 소스 코드를 살펴보면 HttpContext 개체는 ILogicalThreadAffinative 인터페이스를 구현하지 않았습니다. 또한 CallContext.LogicalSetData를 통해서 값을 설정하지도 않았기 때문에 HttpContext 인스턴스 자체는 Thread의 IllogicalCallContext 영역에 보관되어 있습니다.

따라서, ASP.NET Page 내에서 다음과 같이 별도의 스레드로 넘어가버리면 해당 스레드에서는 HttpContext.Current에 아무런 값이 설정되어 있지 않은 상태입니다.

using System;
using System.Web;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace WebApplication1
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(threadFunc, null);
        }

        void threadFunc(object objContext)
        {
            object httpContext = HttpContext.Current; // == null
            object hostContxtObj = CallContext.HostContext; // null
        }
    }
}

애당초 이것이 가능하려면 마이크로소프트가 HttpContext를 직렬화 가능한 클래스로 만들었어야 합니다. 단지, 그렇게 만들지 않았던 것에는 몇 가지 타당한 이유가 있어 보입니다. 가령 HttpContext.Current에서는 Server, Request, Response 등의 객체를 노출시켜 주는데 그런 객체들을 모두 마샬링 가능한 타입으로 만드는 것은 그다지 효율적이지 않았을 수 있습니다. 그 외에도, 새롭게 생성된 스레드가 현재 요청이 처리된 후에 - 즉, HttpContext가 소멸된 이후에 - HttpContext.Current를 접근한다면 이미 모든 정보가 유효하지 않은 상태에서 그것을 접근하게 해주는 것도 자칫 복잡한 문제를 야기시킬 수 있습니다. 따라서 제 의견에도 HttpContext가 객체가 스레드 간에 넘나들지 않게 한 것은 옳은 결정으로 보입니다.

어쨌든 다른 스레드에서는 HttpContext.Current를 넘겨줄 수 없으므로 필요한 정보가 있다면 그것만을 별도로 취합해서 HttpContext가 아닌 CallContext를 직접 사용해서 넘겨주는 방법을 택해야 합니다.

using System;
using System.Web;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace WebApplication1
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            CallContext.LogicalSetData("RequestURL", HttpContext.Current.Request.Url.ToString());

            ThreadPool.QueueUserWorkItem(threadFunc, null);
        }

        void threadFunc(object objContext)
        {
            object obj1 = CallContext.LogicalGetData("RequestURL");
        }
    }
}

이 정도면 HttpContext.Current의 미스테리가 좀 풀렸겠지요. ^^




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/7/2025]

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

비밀번호

댓글 작성자
 



2014-01-27 12시03분
[ryujh] 안녕하세요.

CallContext, ExecutionContext 를 이해하기 전에 HttpContext 를 이해해야 해서 결론을 보고 생각해본 것이,

페이지 호출 당 페이지클래스 인스턴스가 생성되고 그 인스턴스 코드내에 주스레드에서 HttpContext.Current 로 접근 가능하다는 것이 맞을까요?

그리고 ThreadPool.QueueUserWorkItem(threadFunc, null); 대신
ThreadPool.QueueUserWorkItem(threadFunc, HttpContext.Current); 를 넘기면 threadFunc 내에서 사용가능하지 않을지요?

감사합니다.
[guest]
2014-01-27 01시37분
1. 넵. HttpContext.Current로 접근할 수 있습니다.
2. HttpContext.Current를 넘기면 물론 가능합니다. 사실 그게 가장 쉽겠지요. ^^ 단지, 글의 주제상 CallContext를 언급했고 HttpContext가 CallContext에 보관된다는 고유 사실로부터 얻어질 수 있는 효과를 비교하면 아주 동일하진 않습니다. 가령, CallContext의 경우 AppDomain 경계를 넘을 수도 있습니다.
정성태
2020-10-13 09시48분
How to migrate CallContext to .NETStandard and .NETCore
; https://www.cazzulino.com/callcontext-netstandard-netcore.html
정성태
2020-12-30 07시32분
정성태

1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13641정성태6/11/20248642Linux: 71. Ubuntu 20.04를 22.04로 업데이트
13640정성태6/10/20248805Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
13639정성태6/8/20248410오류 유형: 906. C# MAUI - Android Emulator에서 "Waiting For Debugger"로 무한 대기
13638정성태6/8/20248499오류 유형: 905. C# MAUI - 추가한 layout XML 파일이 Resource.Layout 멤버로 나오지 않는 문제
13637정성태6/6/20248425Phone: 20. C# MAUI - 유튜브 동영상을 MediaElement로 재생하는 방법
13636정성태5/30/20248062닷넷: 2264. C# - 형식 인자로 인터페이스를 갖는 제네릭 타입으로의 형변환파일 다운로드1
13635정성태5/29/20248912Phone: 19. C# MAUI - 안드로이드 "Share" 대상으로 등록하는 방법
13634정성태5/24/20249399Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [1]
13633정성태5/22/20248915스크립트: 64. 파이썬 - ASGI를 만족하는 최소한의 구현 코드
13632정성태5/20/20248542Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
13631정성태5/19/20249310Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법 [1]
13630정성태5/19/20248846닷넷: 2263. C# - Thread가 Task보다 더 빠르다는 어떤 예제(?)
13629정성태5/18/20249136개발 환경 구성: 710. Android - adb.exe를 이용한 파일 전송
13628정성태5/17/20248526개발 환경 구성: 709. Windows - WHPX(Windows Hypervisor Platform)를 이용한 Android Emulator 가속
13627정성태5/17/20248586오류 유형: 904. 파이썬 - UnicodeEncodeError: 'ascii' codec can't encode character '...' in position ...: ordinal not in range(128)
13626정성태5/15/20248853Phone: 15. C# MAUI - MediaElement Source 경로 지정 방법파일 다운로드1
13625정성태5/14/20248904닷넷: 2262. C# - Exception Filter 조건(when)을 갖는 catch 절의 IL 구조
13624정성태5/12/20248696Phone: 14. C# - MAUI에서 MediaElement 사용파일 다운로드1
13623정성태5/11/20248402닷넷: 2261. C# - 구글 OAuth의 JWT (JSON Web Tokens) 해석파일 다운로드1
13622정성태5/10/20249189닷넷: 2260. C# - Google 로그인 연동 (ASP.NET 예제)파일 다운로드1
13621정성태5/10/20248635오류 유형: 903. IISExpress - Failed to register URL "..." for site "..." application "/". Error description: Cannot create a file when that file already exists. (0x800700b7)
13620정성태5/9/20248526VS.NET IDE: 190. Visual Studio가 node.exe를 경유해 Edge.exe를 띄우는 경우
13619정성태5/7/20248844닷넷: 2259. C# - decimal 저장소의 비트 구조파일 다운로드1
13618정성태5/6/20248637닷넷: 2258. C# - double (배정도 실수) 저장소의 비트 구조파일 다운로드1
13617정성태5/5/20249454닷넷: 2257. C# - float (단정도 실수) 저장소의 비트 구조파일 다운로드1
13616정성태5/3/20248603닷넷: 2256. ASP.NET Core 웹 사이트의 HTTP/HTTPS + Dual mode Socket (IPv4/IPv6) 지원 방법파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...