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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/10/2024]

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)
13296정성태3/25/20233691Windows: 234. IsDialogMessage와 협업하는 WM_GETDLGCODE Win32 메시지 [1]파일 다운로드1
13295정성태3/24/20233954Windows: 233. Win32 - modeless 대화창을 modal처럼 동작하게 만드는 방법파일 다운로드1
13294정성태3/22/20234125.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234192오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/20234312Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/20234716.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/20234222.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/20233418Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
13288정성태3/17/20233516Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법파일 다운로드1
13287정성태3/16/20233686Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법파일 다운로드1
13286정성태3/15/20234147Windows: 228. Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
13285정성태3/14/20233738Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법파일 다운로드1
13284정성태3/13/20233938Windows: 226. Win32 C/C++ - Dialog에서 값을 반환하는 방법파일 다운로드1
13283정성태3/12/20233480오류 유형: 852. 파이썬 - TypeError: coercing to Unicode: need string or buffer, NoneType found
13282정성태3/12/20233818Linux: 58. WSL - nohup 옵션이 필요한 경우
13281정성태3/12/20233720Windows: 225. 윈도우 바탕화면의 아이콘들이 넓게 퍼지는 경우 [2]
13280정성태3/9/20234465개발 환경 구성: 670. WSL 2에서 호스팅 중인 TCP 서버를 외부에서 접근하는 방법
13279정성태3/9/20234010오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20233967개발 환경 구성: 669. WSL 2의 (init이 아닌) systemd 지원 [1]
13277정성태3/6/20234584개발 환경 구성: 668. 코드 사인용 인증서 신청 및 적용 방법(예: Digicert)
13276정성태3/5/20234313.NET Framework: 2102. C# 11 - ref struct/ref field를 위해 새롭게 도입된 scoped 예약어
13275정성태3/3/20234666.NET Framework: 2101. C# 11의 ref 필드 설명
13274정성태3/2/20234255.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
13273정성태2/28/20233955.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
13272정성태2/27/20234213오류 유형: 850. SSMS - mdf 파일을 Attach 시킬 때 Operating system error 5: "5(Access is denied.)" 에러
13271정성태2/25/20234147오류 유형: 849. Sql Server Configuration Manager가 시작 메뉴에 없는 경우
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...