Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

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); // 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); // QueueUserWorkItem, UnsafeQueueUserWorkItem 모두 null
}

UnsafeQueueUserWorkItem인 경우 일부러 LogicalCallContext를 전달하고 싶다면 대신 다른 방법을 사용해야 합니다.

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

 // ThreadPool.UnsafeQueueUserWorkItem(threadFunc, null);
    ExecutionContext.Run(threadContext, threadFunc, null);
 
    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의 미스테리가 좀 풀렸겠지요. ^^




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

[연관 글]


donaricano-btn



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

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 내에서 사용가능하지 않을지요?

감사합니다.
[손님]
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)
12839정성태9/15/2021217.NET Framework: 1118. C# 10 - (17) 제네릭 타입의 특성 적용파일 다운로드1
12838정성태9/13/2021213.NET Framework: 1117. C# - Task에 전달한 Action, Func 유형에 따라 달라지는 async/await 비동기 처리 [2]파일 다운로드1
12837정성태9/11/2021117VC++: 151. Golang - fmt.Errorf, errors.Is, errors.As 설명
12836정성태9/10/202183Linux: 45. 리눅스 - 실행 중인 다른 프로그램의 출력을 확인하는 방법
12835정성태9/7/2021158.NET Framework: 1116. C# 10 - (16) CallerArgumentExpression 특성 추가파일 다운로드1
12834정성태9/7/202175오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
12833정성태9/6/2021119VC++: 150. Golang - TCP client/server echo 예제 코드파일 다운로드1
12832정성태9/6/202180VC++: 149. Golang - 인터페이스 포인터가 의미 있을까요?
12831정성태9/6/2021109VC++: 148. Golang - 채널에 따른 다중 작업 처리파일 다운로드1
12830정성태9/6/202171오류 유형: 761. Internet Explorer에서 파일 다운로드 시 "Your current security settings do not allow this file to be downloaded." 오류
12829정성태9/5/2021201.NET Framework: 1115. C# 10 - (15) 구조체 타입에 기본 생성자 정의 가능파일 다운로드1
12828정성태9/4/2021192.NET Framework: 1114. C# 10 - (14) 단일 파일 내에 적용되는 namespace 선언파일 다운로드1
12827정성태9/4/2021112스크립트: 27. 파이썬 - 웹 페이지 데이터 수집을 위한 scrapy Crawler 사용법 요약
12826정성태9/3/2021168.NET Framework: 1113. C# 10 - (13) 문자열 보간 성능 개선파일 다운로드1
12825정성태9/3/202169개발 환경 구성: 603. GoLand - WSL 환경과 연동
12824정성태9/2/2021140오류 유형: 760. 파이썬 tensorflow - Dst tensor is not initialized. 오류 메시지
12823정성태9/2/2021164스크립트: 26. 파이썬 - PyCharm을 이용한 fork 디버그 방법
12822정성태9/1/202197오류 유형: 759. 파이썬 tensorflow - ValueError: Shapes (...) and (...) are incompatible
12821정성태9/1/2021150.NET Framework: 1112. C# - .NET 6부터 공개된 ISpanFormattable 사용법
12820정성태9/1/202187VC++: 147. Golang - try/catch에 대응하는 panic/recover파일 다운로드1
12819정성태8/31/2021182.NET Framework: 1111. C# - FormattableString 타입
12818정성태8/31/2021118Windows: 198. 윈도우 - 작업 관리자에서 (tensorflow 등으로 인한) GPU 연산 부하 보는 방법
12817정성태8/31/202176스크립트: 25. 파이썬 - 윈도우 환경에서 directml을 이용한 tensorflow의 AMD GPU 사용 방법
12816정성태8/30/2021318스크립트: 24. 파이썬 - tensorflow 2.6 NVidia GPU 사용 방법 [2]
12815정성태8/30/2021199개발 환경 구성: 602. WSL 2 - docker-desktop-data, docker-desktop (%LOCALAPPDATA%\Docker\wsl\data\ext4.vhdx) 파일을 다른 디렉터리로 옮기는 방법
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...