2020년 6월 23일 화요일

C# 객체에서의 unmanged/manged resource 처리 관련 (Dispose pattern)

예전에 C# RAII(Resource Acquisition Is Initialization) 게시물에서 적기는 했었는데

C# garbage collection의 정책상 object instance가 scope을 벗어나거나 더이상 사용되지 않더라도 바로 destory되지 않으므로
- 명시적인 resource 해제를 위한 IDisposable 인터페이스를 구현하고
- using문을 사용하여 object가 관리(생성,사용,삭제)되도록 해야 한다.


아래는 IDisposable interface에 대한 내용인데 Dispose pattern을 소개하고 있다. 

IDisposable interface

Implement a Dispose method


Dispose pattern 예제 코드이다. 영어 주석을 간략히 한글로 달았음.

    public class MyResource: IDisposable
    {
        // 외부에서 전달받은 unmanaged 리소스
        private IntPtr handle;
        // 내부에서 생성하는 managed 리소스
        private Component component = new Component();
        // Dispose가 호출 되었음을 알려주는 flag
        private bool disposed = false;

        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // 리소스를 해제하기 위한 method
        // 하위 class에서 재정의 하지 못하도록 virtual이 아님
        public void Dispose()
        {
            Dispose(true);
            // object가 위 Dispose메서드 호출로 정리되지만 
            // object가 finalization queue에 포함되어 종료자/finalizer가 호출되지 않도록 
            // 아래 명령어를 호출한다.
            GC.SuppressFinalize(this);
        }

        // 아래 함수가 호출되는 경우는 두가지인데
        // 첫번째는(disposing == true) 사용자의 코드를 통해 호출되는 경우, 이 때는 managed/unmanaged 리소스를 해제해야 한다.
        // 두번째는(disposing == false) finalizer가 호출한 상황으로 다른 obejct가 어떤상황인지 모르므로 unmanaged resource만 해제해야 한다.
        protected virtual void Dispose(bool disposing)
        {
            if(!this.disposed)
            {
                if(disposing)
                {
                    // 첫번째 경우 managed 리소스 해제
                    component.Dispose();
                }
                // 첫번째, 두번째 경우 unmanaged 리소스 해제
                CloseHandle(handle);
                handle = IntPtr.Zero;
                // dispose가 호출되었음을 기록
                disposed = true;
            }
        }

        // unmanaged 리소스를 해제하기 위한 interop
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle);

        // finalization을 위한 종료자/finalizer(소멸자/destructor)
        // Dispose()가 호출되면 아래 코드는 호출되지 않는다.
        ~MyResource()
        {
            Dispose(false);
        }
    }


위 Dispose pattern을 따라 구현하면
- Dispose()를 명시적으로 호출하여 자원을 해제하게 하거나
  사용자가 Dispose() 호출을 잊어버렸을 때 자원을 해제할 수 있게 한다.
- Dispose()를 여러번 호출해도 문제가 없음
- 종료자/finalizer 가 호출 될 때 object가 Dispose가 된 상태라도 문제가 없다.

하지만 이 pattern이 자원 해제와 메모리 해제의 결합도를 가지게하여 종료자 스레드의 부담을 증가한다는 점이 있다고 한다.

함께 위 패턴을 사용함에 있어 Dispose 구현을 사용하게 하려면 Using 문이나 Try/Finally 블록을 사용하면 된다. 어차피 Using이 Try/Finally 생성처럼 동작한다고 함.

Using block has three parts: acquisition, usage, and disposal.

  • Acquisition means creating a variable and initializing it to refer to the system resource. The Using statement can acquire one or more resources, or you can acquire exactly one resource before entering the block and supply it to the Using statement. If you supply resourceexpression, you must acquire the resource before passing control to the Using statement.

  • Usage means accessing the resources and performing actions with them. The statements between Using and End Using represent the usage of the resources.

  • Disposal means calling the Dispose method on the object in resourcename. This allows the object to cleanly terminate its resources. The End Using statement disposes of the resources under the Using block's control.


만약 부모 클래스를 상속하는 자식 클래스가 있다면 자식 클래스에서는 다음과 같은 패턴을 구현해야 한다고 함.

Subclasses should implement the disposable pattern as follows:

  • They must override Dispose(Boolean) and call the base class Dispose(Boolean) implementation.

  • They can provide a finalizer if needed. The finalizer must call Dispose(false).


아래 예제에서는 종료자/finalizer는 구현하지 않는 자식클래스 예제임.

class DerivedClass : BaseClass
{
   bool disposed = false;
   SafeHandle handle = new SafeFileHandle(IntPtr.Zero, true);

   protected override void Dispose(bool disposing)
   {
      if (disposed)
         return;

      if (disposing) {
         handle.Dispose();
         // 다른 managed 리소스를 해제
      }

      // unmanaged 리소스를 해제

      disposed = true;
      // 부모 클래스의 Dispose(bool)를 호출
      base.Dispose(disposing);
   }
}


그리고 아래 책을 보면

C# 6.0 완벽 가이드 깊고 넓게 알려주는 레퍼런스 북 [ 전2권 ]
조셉 앨버허리, 벤 앨버허리 공저 / 류광 역 | 인사이트(insight) 

종료자/finalizer에 대한 언급이 있다.

객체에 종료자/finalizer가 있다면 인스턴스의 메모리 해제 시 종료자/finalizer가 호출된다. 
. 종료자/finalizer가 없다면 객체는 garbage collection 시 바로 삭제되지만
  종료자/finalizer가 있다면 별도의 대기열에 저장하여 관리하며 종료자/finalizer 호출 전까지 객체가 유지된다. 
. 종료자 때문에 메모리할당과 garbage collection이 느려진다. (종료자 객체들의 관리 및 처리)
. 종료자는 객체와 객체가 참조하는 객체의 수명을 필요 이상으로 늘린다.
. 종료자의 호출 순서를 예측할 수 없고 시점을 제어할 수 없다.
. 종료자안에서 코드 실행이 차단되면 다른 객체의 종료자가 호출되지 못한다.
. app이 제대로 종료되지 않는다면 종료자들이 호출되지 않을 수 있다.

그래서 더더욱 resource 해제를 위해서라면 위 Dispose pattern을 구현하는것이 맞겠다.


마지막으로 Garbage collection에 대해 설명하는 공식 문서

댓글 없음:

댓글 쓰기