Microsoft MVP성태의 닷넷 이야기
.NET Framework: 353. x86 - AspCompat과 STA COM 개체가 성능에 미치는 영향 [링크 복사], [링크+제목 복사],
조회: 22066
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  [51]  52  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12665정성태6/9/202117559.NET Framework: 1066. Wslhub.Sdk 사용으로 알아보는 CoInitializeSecurity 사용 제약파일 다운로드1
12664정성태6/9/202115381오류 유형: 723. COM+ PIA 참조 시 "This operation failed because the QueryInterface call on the COM component" 오류
12663정성태6/9/202117739.NET Framework: 1065. Windows Forms - 속성 창의 디자인 설정 지원: 문자열 목록 내에서 항목을 선택하는 TypeConverter 제작파일 다운로드1
12662정성태6/8/202115446.NET Framework: 1064. C# COM 개체를 PIA(Primary Interop Assembly)로써 "Embed Interop Types" 참조하는 방법파일 다운로드1
12661정성태6/4/202127439.NET Framework: 1063. C# - MQTT를 이용한 클라이언트/서버(Broker) 통신 예제 [4]파일 다운로드1
12660정성태6/3/202118169.NET Framework: 1062. Windows Forms - 폼 내에서 발생하는 마우스 이벤트를 자식 컨트롤 영역에 상관없이 수신하는 방법 [1]파일 다운로드1
12659정성태6/2/202119083Linux: 40. 우분투 설치 후 MBR 디스크 드라이브 여유 공간이 인식되지 않은 경우 - Logical Volume Management
12658정성태6/2/202116871Windows: 194. Microsoft Store에 있는 구글의 공식 Youtube App
12657정성태6/2/202117662Windows: 193. 윈도우 패키지 관리자 - winget 설치
12656정성태6/1/202115999.NET Framework: 1061. 서버 유형의 COM+에 적용할 수 없는 Server GC
12655정성태6/1/202114632오류 유형: 722. windbg/sos - savemodule - Fail to read memory
12654정성태5/31/202115310오류 유형: 721. Hyper-V - Saved 상태의 VM을 시작 시 오류 발생
12653정성태5/31/202118554.NET Framework: 1060. 닷넷 GC에 새롭게 구현되는 DPAD(Dynamic Promotion And Demotion for GC)
12652정성태5/31/202116029VS.NET IDE: 164. Visual Studio - Web Deploy로 Publish 시 암호창이 매번 뜨는 문제
12651정성태5/31/202116230오류 유형: 720. PostgreSQL - ERROR: 22P02: malformed array literal: "..."
12650정성태5/17/202115600기타: 82. OpenTabletDriver의 버튼에 더블 클릭을 매핑 및 게임에서의 지원 방법
12649정성태5/16/202117761.NET Framework: 1059. 세대 별 GC(Garbage Collection) 방식에서 Card table의 사용 의미 [1]
12648정성태5/16/202116517사물인터넷: 66. PC -> FTDI -> NodeMCU v1 ESP8266 기기를 UART 핀을 연결해 직렬 통신하는 방법파일 다운로드1
12647정성태5/15/202116758.NET Framework: 1058. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용파일 다운로드1
12646정성태5/15/202115567사물인터넷: 65. C# - Arduino IDE의 Serial Monitor 기능 구현파일 다운로드1
12645정성태5/14/202115644사물인터넷: 64. NodeMCU v1 ESP8266 - LittleFS를 이용한 와이파이 접속 정보 업데이트파일 다운로드1
12644정성태5/14/202116903오류 유형: 719. 윈도우 - 제어판의 "프로그램 및 기능" / "Windows 기능 켜기/끄기" 오류 0x800736B3
12643정성태5/14/202116898오류 유형: 718. 서버 유형의 COM+ 사용 시 0x80080005(Server execution failed) 오류 발생
12642정성태5/14/202118541오류 유형: 717. The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine.
12641정성태5/13/202117358디버깅 기술: 179. 윈도우용 .NET Core 3 이상에서 Windbg의 sos 사용법
12640정성태5/13/202120982오류 유형: 716. RDP 연결 - Because of a protocol error (code: 0x112f), the remote session will be disconnected. [1]
... 46  47  48  49  50  [51]  52  53  54  55  56  57  58  59  60  ...