Microsoft MVP성태의 닷넷 이야기
.NET Framework: 814. Critical Finalizer와 SafeHandle의 사용 의미 [링크 복사], [링크+제목 복사],
조회: 11819
글쓴 사람
정성태 (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)
12606정성태4/21/202112778.NET Framework: 1042. C# - enum 값을 int로 암시적(implicit) 형변환하는 방법? [2]파일 다운로드1
12605정성태4/18/20218782.NET Framework: 1041. C# - AssemblyID, ModuleID를 관리 코드에서 구하는 방법파일 다운로드1
12604정성태4/18/20217451VS.NET IDE: 163. 비주얼 스튜디오 속성 창의 "Build(빌드)" / "Configuration(구성)"에서의 "활성" 의미
12603정성태4/16/20218347VS.NET IDE: 162. 비주얼 스튜디오 - 상속받은 컨트롤이 디자인 창에서 지원되지 않는 문제
12602정성태4/16/20219536VS.NET IDE: 161. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 [1]
12601정성태4/15/20218657.NET Framework: 1040. C# - REST API 대신 github 클라이언트 라이브러리를 통해 프로그래밍으로 접근
12600정성태4/15/20218869.NET Framework: 1039. C# - Kubeconfig의 token 설정 및 인증서 구성을 자동화하는 프로그램
12599정성태4/14/20219531.NET Framework: 1038. C# - 인증서 및 키 파일로부터 pfx/p12 파일을 생성하는 방법파일 다운로드1
12598정성태4/14/20219655.NET Framework: 1037. openssl의 PEM 개인키 파일을 .NET RSACryptoServiceProvider에서 사용하는 방법 (2)파일 다운로드1
12597정성태4/13/20219715개발 환경 구성: 569. csproj의 내용을 공통 설정할 수 있는 Directory.Build.targets / Directory.Build.props 파일
12596정성태4/12/20219439개발 환경 구성: 568. Windows의 80 포트 점유를 해제하는 방법
12595정성태4/12/20218876.NET Framework: 1036. SQL 서버 - varbinary 타입에 대한 문자열의 CAST, CONVERT 변환을 C# 코드로 구현
12594정성태4/11/20218319.NET Framework: 1035. C# - kubectl 명령어 또는 REST API 대신 Kubernetes 클라이언트 라이브러리를 통해 프로그래밍으로 접근 [1]파일 다운로드1
12593정성태4/10/20219485개발 환경 구성: 567. Docker Desktop for Windows - kubectl proxy 없이 k8s 대시보드 접근 방법
12592정성태4/10/20219290개발 환경 구성: 566. Docker Desktop for Windows - k8s dashboard의 Kubeconfig 로그인 및 Skip 방법
12591정성태4/9/202112584.NET Framework: 1034. C# - byte 배열을 Hex(16진수) 문자열로 고속 변환하는 방법 [2]파일 다운로드1
12590정성태4/9/20219070.NET Framework: 1033. C# - .NET 4.0 이하에서 Console.IsInputRedirected 구현 [1]
12589정성태4/8/202110393.NET Framework: 1032. C# - Environment.OSVersion의 문제점 및 윈도우 운영체제의 버전을 구하는 다양한 방법 [1]
12588정성태4/7/202110976개발 환경 구성: 565. PowerShell - New-SelfSignedCertificate를 사용해 CA 인증서 생성 및 인증서 서명 방법
12587정성태4/6/202111752개발 환경 구성: 564. Windows 10 - ClickOnce 배포처럼 사용할 수 있는 MSIX 설치 파일 [1]
12586정성태4/5/20219457오류 유형: 710. Windows - Restart-Computer / shutdown 명령어 수행 시 Access is denied(E_ACCESSDENIED)
12585정성태4/5/20219235개발 환경 구성: 563. 기본 생성된 kubeconfig 파일의 내용을 새롭게 생성한 인증서로 구성하는 방법
12584정성태4/1/20219918개발 환경 구성: 562. kubeconfig 파일 없이 kubectl 옵션만으로 실행하는 방법
12583정성태3/29/202111426개발 환경 구성: 561. kubectl 수행 시 다른 k8s 클러스터로 접속하는 방법
12582정성태3/29/202110084오류 유형: 709. Visual C++ - 컴파일 에러 error C2059: syntax error: '__stdcall'
12581정성태3/28/202110025.NET Framework: 1031. WinForm/WPF에서 Console 창을 띄워 출력하는 방법 (2) - Output 디버깅 출력을 AllocConsole로 우회 [2]
... 31  32  33  34  35  36  37  38  39  40  [41]  42  43  44  45  ...