Programming and my thoughts

메모리 관리는 고전적인 주제이지만 다루기가 쉽지않다.

기본적인 내용은 (Dispose 패턴에 대한 소개 및 쓰는 방법 등등) 인터넷에 상세한 설명이 많으니 여기서는 조금 더 복잡한 내용을 간단히 정리해 둔다.


* 참고자료

https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/dispose-pattern


아래 예제 코드를 보자.


출처 : https://stackoverflow.com/questions/898828/finalize-dispose-pattern-in-c-sharp


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class B : IDisposable
{    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // get rid of managed resources
        }   
        // get rid of unmanaged resources
    }
 
    // only if you use unmanaged resources directly in B
    //~B()
    //{
    //    Dispose(false);
    //}
}
 
public class C : B
{
    private IntPtr m_Handle;
 
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // get rid of managed resources
        }
        ReleaseHandle(m_Handle);
 
        base.Dispose(disposing);
    }
 
    ~C() {
        Dispose(false);
    }
}
cs


위 코드에 Dispose 패턴에 대해 깔끔하게 정리되어 있다.


기본적으로 C# 에서 동작하는 메모리 관리는 다음과 같다.


1) A 라는 자원을 쓰지 않는다.

2) 개발자가 A.Dispose() 를 이용하여 자원을 반환한다. 

3) 어느 순간 (언제가 될지 알 수 없음) GC (가비지 컬렉터) 가 와서 "어? A 를 쓰는 곳이 없구나... A 는 안쓰는거니 Finalization Queue 에 넣어야지" 하고 표시를 한다.

4) 언제가 될지 알 수 없으나... 다음에 다시 GC 가 왔을 때에 해당 자원 A 를 반환한다.


코드에서 GC.SuppressFinalize(this) 는 ... 위 과정중 3) 번을 건너뛰게 만드는 역할을 한다.

자원이 Finalization Queue 에 들어가서 반환될 때까지 기다리는 과정이 줄어드는 것이다.


1. IDisposable 을 구현하는 Base 클래스에서만 GC.SuppressFinalize(this) 를 호출하도록 한다.


Base 를 상속받는 클래스에서는 GC.SuppressFinalize(this) 를 호출할 필요가 없다.

SuppressFinalize 는 "이 리소스는 이제 안쓰니까 Finalization Queue 에 넣겠습니다. 다음번에 GC 에서 이 리소스를 완전히 반환해주세요" 라는 과정을 건너뛰게 한다고 했다.

즉, 다음번 GC 가 메모리 반환을 수행할 때... SuppressFinalize 를 수행한 리소스에 대해서는 바로 반환하는 것이다.

하지만, 자식 클래스 입장에서 Dispose 가 실행될 때... 부모(Base) 클래스에 대해서는 아직 Dispose 가 완료되지 않았다.

GC.SuppressFinalize 는 번거로운 메모리 반환 과정(Finalization Queue)을 건너뛰게 해주는 것인데,

부모 클래스에 대한 Dispose 가 완료되지 않았기 때문에... 이것을 호출해서는 안된다.


부모 클래스에서만 이것이 호출되도록 하자. base.Dispose() 를 이용하면 된다.


2. 클래스 내부에 Unmanaged Resource 가 없다면 Finalizer 를 구현할 필요가 없다.


Finalizer 는 ~C() 이렇게 생긴 것을 말하는데... C++ 의 Destructor 와 비슷한 개념이나 동작 자체는 다르다.

Dispose(true) : Managed Resource 를 반환하고, 그 다음 Unmanaged Resource 를 반환한다.

Dispose(false) : Unmanaged Resource 를 반환한다.

Finalizer 에서 Dispose(false) 라고 하는 이유는... Unmanaged Resource 에 대한 반환을 수행하고자 함이다.


SafeHandle 을 이용하면 unmanaged resource 의 사용을 줄일 수 있다.


3. 자식 클래스에서... 새로이 등장한 iDisposable 객체가 없다면, 자식 클래스는 Dispose 패턴을 구현할 필요가 없다.


그냥 부모 클래스의 Dispose 패턴을 그대로 사용하면 된다.

using(var c1 = new DerivedClass())

이렇게 사용해도 무방하다.


출처 : https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/dispose-pattern


위 출처에서 가져온 아래 코드를 한 번 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DisposableResourceHolder : IDisposable {  
 
    private SafeHandle resource; // handle to a resource  
 
    public DisposableResourceHolder(){  
        this.resource = ... // allocates the resource  
    }  
 
    public void Dispose(){  
        Dispose(true);  
        GC.SuppressFinalize(this);  
    }  
 
    protected virtual void Dispose(bool disposing){  
        if (disposing){  
            if (resource!= null) resource.Dispose();  
        }  
    }  
}  
cs


SafeHandle 은 외부 자원을 참조하는 일종의 포인터인데...


대용량 고화질 그림 파일을 읽어와서 이것을 메모리에 넣어두고 포인터로 가리키고 있다고 하면...

SafeHandle 을 사용하지 않으면 이 자원은 unmanaged resource 이지만...

SafeHandle 을 이용하여 이 자원을 가리키면 그것은 managed resource 가 된다.

즉, 이 예제에서의 SafeHandle 은 managed resource 이다.


이제... 아래의 예제 코드를 보자...


출처 : https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose


>> IDisposable 인터페이스를 구현하는 클래스에 대한 DIspose 패턴 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
 
class BaseClass : IDisposable
{
   // Flag: Has Dispose already been called?
   bool disposed = false;
   
   // Public implementation of Dispose pattern callable by consumers.
   public void Dispose()
   { 
      Dispose(true);
      GC.SuppressFinalize(this);           
   }
   
   // Protected implementation of Dispose pattern.
   protected virtual void Dispose(bool disposing)
   {
      if (disposed)
         return
      
      if (disposing) {
         // Free any other managed objects here.
         //
      }
      
      // Free any unmanaged objects here.
      //
      disposed = true;
   }
 
   ~BaseClass()
   {
      Dispose(false);
   }
}
cs

* GC.SuppressFinalize(this) 구문에 주목하자. IDisposable 을 구현한 부모 클래스이므로 GC.SuppressFinalize(this) 가 필요하다.

>> BaseClass (IDisposable 을 구현함) 를 상속하는 DerivedClass 에 대한 Dispose 패턴 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
 
class DerivedClass : BaseClass
{
   // Flag: Has Dispose already been called?
   bool disposed = false;
   
   // Protected implementation of Dispose pattern.
   protected override void Dispose(bool disposing)
   {
      if (disposed)
         return
      
      if (disposing) {
         // Free any other managed objects here.
         //
      }
      
      // Free any unmanaged objects here.
      //
      disposed = true;
      
      // Call the base class implementation.
      base.Dispose(disposing);
   }
 
   ~DerivedClass()
   {
      Dispose(false);
   }
}
cs

* 위 클래스의 경우 클래스 멤버중에 unmanaged resources 가 없으면 Finalizer 는 없어도 된다. (~DerivedClass() 부분)
* base.Dispose(disposing) 구문이 dispose = true 다음에 실행된다는 사실에 주목하자.
* DerivedClass 에서는 GC.SuppressFinalize(this) 구문이 없다. 이것은 부모 클래스에서만 한 번 실행된다.

>> SafeHandle 을 사용한 Dispose 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;
 
public class DisposableStreamResource : IDisposable
{
   // Define constants.
   protected const uint GENERIC_READ = 0x80000000;
   protected const uint FILE_SHARE_READ = 0x00000001;
   protected const uint OPEN_EXISTING = 3;
   protected const uint FILE_ATTRIBUTE_NORMAL = 0x80;
   protected IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
   private const int INVALID_FILE_SIZE = unchecked((int0xFFFFFFFF);
   
   // Define Windows APIs.
   [DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode)]
   protected static extern IntPtr CreateFile (
                                  string lpFileName, uint dwDesiredAccess, 
                                  uint dwShareMode, IntPtr lpSecurityAttributes, 
                                  uint dwCreationDisposition, uint dwFlagsAndAttributes, 
                                  IntPtr hTemplateFile);
   
   [DllImport("kernel32.dll")]
   private static extern int GetFileSize(SafeFileHandle hFile, out int lpFileSizeHigh);
    
   // Define locals.
   private bool disposed = false;
   private SafeFileHandle safeHandle; 
   private long bufferSize;
   private int upperWord;
   
   public DisposableStreamResource(string filename)
   {
      if (filename == null)
         throw new ArgumentNullException("The filename cannot be null.");
      else if (filename == "")
         throw new ArgumentException("The filename cannot be an empty string.");
            
      IntPtr handle = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ,
                                 IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
                                 IntPtr.Zero);
      if (handle != INVALID_HANDLE_VALUE)
         safeHandle = new SafeFileHandle(handle, true);
      else
         throw new FileNotFoundException(String.Format("Cannot open '{0}'", filename));
      
      // Get file size.
      bufferSize = GetFileSize(safeHandle, out upperWord); 
      if (bufferSize == INVALID_FILE_SIZE)
         bufferSize = -1;
      else if (upperWord > 0
         bufferSize = (((long)upperWord) << 32+ bufferSize;
   }
   
   public long Size 
   { get { return bufferSize; } }
 
   public void Dispose()
   {
      Dispose(true);
      GC.SuppressFinalize(this);
   }           
 
   protected virtual void Dispose(bool disposing)
   {
      if (disposed) return;
 
      // Dispose of managed resources here.
      if (disposing)
         safeHandle.Dispose();
      
      // Dispose of any unmanaged resources not wrapped in safe handles.
      
      disposed = true;
   }  
}
cs

* 본 포스팅에서 설명한 바와 같이... Safe Handle 을 이용하면 unmanaged resource 를 managed resource 처럼 사용할 수 있다.