Microsoft MVP성태의 닷넷 이야기
.NET Framework: 814. Critical Finalizer와 SafeHandle의 사용 의미 [링크 복사], [링크+제목 복사],
조회: 11801
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

Critical Finalizer와 SafeHandle의 사용 의미

다음의 글에 보면, 이에 대한 설명이 자세하게 나와 있습니다.

2005년 10월 MSDN Magazine
Keep Your Code Running with the Reliability Features of the .NET Framework
; https://docs.microsoft.com/en-us/archive/msdn-magazine/2005/october/using-the-reliability-features-of-the-net-framework

CHM
; http://download.microsoft.com/download/3/a/7/3a7fa450-1f33-41f7-9e6d-3aa95b5a6aea/MSDNMagazineOctober2005en-us.chm

우선 Critical Finalizer를 이해하기 위해서는 "application domain unload"의 "graceful" 방식과 "rude" 방식을 알아야 합니다. 그리고 그것에 대한 이해를 하기 위해서는 다시 "thread abort"에 대해 "graceful" 방식과 "rude" 방식을 알아야 합니다. 위의 문서에 따라 설명해 보면, ThreadAbortException 예외가 던져지면 스레드 종료(abort) 작업에 들어가게 됩니다. 하지만 해당 스레드는 thread abort 관련한 작업을 우선 처리해야 하기 때문에 그 작업으로 인해 실제적인 스레드 종료 작업이 안 될 수도 있습니다. 이런 경우를 위해 CLR은 보다 거친(rude) 방식으로 수행하는 스레드 종료 작업을 제공하는데 그걸 일컬어서 "rude thread abort"라고 합니다. 이 방식의 스레드 종료 작업은 일반적인 thread abort에 관련된 작업을 처리하지 않고 무조건 스레드 실행을 중지시킨다고 합니다. 따라서 CLR은 CER 내에서 코드가 실행되지 않는 한 해당 스레드에서 실행 중이던 back-out 코드에 대해 어떠한 안전성도 제공할 수 없는 것입니다.

그리고 "application domain unload" 방식에 있어 해당 AppDomain과 관련된 스레드를 "graceful"하게 종료한다면 그것이 "graceful application domain unload"이고, 반면 rude하게 스레드를 종료시킨다면 "rude application domain unload"가 됩니다. "graceful" application domain unload의 경우 (CER로 지정되지 않았어도) finally 블록과 AppDomain에 관여된 finalizer를 처리하는 반면 "rude" 방식에서는 그런 것들이 보장되지 않습니다.

물론, 일반적인 .NET 응용 프로그램에서 "rude" 방식의 thread abort나 application domain unload 작업이 수행되는 것은 아니고, CLR의 정책이 그렇게 설정된 경우에 한해 동작을 하는 것입니다. 그리고 이런 정책이 설정된 환경이 바로 SQL Server 2005에서부터 지원하는 CLR 호스팅으로, 여기서는 graceful하게 수행되는 thread abort 작업이 SQL 서버가 지정한 시간 내에 스레드가 종료되지 않으면 강제로 "rude"하게 thread abort를 처리합니다. 마찬가지로 application domain unload 작업도 graceful하게 진행할 시간이 지나면 강제로 "rude"하게 진행하게 됩니다.

.NET 2.0부터, CLR의 "graceful" thread abort는 다음의 경우에 대해 종료 작업을 지연시킵니다.

  • CER 영역 내의 코드 수행 중,
  • finally 블록 내의 코드 수행 중,
  • catch block 내의 코드 수행 중,
  • 정적 생성자 내의 코드 수행 중,
  • unmanaged 코드 수행 중

하지만, "rude" thread abort는 위의 경우에서 CER과 unmanaged 코드 수행 중을 제외하고는 강제로 스레드를 종료시킬 수 있습니다.




"rude" thread abort가 일반적인 finalizer에 대한 수행을 보장하지 않는 문제는 .NET 2.0에서 새롭게 도입된 "critical finalizer"로 해결이 됩니다. 이 유형의 finalizer는 CER 내에서 수행되고 따라서 "rude application domain unload"에서도 실행을 보장받게 됩니다.

critical finalizer를 구현하려면 해당 클래스를 CriticalFinalizerObject 타입으로부터 상속해야 합니다.

namespace System.Runtime.ConstrainedExecution
{
    [ComVisible(true)]
    [SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
    public abstract class CriticalFinalizerObject
    {
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected CriticalFinalizerObject()
        {
        }

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        ~CriticalFinalizerObject()
        {
        }
    }
}

일단 CriticalFinalizerObject로부터 상속받은 타입에서 finalizer를 동일하게 "CriticalFinalizerObject.~CriticalFinalizerObject" 메서드에 지정한 ReliabilityContract를 부여하면 그 메서드는 CER에서 실행됩니다.

CriticalFinalizerObject를 상속받는 대표적인 사례가 바로 SafeHandle 타입입니다. 달리 말하면 SafeHandle로 처리한 경우 rude thread abort 시에도 종료 작업을 보장받게 됩니다. SafeHandle로 인한 특별한 장점이 있다면 2가지 정도가 있습니다.

첫 번째는, .NET 1.x 시절에는 IntPtr을 이용한 네이티브 리소스를 관리하면서 Finalizer를 구현해야 했지만 SafeHandle을 사용함으로써 사용자 업무 클래스에서는 더 이상 Finalizer를 구현해야 할 이유가 사라졌습니다. 왜냐하면 어차피 사용자 클래스는 SafeHandle을 소유하고 있을 테고, SafeHandle은 CriticalFinalizerObject로부터 상속한 타입이므로 GC는 그와 관련된 네이티브 자원을 해제하기 위한 Finalizer를 호출할 것이기 때문입니다.

두 번째는, SafeHandle의 스레드 abort에 대한 안정성입니다. 가령, .NET 1.x 시절에 작성한 다음의 코드는,

private static void AllocIntPtr()
{
    IntPtr memPtr = Marshal.AllocHGlobal(0x100);
    try
    {
    }
    finally { Marshal.FreeHGlobal(memPtr); }
}

AllocHGlobal에서 메모리 할당을 한 후, memPtr에 대입하는 시점에 thread abort가 발생하면 리소스 누수 현상으로 이어집니다. 하지만, SafeHandle은 다음과 같이 사용하게 되므로,

[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public sealed class SafeLocalAllocArrayHandle :
    SafeHandleZeroOrMinusOneIsInvalid
{
    [DllImport("kernel32.dll")]
    private static extern SafeLocalAllocArrayHandle LocalAlloc(LMEM uFlags, IntPtr sizetdwBytes);

    int _size = 0;

    private SafeLocalAllocArrayHandle() : base(true)
    {
    }

    public static SafeLocalAllocArrayHandle Alloc(int size)
    {
        SafeLocalAllocArrayHandle handle = LocalAlloc(LMEM.ZEROINIT, new IntPtr(size));

        if (handle.IsInvalid == false)
        {
            handle._size = size;
        }

        return handle;
    }

    protected override bool ReleaseHandle()
    {
        return LocalFree(handle) == IntPtr.Zero;
    }

    [SuppressUnmanagedCodeSecurity]
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr LocalFree(IntPtr handle);

    public enum LMEM
    {
        LHND = 0x42,

        FIXED = 0,
        MOVEABLE = 0x2,
        ZEROINIT = 0x40,
    }
}

애초에 SafeHandle로 반환받게 되고 CLR은 그 과정에 thread abort가 개입하지 못하도록 합니다. 따라서 LocalAlloc이 메모리 할당에 성공했다면 반환받은 SafeHandle에는 언제나 값을 담고 있게 되는 것입니다.

(첨부 파일은 이 글의 예제 프로젝트를 포함합니다.)




정리해 보면, SafeHandle의 등장으로 인해 응용 프로그램 개발자는 더 이상 Finalizer를 사용자 클래스에 구현할 필요가 없어졌습니다. 자원 해제가 필요한 네이티브 리소스는 무조건 SafeHandle로 감싸서 처리하면 되기 때문입니다.





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

[연관 글]






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

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

비밀번호

댓글 작성자
 




... 31  32  [33]  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12805정성태8/24/20218263.NET Framework: 1104. C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능파일 다운로드1
12804정성태8/24/20219032.NET Framework: 1103. C# 10 - (7) Source Generator V2 APIs
12803정성태8/23/20218617개발 환경 구성: 600. pip cache 디렉터리 옮기는 방법
12802정성태8/23/20218927.NET Framework: 1102. .NET Conf Mini 21.08 - WinUI 3 따라해 보기 [1]
12801정성태8/23/20218431.NET Framework: 1101. C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용파일 다운로드1
12800정성태8/22/20218668개발 환경 구성: 599. PyCharm - (반대로) 원격 프로세스가 PyCharm에 디버그 연결하는 방법
12799정성태8/22/20218729.NET Framework: 1100. C# 10 - (5) 속성 패턴의 개선파일 다운로드1
12798정성태8/21/202110065개발 환경 구성: 598. PyCharm - 원격 프로세스를 디버그하는 방법
12797정성태8/21/20217760Windows: 197. TCP의 MSS(Maximum Segment Size) 크기는 고정된 것일까요?
12796정성태8/21/20218434.NET Framework: 1099. C# 10 - (4) 상수 문자열에 포맷 식 사용 가능파일 다운로드1
12795정성태8/20/20219065.NET Framework: 1098. .NET 6에 포함된 신규 BCL API - 스레드 관련
12794정성태8/20/20218506스크립트: 23. 파이썬 - WSGI를 만족하는 최소한의 구현 코드 및 PyCharm에서의 디버깅 방법 [1]
12793정성태8/20/20219227.NET Framework: 1097. C# 10 - (3) 개선된 변수 초기화 판정파일 다운로드1
12792정성태8/19/20219703.NET Framework: 1096. C# 10 - (2) 전역 네임스페이스 선언파일 다운로드1
12791정성태8/19/20218031.NET Framework: 1095. C# COM 개체를 C++에서 사용하는 예제 [3]파일 다운로드1
12790정성태8/18/202110260.NET Framework: 1094. C# 10 - (1) 구조체를 생성하는 record struct파일 다운로드1
12789정성태8/18/20219263개발 환경 구성: 597. PyCharm - 윈도우 환경에서 WSL을 이용해 파이썬 앱 개발/디버깅하는 방법
12788정성태8/17/20217829.NET Framework: 1093. C# - 인터페이스의 메서드가 다형성을 제공할까요? (virtual일까요?)파일 다운로드1
12787정성태8/17/20218037.NET Framework: 1092. (책 내용 수정) "4.5.1.4 인터페이스"의 "인터페이스와 다형성"
12786정성태8/16/20219569.NET Framework: 1091. C# - Python range 함수 구현 (2) INumber<T>를 이용한 개선 [1]파일 다운로드1
12785정성태8/16/20217826.NET Framework: 1090. .NET 6 Preview 7에 추가된 숫자 형식에 대한 제네릭 연산 지원 [1]파일 다운로드1
12784정성태8/15/20217220오류 유형: 757. 구글 메일 - 아웃룩에서 메일 전송 시 Sending' reported error (0x800CCC0F, 0x800CCC92)
12783정성태8/15/20216788.NET Framework: 1089. C# - Indexer에 Range 및 람다 식을 이용한 필터 구현 [1]파일 다운로드1
12782정성태8/14/20216577오류 유형: 756. 파이썬 - 윈도우 환경에서 pytagcloud의 한글 출력 방법
12781정성태8/14/20218736오류 유형: 755. 파이썬 - konlpy 사용 시 JVM과 jpype1 관련 오류
12780정성태8/13/20217117.NET Framework: 1088. C# - 버스 노선 및 위치 정보 조회 API 사용을 위한 기초 라이브러리 [2]
... 31  32  [33]  34  35  36  37  38  39  40  41  42  43  44  45  ...