Microsoft MVP성태의 닷넷 이야기
.NET Framework: 353. x86 - AspCompat과 STA COM 개체가 성능에 미치는 영향 [링크 복사], [링크+제목 복사],
조회: 21951
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)

x86 - AspCompat과 STA COM 개체가 성능에 미치는 영향


테스트 환경은 다음과 같습니다.

  • .NET 4.5 x86
  • IIS 7.5




Native 시대의 COM 개체를 잘 모르더라도 ASP.NET에서 기존 COM 개체를 사용하려면 Apartment 유형에 따라 AspCompat 옵션 사용 유무를 판단해야 한다는 것은 알아야 합니다. ^^

ASP.NET 스레드 풀 내에 존재하는 모든 스레드는 기본적으로 (MTA는 아니고) MTA 성격이기 때문에 STA COM 개체를 생성하려면 특별한 마크를 해주어야 합니다. 그것이 AspCompat인데요. 이 때문에 STA COM을 사용하는 전형적인 페이지는 다음과 같이 됩니다.

// ==== default.aspx ====
<%@ Page AspCompat="true" Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>

	... [HTML 생략]...

// ==== default.aspx.cs ====
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebApplication1
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            WebTestHelperLib.ComAptTestClass catc = new WebTestHelperLib.ComAptTestClass();

            catc.DoMethod();
        }
    }
}

재미있는 것은, 위의 페이지를 실행하는 스레드의 흐름입니다. 이를 확인하려면 웹 페이지 코드를 다음과 같이 변경시키고,

public partial class _Default : System.Web.UI.Page
{
    int _pageTid;

    public _Default()
    {
        _pageTid = AppDomain.GetCurrentThreadId();
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        WebTestHelperLib.ComAptTestClass catc = new WebTestHelperLib.ComAptTestClass();
        int tid;
        int aptKind = catc.DoMethod3(5, out tid);

        StringBuilder sb = new StringBuilder();

        sb.AppendFormat("ctor Thread: {0}<br />", _pageTid);
        sb.AppendFormat("Page_Load Thread: {0}<br />", AppDomain.GetCurrentThreadId());
        sb.AppendFormat("Com Thread: {0}<br />", tid);
        sb.AppendFormat("Com Apt: {0}<br />", aptKind);

        Label1.Text = sb.ToString();
    }
}

COM 개체에서 제공되는 DoMethod3 메서드를 이렇게 구현해 주어야 합니다.

// sleepSecond: 메서드 실행 시 일부러 지연시킬 시간을 지정
// tid: DoMethod3을 실행하는 스레드의 Native ID가 반환됨
// aptKind: DoMethod3을 실행하는 스레드의 Apartment 유형을 반환
STDMETHODIMP CComAptTest::DoMethod3(LONG sleepSecond,LONG *tid, LONG *aptKind)
{
    ::Sleep(sleepSecond * 1000);

    *tid = ::GetCurrentThreadId();

    do 
    {
        // 아래의 코드는 https://www.sysnet.pe.kr/2/0/1351 글을 참조.
        BYTE *pTeb = (BYTE *)NtCurrentTeb();
        int *pOle = (int *)(pTeb + 0xf80);

        if (*pOle == 0)
        {
            printf("No apartment\n");
            break;
        }

        int *pNativeApt = (int *)(*pOle + 0x50);
        *aptKind = *(int *)(*pNativeApt + 0x0c);
    } while (false);

    return S_OK;
}

/*
*aptKind에 담기는 값은 다음 중의 하나입니다.

enum tagAPTKIND {
    APTKIND_NEUTRALTHREADED = 1,
    APTKIND_MULTITHREADED = 2,
    APTKIND_APARTMENTTHREADED = 4,
    APTKIND_APPLICATION_STA = 8
};
*/

자... 이렇게 하고 실행하면 다음과 같은 결과를 얻을 수 있습니다.

ctor Thread: 10992
Page_Load Thread: 5628
Com Thread: 5628
Com Apt: 4 (== APTKIND_APARTMENTTHREADED)

즉, 스레드의 실행 흐름이 다음과 같이 변경됩니다.

  1. 클라이언트의 요청을 ASP.NET Thread Pool 내의 10992 스레드가 받고,
  2. 10992 스레드가 Page 객체를 생성한 후,
  3. 10992 스레드가 AspCompat 값이 True임을 확인하고 STA COM 개체가 생성될 것임을 짐작,
  4. 따라서 별도로 생성해 둔 5628 STA 스레드로 Request 문맥을 전달,
  5. 5628은 이후 Page 개체의 나머지 실행 처리를 담당.

여기서 문제는 5628 STA 스레드가 모든 COM 개체의 사용에 대응된다는 것입니다. 따라서, 해당 COM 개체를 사용하는 모든 웹 페이지의 실행은 5628 스레드로 직렬화 되어 성능 저하가 심각해 집니다. 예를 들어, 위의 test.aspx.cs에서 COM 개체의 DoMethod3 메서드에 5초의 지연시간을 갖도록 지정해 보겠습니다. 2명의 사용자가 동시에 test.aspx 웹 페이지를 방문하면 다음과 같은 현상이 발생합니다.

  1. A와 B사용자가 동시에 test.aspx 웹 페이지 요청
  2. A 요청을 10992 스레드가 받아서 5628 STA 스레드에 전달
  3. B 요청은 10330 스레드가 받아서 5628 STA 스레드에 전달
  4. 5628 스레드는 먼저 전달된 A 요청을 실행 - 5초 걸림
  5. 5초 후 5628 스레드는 두 번째로 전달된 B 요청을 실행 - 5초 걸림

결국, 이런 직렬화 처리로 인해 B 요청은 A 요청과 거의 비슷한 시기에 전달되었음에도 불구하고 10초 후에나 응답을 받게 됩니다.

이 문제를 해결하려면, STA COM 개체를 MTA COM 개체로 만들어야 합니다.




그런데, 만약 AspCompat=True 값을 설정하지 않았다면 어떻게 될까요? 역시 같은 방법으로 2개의 요청을 동시에 보내보면 다음과 같은 결과를 얻을 수 있습니다.

=== A 요청 ====
ctor Thread: 34968
Page_Load Thread: 34968
Com Thread: 5628
Com Apt: 4

=== B 요청 ====
ctor Thread: 13432
Page_Load Thread: 13432
Com Thread: 5628
Com Apt: 4

Page_Load까지도 ASP.NET 스레드 풀 내의 스레드가 처리를 담당하다가 정작 STA COM 개체의 메서드 호출에는 별도로 생성되어 있는 STA 스레드에 맡겨지고 있습니다. 이 결과와 AspCompat=True였을 때의 상황과는 어떤 차이가 있을까요?

이에 대한 해답은 AspCompat=False이고, STA COM 개체를 썼을 때 2개의 동시 요청을 어떤 식으로 처리하는지 파악해 보면 알 수 있습니다.

  1. A와 B사용자가 동시에 test.aspx 웹 페이지 요청
  2. A 요청을 34968 스레드가 받아서 5628 STA 스레드에 전달하고 대기
  3. B 요청은 13432 스레드가 받아서 5628 STA 스레드에 전달하고 대기
  4. 5628 스레드는 먼저 전달한 A 요청을 실행(5초 걸림)하고 34968 스레드로 실행 반환
  5. 5628 스레드는 두 번째로 전달한 B 요청을 실행(5초 걸림)하고 13432 스레드로 실행 반환

차이를 아시겠어요? AspCompat=True일 때는 ASP.NET의 요청을 처리하는 스레드 풀의 스레드가 자유로웠지만, False일 때는 그 스레드가 STA 스레드의 실행이 끝날 때까지 대기한다는 문제가 발생합니다.

이 때문에, 스레드 풀의 스레드 수가 500개이면 위의 페이지가 500개의 요청을 받는 경우 모든 스레드가 바닥난다는 의미가 됩니다. 더 이상 다음의 요청을 받아서 처리할 수 없다는 것이지요.




AspCompat=True인 경우, STA COM 개체를 Page_Load가 아닌 생성자나 멤버 변수 정의시점에 만들면 상황이 더 재미있어집니다.

public partial class _Default : System.Web.UI.Page
{
    WebTestHelperLib.ComAptTestClass catc; // 여기서 new를 하거나!

    public _Default()
    {
        catc = new WebTestHelperLib.ComAptTestClass(); // 여기서 new를 한 경우!
    }

    protected void Page_Load(object sender, EventArgs e)
    {
    }
}

AspCompat 속성은 _Default 페이지 개체를 생성하는 순간에는 그 값을 알 수 없습니다. 따라서 ASP.NET이 AspCompat 속성을 보고 어떠한 조치를 취하기에는 너무 빠르다는 것인데요.

이 때문에 이런 요청을 A, B 두 사용자가 동시에 보낸다면 결과가 다음과 같이 나옵니다.

ctor Thread: 24344
Page_Load Thread: 23712
Com Thread: 5628
Com Apt: 4

ctor Thread: 348
Page_Load Thread: 23712
Com Thread: 5628
Com Apt: 4

차이가 눈에 보이시나요? ctor를 실행한 스레드는 ASP.NET의 스레드 풀 중의 하나입니다. 그리곤 뒤늦게 AspCompat=True 속성을 인식하고는 23712 스레드로 Page_Load를 실행하도록 변경하긴 했으나 이미 해당 COM 개체는 별도의 STA 스레드에 묶인 상태입니다.

이런 경우에는 스레드 풀의 모든 요청이 Page_Load 스레드에서 미리 직렬화됩니다. 23712 스레드가 24344 요청을 처리하고 COM 메서드를 실행한 다음 사용자에게 결과를 반환하고, 다시 348 요청을 COM 메서드를 실행한 다음 결과를 반환합니다.

AspCompat=True에서의 이런 문제점은 MSDN의 도움말에 다음과 같이 씌여 있습니다.

COM Component Compatibility
; https://docs.microsoft.com/en-us/previous-versions/aspnet/zwk9h2kb(v=vs.100)

COM components that are created at construction time run before the request is scheduled to the STA thread pool and consequently run on a multithreaded apartment (MTA) thread. This has a substantial negative performance impact and should be avoided. If you use AspCompat with STA components, you should create COM components only from the Page_Load event or later in the execution chain and not at page construction time.





결론을 내려 보면, STA COM 개체를 사용한다면 AspCompat=True로 설정하고 Page_Load 이후에 COM 개체를 생성하는 것이 좋습니다.

그리고, 만약 가능하다면 COM 개체를 만든 업체 측에 MTA로 해달라고 요청하는 것이 성능을 위해 가장 좋습니다.

참고 삼아 AspCompat=True인 경우의 콜 스택을 보면 다음과 같이 나옵니다.

WebApplication1.dll!WebApplication1.WebForm1.Page_Load(object sender, System.EventArgs e) Line 16	C#
System.Web.dll!System.Web.UI.Control.LoadRecursive() + 0x74 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb5e bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb9 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest() + 0x77 bytes	
System.Web.dll!System.Web.HttpApplication.ExecuteStep(System.Web.HttpApplication.IExecutionStep step, ref bool completedSynchronously) + 0xa5 bytes	
System.Web.dll!System.Web.Util.AspCompatApplicationStep.ExecuteAspCompatCode() + 0x83 bytes	
System.Web.dll!System.Web.Util.AspCompatApplicationStep.OnAspCompatExecution() + 0x79 bytes	

ExecuteAspCompatCode, OnAspCompatExecution 2개의 메서드가 눈에 띕니다. 또한 HttpContext.Current가 유효한 단계는 System.Web.UI.Page.ProcessRequest 호출이므로 그 이후의 호출 스택에서만 HTTP 문맥 개체를 사용할 수 있습니다.

비교를 위해 다음은 일반적인 웹 페이지의 콜 스택을 보여주고 있습니다.

WebApplication1.dll!WebApplication1.WebForm1.Page_Load(object sender, System.EventArgs e) Line 16	C#
System.Web.dll!System.Web.UI.Control.LoadRecursive() + 0x74 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb5e bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb9 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest() + 0x77 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest(System.Web.HttpContext context) + 0xa3 bytes	
App_Web_cx5dgpsp.dll!ASP.default_aspx.ProcessRequest(System.Web.HttpContext context) + 0x33 bytes	C#
System.Web.dll!System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() + 0x445 bytes	
System.Web.dll!System.Web.HttpApplication.ExecuteStep(System.Web.HttpApplication.IExecutionStep step, ref bool completedSynchronously) + 0xa5 bytes	
System.Web.dll!System.Web.HttpApplication.PipelineStepManager.ResumeSteps(System.Exception error) + 0x4de bytes	
System.Web.dll!System.Web.HttpApplication.BeginProcessRequestNotification(System.Web.HttpContext context, System.AsyncCallback cb) + 0x85 bytes	
System.Web.dll!System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest wr, System.Web.HttpContext context) + 0x255 bytes	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x47e bytes	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x22 bytes	
[Native to Managed Transition]	
[Managed to Native Transition]	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x632 bytes	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x22 bytes	
[Appdomain Transition]	




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

[연관 글]






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

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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...
NoWriterDateCnt.TitleFile(s)
13566정성태2/27/20249556오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
13565정성태2/23/20248651닷넷: 2219. .NET CLR2 보안 모델에서의 개별 System.Security.Permissions 제어
13564정성태2/22/202410163Windows: 259. Hyper-V Generation 1 유형의 VM을 Generation 2 유형으로 바꾸는 방법
13563정성태2/21/20249646디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
13562정성태2/21/20249360오류 유형: 896. ASP.NET - .NET Framework 기본 예제에서 System.Web에 대한 System.IO.FileNotFoundException 예외 발생
13561정성태2/20/202410163닷넷: 2218. C# - (예를 들어, Socket) 비동기 I/O에 대한 await 호출 시 CancellationToken을 이용한 취소파일 다운로드1
13560정성태2/19/202410215디버깅 기술: 195. windbg 분석 사례 - Semaphore 잠금으로 인한 Hang 현상 (닷넷)
13559정성태2/19/202411040오류 유형: 895. ASP.NET - System.Security.SecurityException: 'Requested registry access is not allowed.'
13558정성태2/18/202410196닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
13557정성태2/18/20249077Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20249456Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20249667닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20249089VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
13553정성태2/14/20248618닷넷: 2215. windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse
13552정성태2/13/20248743닷넷: 2214. windbg - Monitor.Enter의 thin lock과 fat lock
13551정성태2/12/20249841닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
13550정성태2/11/202410293Windows: 256. C# - Server socket이 닫히면 Accept 시켰던 자식 소켓이 닫힐까요?
13549정성태2/3/202411255개발 환경 구성: 706. C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성
13548정성태2/1/202410818개발 환경 구성: 705. "Docker Desktop for Windows" - ASP.NET Core 응용 프로그램의 소켓 주소 바인딩(IPv4/IPv6 loopback, Any)
13547정성태1/31/202410490개발 환경 구성: 704. Visual Studio - .NET 8 프로젝트부터 dockerfile에 추가된 "USER app" 설정
13546정성태1/30/202410164Windows: 255. (디버거의 영향 등으로) 대상 프로세스가 멈추면 Socket KeepAlive로 연결이 끊길까요?
13545정성태1/30/20249711닷넷: 2212. ASP.NET Core - 우선순위에 따른 HTTP/HTTPS 호스트:포트 바인딩 방법
13544정성태1/30/20249354오류 유형: 894. Microsoft.Data.SqlClient - Could not load file or assembly 'System.Security.Permissions, ...'
13543정성태1/30/20249772Windows: 254. Windows - 기본 사용 중인 5357 포트 비활성화는 방법
13542정성태1/30/20248858오류 유형: 893. Visual Studio - Web Application을 실행하지 못하는 IISExpress - 두 번째 이야기
13541정성태1/29/20249624VS.NET IDE: 188. launchSettings.json의 useSSL 옵션
1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...