Microsoft MVP성태의 닷넷 이야기
.NET Framework: 814. Critical Finalizer와 SafeHandle의 사용 의미 [링크 복사], [링크+제목 복사]
조회: 910
글쓴 사람
홈페이지
첨부 파일

Critical Finalizer와 SafeHandle의 사용 의미

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

2005년 10월 MSDN Magazine
Keep Your Code Running with the Reliability Features of the .NET Framework
; https://web.archive.org/web/20150423173148/https://msdn.microsoft.com/en-us/magazine/cc163716.aspx

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로 감싸서 처리하면 되기 때문입니다.





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

[연관 글]





[최초 등록일: ]
[최종 수정일: 4/15/2019 ]

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

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
11929정성태6/5/2019962.NET Framework: 841. Windows Forms/C# - 클립보드에 RTF 텍스트를 복사 및 확인하는 방법
11928정성태6/5/2019988오류 유형: 543. PowerShell 확장 설치 시 "Catalog file '[...].cat' is not found in the contents of the module" 오류 발생
11927정성태6/5/20191200스크립트: 15. PowerShell ISE의 스크립트를 복사 후 PPT/Word에 붙여 넣으면 한글이 깨지는 문제 [1]
11926정성태6/4/20191326오류 유형: 542. Visual Studio - pointer to incomplete class type is not allowed
11925정성태6/4/20191332VC++: 131. Visual C++ - uuid 확장 속성과 __uuidof 확장 연산자파일 다운로드1
11924정성태5/30/20191409Math: 57. C# - 해석학적 방법을 이용한 최소 자승법 [1]파일 다운로드1
11923정성태5/30/20191180Math: 56. C# - 그래프 그리기로 알아보는 경사 하강법의 최소/최댓값 구하기파일 다운로드1
11922정성태5/30/2019849.NET Framework: 840. ML.NET 데이터 정규화파일 다운로드1
11921정성태5/28/20191285Math: 55. C# - 다항식을 위한 최소 자승법(Least Squares Method)파일 다운로드1
11920정성태5/28/2019966.NET Framework: 839. C# - PLplot 색상 제어
11919정성태5/27/20191232Math: 54. C# - 최소 자승법의 1차 함수에 대한 매개변수를 단순 for 문으로 구하는 방법 [1]파일 다운로드1
11918정성태5/27/20191344Math: 53. C# - 행렬식을 이용한 최소 자승법(LSM: Least Square Method)파일 다운로드1
11917정성태5/24/20191267Math: 52. MathNet을 이용한 간단한 통계 정보 처리 - 분산/표준편차파일 다운로드1
11916정성태5/24/2019896Math: 51. MathNET + OxyPlot을 이용한 간단한 통계 정보 처리 - Histogram파일 다운로드1
11915정성태5/24/2019966Linux: 11. 리눅스의 환경 변수 관련 함수 정리 - putenv, setenv, unsetenv
11914정성태5/24/20191426Linux: 10. 윈도우의 GetTickCount와 리눅스의 clock_gettime파일 다운로드1
11913정성태5/24/2019955.NET Framework: 838. C# - 숫자형 타입의 bit(2진) 문자열, 16진수 문자열 구하는 방법파일 다운로드1
11912정성태5/23/2019891VS.NET IDE: 137. Visual Studio 2019 버전 16.1부터 리눅스 C/C++ 프로젝트에 추가된 WSL 지원
11911정성태5/23/2019974VS.NET IDE: 136. Visual Studio 2019 - 리눅스 C/C++ 프로젝트에 인텔리센스가 동작하지 않는 경우
11910정성태5/24/20191908Math: 50. C# - MathNet.Numerics의 Matrix(행렬) 연산파일 다운로드1
11909정성태5/25/20191403.NET Framework: 837. C# - PLplot 사용 예제 [1]파일 다운로드1
11908정성태5/22/2019930.NET Framework: 836. C# - Python range 함수 구현파일 다운로드1
11907정성태6/4/2019744오류 유형: 541. msbuild - MSB4024 The imported project file "...targets" could not be loaded
11906정성태5/21/2019667.NET Framework: 835. .NET Core/C# - 리눅스 syslog에 로그 남기는 방법
11905정성태5/21/2019861.NET Framework: 834. C# - 폴더 경로 문자열에서 "..", "." 표기를 고려한 최종 문자열을 얻는 방법 - 두 번째 이야기
11904정성태5/21/20191709.NET Framework: 833. C# - Open Hardware Monitor를 이용한 CPU 온도 정보파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...