Microsoft MVP성태의 닷넷 이야기
.NET Framework: 353. x86 - AspCompat과 STA COM 개체가 성능에 미치는 영향 [링크 복사], [링크+제목 복사],
조회: 15464
글쓴 사람
정성태 (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)
13615정성태5/3/202458닷넷: 2255. C# 배열을 Numpy ndarray 배열과 상호 변환
13614정성태5/2/2024169닷넷: 2254. C# - COM 인터페이스의 상속 시 중복으로 메서드를 선언
13613정성태5/1/2024219닷넷: 2253. C# - Video Capture 장치(Camera) 열거 및 지원 포맷 조회파일 다운로드1
13612정성태4/30/2024198오류 유형: 902. Visual Studio - error MSB3021: Unable to copy file
13611정성태4/29/2024387닷넷: 2252. C# - GUID 타입 전용의 UnmanagedType.LPStruct - 두 번째 이야기파일 다운로드1
13610정성태4/28/2024487닷넷: 2251. C# - 제네릭 인자를 가진 타입을 생성하는 방법 - 두 번째 이야기
13609정성태4/27/2024674닷넷: 2250. PInvoke 호출 시 참조 타입(class)을 마샬링하는 [IN], [OUT] 특성파일 다운로드1
13608정성태4/26/2024979닷넷: 2249. C# - 부모의 필드/프로퍼티에 대해 서로 다른 자식 클래스 간에 Reflection 접근이 동작할까요?파일 다운로드1
13607정성태4/25/20241026닷넷: 2248. C# - 인터페이스 타입의 다중 포인터를 인자로 갖는 C/C++ 함수 연동
13606정성태4/24/2024987닷넷: 2247. C# - tensorflow 연동 (MNIST 예제)파일 다운로드1
13605정성태4/23/2024973닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동파일 다운로드1
13604정성태4/22/2024971오류 유형: 901. Visual Studio - Unable to set the next statement. Set next statement cannot be used in '[Exception]' call stack frames.
13603정성태4/21/20241022닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동파일 다운로드1
13602정성태4/20/20241011닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)파일 다운로드1
13601정성태4/19/20241025닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/20241054닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024964닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/20241011닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드2
13597정성태4/15/20241043닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/20241133닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/20241086닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241109닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241113닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241457C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241436닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...