C# delegate의 내부 구조에 대하여 II (MultiCast)

[제목] C# delegate의 내부 구조에 대하여 II (MultiCast)

지난 SingleCast C# delegate 내부구조 아티클에 이어 이번에는 복수개의 델리게이트를 가진 MultiCast C# delegate의 내부 구조를 살펴보자.

(주: C# delegate의 내부 구조에 대하여 I (SingleCast) 을 읽지 않은 분들은 먼저 그 아티클을 읽기 바랍니다.)

.NET의 Delegate 클래스는 Combine()이라는 메서드를 제공하는데, 이 메서드는 2개의 Delegate 객체를 하나로 합치는 기능을 제공한다. C#에서 이와 비슷한 기능으로 좀더 편리한 += 연산자를 사용할 수 있는데, 이 연산자를 기존 Delegate 객체에 계속 += 연산자 이후의 Delegate 객체를 추가하게 된다. 다음 예제는 3개의 메서드를 m 이라는 Delegate 객체에 계속 추가하는 코드를 보여주고 있다.

class Program
{
    public delegate void MyDelegate();

    static void Main(string[] args)
    {
	    // Class1은 [C# delegate 내부구조 1] 참조
        Class1 c = new Class1();

        MyDelegate m = null;
		
		// 1개의 메서드 레퍼런스 
        m += c.InstRun;
		
		// 2개의 메서드 레퍼런스 
        m += Class1.StaticRun;
		
		// 3개의 메서드 레퍼런스 
        m += () => Console.WriteLine("*");

        Console.ReadLine(); 
		
		// 3개를 순서대로 호출
        m();   

		// GetInvocationList()을 사용하여
		// Delegate를 하나씩 처리할 수도 있다.
        foreach (Delegate d in m.GetInvocationList())
        {
            Console.WriteLine(d.Method.Name);
        }		
    }
}

위의 예에서 첫번째 메서드는 Class1의 Instance 메서드이고, 이때는 SingleCast Delegate로서 이전 아티클에서 보았듯이 _target과 _methodPtr 필드를 사용한다. 이어 두번째 Class1.StaticRun 메서드가 추가되면, 이는 MultiCast Delegate가 되면서, 마지막 2개의 필드를 사용한다. 즉, 6번째 필드인 _invocationCount이 2가되고, 5번째 필드인 _invocationList 필드에 컬렉션으로서 2개의 델리게이트 객체를 갖는다.

위의 코드에서 3개가 모두 추가된 후, MultiCast 델리게이트 객체가 된 후에 Console.Readline()이 호출된다. Windbg 디버거를 통해 Console.ReadLine() 에서 중지하고 !clrstack 을 실행하면 맨 마지막에 아래와 같은 Main() 메서드의 로컬 변수들을 볼 수 있다.

0:000> !clrstack -a
...생략...
0036efec 001e01e5 *** WARNING: Unable to verify checksum for DeleApp.exe
DeleApp.Program.Main(System.String[]) [c:\Dev\DeleApp\Program.cs @ 21]
    PARAMETERS:
        args (0x0036f01c) = 0x02772250
    LOCALS:
        0x0036f018 = 0x02772260
        0x0036f014 = 0x02772324

두번재 로컬변수 0x02772324 을 살펴보면, 이것이 MyDelegate 객체임을 알 수 있고, 이 멀티캐스트 델리게이트 객체가 3개 (_invocationCount)의 메서드 레퍼런스를 가지고 있음을 알 수 있다.

0:000> !do /d 0x02772324
Name:        DeleApp.Program+MyDelegate
MethodTable: 00143904
EEClass:     00141368
Size:        32(0x20) bytes
File:        C:\Dev\DeleApp\bin\Debug\DeleApp.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
5d44b064  400002d        4        System.Object  0 instance 02772324 _target
5d44b064  400002e        8        System.Object  0 instance 00000000 _methodBase
5d44a0e8  400002f        c        System.IntPtr  1 instance   13a01c _methodPtr
5d44a0e8  4000030       10        System.IntPtr  1 instance   1438c0 _methodPtrAux
5d44b064  4000031       14        System.Object  0 instance 02772304 _invocationList  <== 컬렉션
5d44a0e8  4000032       18        System.IntPtr  1 instance        3 _invocationCount  <== 갯수

_invocationList 리스트를 출력해 보기 위해 아래와 같이 SOS의 !DumpArray를 사용할 수 있다. 출력 결과의 마지막에 보면 [0][1][2]의 3개의 요소가 NULL이 아닌 값들이 들어 있는 것을 볼 수 있다.

0:000> !DumpArray /d 02772304
Name:        System.Object[]
MethodTable: 5d3fab9c
EEClass:     5d0bbb80
Size:        32(0x20) bytes
Array:       Rank 1, Number of elements 4, Type CLASS
Element Methodtable: 5d44b064
[0] 0277226c
[1] 0277228c
[2] 027722e4
[3] null

이 3개의 요소중 첫번째 요소를 !do로 검사해 보면, 이는 MyDelegate 타입의 (SingleCast) 객체이며, _methodPtrAux가 0인 것으로 보아 Instance 메서드이고, 따라서 _target과 _methodPtr 값으로 인스턴스 메서드를 찾아낼 수 있다.

0:000> !DumpObj /d 0277226c
Name:        DeleApp.Program+MyDelegate
MethodTable: 00143904
EEClass:     00141368
Size:        32(0x20) bytes
File:        C:\Dev\DeleApp\bin\Debug\DeleApp.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
5d44b064  400002d        4        System.Object  0 instance 02772260 _target   <== Class1 객체
5d44b064  400002e        8        System.Object  0 instance 00000000 _methodBase
5d44a0e8  400002f        c        System.IntPtr  1 instance   14c078 _methodPtr  <== 메서드
5d44a0e8  4000030       10        System.IntPtr  1 instance        0 _methodPtrAux
5d44b064  4000031       14        System.Object  0 instance 00000000 _invocationList
5d44a0e8  4000032       18        System.IntPtr  1 instance        0 _invocationCount

0:000> !dumpmd poi(14c078+8)
Method Name:  DeleApp.Class1.InstRun()
Class:        00141314
MethodTable:  0014385c
mdToken:      06000008
Module:       00142e94
IsJitted:     no
CodeAddr:     ffffffff
Transparency: Critical

이러한 방식으로 2번째 요소가 Static 메서드를 가리킨다는 것을 알 수 있다. 그런데, 위 예제에서 3번째 메서드는 람다식을 사용했는데, 이는 어떻게 될까? 이를 동일한 방식으로 검사해 보면, <Main>b__0() 와 같이 컴파일러가 생성한 메서드명을 가리키고 있음을 알 수 있다.

0:000> !DumpObj /d 027722e4
Name:        DeleApp.Program+MyDelegate
MethodTable: 00143904
EEClass:     00141368
Size:        32(0x20) bytes
File:        C:\Dev\DeleApp\bin\Debug\DeleApp.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
5d44b064  400002d        4        System.Object  0 instance 027722e4 _target
5d44b064  400002e        8        System.Object  0 instance 00000000 _methodBase
5d44a0e8  400002f        c        System.IntPtr  1 instance   8907ec _methodPtr
5d44a0e8  4000030       10        System.IntPtr  1 instance   14c098 _methodPtrAux  <== 람다식
5d44b064  4000031       14        System.Object  0 instance 00000000 _invocationList
5d44a0e8  4000032       18        System.IntPtr  1 instance        0 _invocationCount

0:000> !dumpmd /d  poi(14c098+8)
Method Name:  DeleApp.Program.
b__0() <== 생성된 메서드 Class: 001412c0 MethodTable: 001437e8 mdToken: 06000003 Module: 00142e94 IsJitted: no CodeAddr: ffffffff Transparency: Critical

C# delegate는 해당 delegate가 SingleCast이건 MultiCast이건 호출하는 하는 방식을 동일하다. 즉, 위의 예제에서 처럼 m(); 과 같이 SingleCast 델리게이트 호출과 동일하다. 단, MultiCast 델리게이트인 경우 즉 _invocationlist가 2이상인 경우 InvocationList에서 요소를 하나씩 가져와 차례로 모두 호출한다는 차이점이 있다. 이때 만약, 한 메서드에서 오류가 발생하면 어떻게 될까? 만약 100개의 메서드가 있는데, 이 중 2번째에서 Exception이 발생하면, 나머지 98개는 실행되지 않는다. 이를 방지하려면, Delegate의 GetInvocationList()를 호출해서 손수 하나씩 처리하고, Exception이 발생하면 try.. catch로 잡아 Exception을 무시하는 것도 방법이 될 수 있다.



본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.