Microsoft MVP성태의 닷넷 이야기
.NET Framework: 814. Critical Finalizer와 SafeHandle의 사용 의미 [링크 복사], [링크+제목 복사],
조회: 20044
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  101  102  103  [104]  105  ...
NoWriterDateCnt.TitleFile(s)
11325정성태10/14/201719281.NET Framework: 689. CLR 4.0 환경에서 DLL 모듈의 로드 주소(Base address) 알아내는 방법
11324정성태10/13/201720860디버깅 기술: 101. windbg - "*** WARNING: Unable to verify checksum for" 경고 없애는 방법
11322정성태10/13/201718277디버깅 기술: 100. windbg - .NET 4.0 응용 프로그램의 Main 메서드에 Breakpoint 걸기
11321정성태10/11/201719800.NET Framework: 688. NGen 모듈과 .NET Profiler
11320정성태10/11/201720590.NET Framework: 687. COR_PRF_USE_PROFILE_IMAGES 옵션과 NGen의 "profiler-enhanced images" [1]
11319정성태10/11/201728160.NET Framework: 686. C# - string 배열을 담은 구조체를 직렬화하는 방법
11318정성태10/7/201720928VS.NET IDE: 122. 비주얼 스튜디오에서 관리자 권한을 요구하는 C# 콘솔 프로그램 제작 [1]
11317정성태10/4/201726102VC++: 120. std::copy 등의 함수 사용 시 _SCL_SECURE_NO_WARNINGS 에러 발생
11316정성태9/30/201724143디버깅 기술: 99. (닷넷) 프로세스(EXE)에 디버거가 연결되어 있는지 아는 방법 [4]
11315정성태9/29/201740226기타: 68. "시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지" 구매하신 분들을 위한 C# 7.0/7.1 추가 문법 PDF [8]
11314정성태9/28/201722003디버깅 기술: 98. windbg - 덤프 파일로부터 닷넷 버전 확인하는 방법
11313정성태9/25/201719303디버깅 기술: 97. windbg - 메모리 덤프로부터 DateTime 형식의 값을 알아내는 방법파일 다운로드1
11312정성태9/25/201722397.NET Framework: 685. C# - 구조체(값 형식)의 필드를 리플렉션을 이용해 값을 바꾸는 방법파일 다운로드1
11311정성태9/20/201716833.NET Framework: 684. System.Diagnostics.Process 객체의 명시적인 해제 권장
11310정성태9/19/201720255.NET Framework: 683. WPF의 Window 객체를 생성했는데 GC 수집 대상이 안 되는 이유 [3]
11309정성태9/13/201718382개발 환경 구성: 335. Octave의 명령 창에서 실행한 결과를 복사하는 방법
11308정성태9/13/201719442VS.NET IDE: 121. 비주얼 스튜디오에서 일부 텍스트 파일을 무조건 메모장으로만 여는 문제파일 다운로드1
11307정성태9/13/201721973오류 유형: 421. System.Runtime.InteropServices.SEHException - 0x80004005
11306정성태9/12/201720027.NET Framework: 682. 아웃룩 사용자를 위한 중국어 스팸 필터 Add-in
11305정성태9/12/201721523개발 환경 구성: 334. 기존 프로젝트를 Visual Studio를 이용해 Github의 신규 생성된 repo에 올리는 방법 [1]
11304정성태9/11/201718649개발 환경 구성: 333. 3ds Max를 Hyper-V VM에서 실행하는 방법
11303정성태9/11/201721956개발 환경 구성: 332. Inno Setup 파일의 관리자 권한을 제거하는 방법
11302정성태9/11/201718183개발 환경 구성: 331. SQL Server Express를 위한 방화벽 설정
11301정성태9/11/201717081오류 유형: 420. SQL Server Express 연결 오류 - A network-related or instance-specific error occurred while establishing a connection to SQL Server.
11300정성태9/10/201720943.NET Framework: 681. dotnet.exe - run, exec, build, restore, publish 차이점 [3]
11299정성태9/9/201719679개발 환경 구성: 330. Hyper-V VM의 Internal Network를 Private 유형으로 만드는 방법
... 91  92  93  94  95  96  97  98  99  100  101  102  103  [104]  105  ...