Microsoft MVP성태의 닷넷 이야기
.NET Framework: 718. AsyncTaskMethodBuilder.Create() 메서드 동작 방식 [링크 복사], [링크+제목 복사],
조회: 12430
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)

AsyncTaskMethodBuilder.Create() 메서드 동작 방식

일반 메서드에 async 예약어가 붙게 되면 다음과 같은 상태 머신 객체로 메서드의 구현이 바뀝니다.

[AsyncStateMachine(typeof(TaskDelay_StateMachine)), DebuggerStepThrough]
private Task TaskDelay()
{
    TaskDelay_StateMachine stateMachine = new TaskDelay_StateMachine {
        _this = this,
        _builder = AsyncTaskMethodBuilder.Create(),
        _state = -1
    };
    stateMachine._builder.Start<TaskDelay_StateMachine>(ref stateMachine);
    return stateMachine._builder.Task;
}

그중에서 AsyncTaskMethodBuilder.Create 메서드가 어떤 일을 하는지 궁금했습니다. Reflector 도구로 보면 다음과 같이 단순히 기본 생성자를 호출하는 것에 불과합니다.

[__DynamicallyInvokable]
public static AsyncTaskMethodBuilder Create()
{
    return new AsyncTaskMethodBuilder();
}

일단, public static 메서드이기 때문에 외부에서도 마음 편하게 직접 실행해 볼 수 있습니다.

AsyncTaskMethodBuilder asyncBuilder = AsyncTaskMethodBuilder.Create();

그리고 asyncBuilder 인스턴스를 Visual Studio의 디버거 watch 창으로 보면 다음과 같은 출력을 볼 수 있습니다.

asyncBuilder
+       Task    Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"
+       ObjectIdForDebugger Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"
-       m_builder   {System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>}
+		    Task	Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"
+		    m_coreState    {System.Runtime.CompilerServices.AsyncMethodBuilderCore}


위의 출력에서 기본 생성자만 호출했음에도 불구하고 AsyncTaskMethodBuilder 객체의 내부 변수 m_builder는 값이 채워져 있다는 점입니다. 이게 어떻게 가능할까요? ^^

왜냐하면 m_builder 필드의 타입인 AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>가 struct 타입이기 때문입니다. 마찬가지로 m_coreState의 객체도 null이 아닐 수 있는 것이 AsyncMethodBuilderCore 타입이 struct이기 때문입니다.

따라서, AsyncTaskMethodBuilder.Create() 메서드 호출 하나로 생성되는 객체는 AsyncTaskMethodBuilder, AsyncTaskMethodBuilder<VoidTaskResult>, AsyncMethodBuilderCore가 됩니다.

그런데, 여기서 asyncBuilder의 Task 속성도 값을 보여주고 있습니다.

Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"

왜냐하면, Task 속성은 다음과 같이 값이 없으면 기본 Task 객체를 생성해서 반환하기 때문입니다.

[__DynamicallyInvokable]
public Task<TResult> Task
{
    [__DynamicallyInvokable]
    get
    {
        Task<TResult> task = this.m_task;
        if (task == null)
        {
            this.m_task = task = new Task<TResult>();
        }
        return task;
    }
}

그런데, 이렇게 생성된 m_task의 디버거 출력값이 왜 Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"로 되는 걸까요?

m_task의 타입인 Task<TResult>는 ToString에 대한 재정의도 없습니다. 대신 디버거가 값을 출력할 수 있도록 다음과 같은 특성을 달고 있습니다.

[...[생략]..., DebuggerDisplay("Id = {Id}, Status = {Status}, Method = {DebuggerDisplayMethodDescription}, Result = {DebuggerDisplayResultDescription}"), ...[생략]...)]
public class Task : Task
{
    // ...[생략]...
}

즉, Id, Status, Method, DebuggerDisplayMethodDescription, DebuggerDisplayResultDescription 값을 보여주게 됩니다.

Id 속성도 접근할 때 없으면 생성하는 방식이라,

[__DynamicallyInvokable]
public int get_Id()
{
    if (this.m_taskId == 0)
    {
        int num = NewId();
        Interlocked.CompareExchange(ref this.m_taskId, num, 0);
    }
    return this.m_taskId;
}

internal static int NewId()
{
    int taskID = 0;
    do
    {
        taskID = Interlocked.Increment(ref s_taskIdCounter);
    }
    while (taskID == 0);
    TplEtwProvider.Log.NewID(taskID);
    return taskID;
}

프로그램 시작 후 최초 0이었던 s_taskIdCounter 값이 1씩 증가하는 값을 반환합니다.

Status는 m_stateFlags의 값에 따라 TaskStatus 열 값을 반환합니다.

[__DynamicallyInvokable]
public TaskStatus get_Status()
{
    int stateFlags = this.m_stateFlags;
    if ((stateFlags & 0x200000) != 0)
    {
        return TaskStatus.Faulted;
    }
    if ((stateFlags & 0x400000) != 0)
    {
        return TaskStatus.Canceled;
    }
    if ((stateFlags & 0x1000000) != 0)
    {
        return TaskStatus.RanToCompletion;
    }
    if ((stateFlags & 0x800000) != 0)
    {
        return TaskStatus.WaitingForChildrenToComplete;
    }
    if ((stateFlags & 0x20000) != 0)
    {
        return TaskStatus.Running;
    }
    if ((stateFlags & 0x10000) != 0)
    {
        return TaskStatus.WaitingToRun;
    }
    if ((stateFlags & 0x2000000) != 0)
    {
        return TaskStatus.WaitingForActivation;
    }
    return TaskStatus.Created;
}

디버거에 보이는 Status 값이 WaitingForActivation이므로 m_stateFlags 값에 0x2000000로 설정되어 있음을 추측하게 됩니다. 이 값은 Task 생성자에서 초기화한 값입니다.

// System.Threading.Tasks.Task
internal Task()
{
    this.m_stateFlags = 0x2000400;
}

다음으로 DebuggerDisplayMethodDescription은,

private string DebuggerDisplayMethodDescription
{
    get
    {
        Delegate action = (Delegate) this.m_action;
        if (action == null)
        {
            return "{null}";
        }
        return action.Method.ToString();
    }
}

기본 생성자를 거친 객체이므로 m_action이 null이라 "{null}"로 출력된 것이 맞습니다. 마지막으로 Result는 DebuggerDisplayResultDescription 속성과 연결되어 있는데,

private string DebuggerDisplayResultDescription
{
    get
    {
        if (!base.IsRanToCompletion)
        {
            return Environment.GetResourceString("TaskT_DebuggerNoResult");
        }
        return (this.m_result);
    }
}

internal bool IsRanToCompletion
{
    get
    {
        return ((this.m_stateFlags & 0x1600000) == 0x1000000);
    }
}

IsRanToCompletion == false이므로 Environment.GetResourceString("TaskT_DebuggerNoResult"); 코드를 실행하게 되고, 이는 mscorlib.dll의 mscorlib.resources 리소스에 담긴 "TaskT_DebuggerNoResult" Name의 값이 "{Not yet computed}"이기 때문에 출력된 것입니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/22/2017]

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

비밀번호

댓글 작성자
 



2023-09-11 09시50분
"m_builder, m_coreState는 struct 타입이기 때문에 null이 아닙니다."라는 문장이 확인하기 위해서
(선생님을 의심하는 것이 아닙니다 ^^...)아래 간단한 예제를 구현했습니다.
// Location.cs
public class Location
{
    public int x;
    public int y;
}

// Person.cs
public class Person
{
    public string name;
    public int age;
    public Location location;    
}

// Program.cs
static void Main(string[] args)
{
    // Location이 class인 경우 Person 객체를 생성하면 location = null이고
    // Location이 struct인 경우 location = {ConsoleApp1.Location}가 된다.
    Person mijung = new Person();
}
궁금한 점이 Location이 class라면 null이고
struct라면 null이 아니라 객체를 생성해서 할당하도록 C# 언어를 설계한 이유가 있을까요?
선생님 교재 219 ~ 226쪽을 참고해서 추론하면
구조체는 스택에 할당되고 클래스는 힙에 할당되는데
힙은 가비지 컬렉션에 의해 관리되기 때문에
사용하지 않는 객체는 수거되므로
미리 생성하는 것이 의미가 없다고 생각해서 class는 null이라고 생각하는데 너무 단편적으로 생각했을까요?
한예지
2023-09-11 10시46분
해당 질문은 결국 "참조 형식" 이외에 왜 "값 형식"을 만들었느냐가 됩니다.

사실, C# 언어를 그렇게 안 만들어도 됐을 것입니다. 실제로 Java의 경우에는 언어 차원에서 "값 형식"을 만들 수 있는 구문을 제공하지 않습니다. 굳이 값 형식을 만든 주된 이유는, 가능한 "GC"의 부담을 줄이는데 있습니다. 스택을 쓸 수 있는 수단이 생긴 것이므로 상황에 따라 GC의 구동을 지연시키는 것이 가능할 수 있습니다.

참고로, 말씀하신 예제에서는 "class Person" 내에 "struct Location"이 있다면, 그런 경우 Location 정보는 스택에 할당되지 않고 Person의 다른 필드와 함께 보관이 됩니다. 이에 대해서는 다음의 글을 참고하세요.

C# - struct/class가 스택/힙에 할당되는 사례 정리
; https://www.sysnet.pe.kr/2/0/12624

------------------------------

그리고 할 수만 있었다면 값 형식도 참조 형식처럼 null 상태를 갖고 싶었을 것입니다. 하지만, 스택은 함수의 실행 순간에 사용할 공간이 정해져야 하므로 null 상태를 갖기보다는 해당 타입의 크기만큼 미리 점유해 놓을 수밖에 없고, 이로 인해 자연스럽게 null일 수 없습니다.
정성태

... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...
NoWriterDateCnt.TitleFile(s)
12128정성태1/26/202010094오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
12127정성태1/25/20209956.NET Framework: 881. C# DLL에서 제공하는 Win32 export 함수의 내부 동작 방식(VT Fix up Table)파일 다운로드1
12126정성태1/25/202010739.NET Framework: 880. C# - PE 파일로부터 IMAGE_COR20_HEADER 및 VTableFixups 테이블 분석파일 다운로드1
12125정성태1/24/20208622VS.NET IDE: 141. IDE0019 - Use pattern matching
12124정성태1/23/202010436VS.NET IDE: 140. IDE1006 - Naming rule violation: These words must begin with upper case characters: ...
12123정성태1/23/202011927웹: 39. Google Analytics - gtag 함수를 이용해 페이지 URL 수정 및 별도의 이벤트 생성 방법 [2]
12122정성태1/20/20208908.NET Framework: 879. C/C++의 UNREFERENCED_PARAMETER 매크로를 C#에서 우회하는 방법(IDE0060 - Remove unused parameter '...')파일 다운로드1
12121정성태1/20/20209437VS.NET IDE: 139. Visual Studio - Error List: "Could not find schema information for the ..."파일 다운로드1
12120정성태1/19/202010913.NET Framework: 878. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)파일 다운로드1
12119정성태1/17/202010950디버깅 기술: 160. Windbg 확장 DLL 만들기 (3) - C#으로 만드는 방법
12118정성태1/17/202011571개발 환경 구성: 466. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기 [1]
12117정성태1/15/202010588디버깅 기술: 159. C# - 디버깅 중인 프로세스를 강제로 다른 디버거에서 연결하는 방법파일 다운로드1
12116정성태1/15/202011054디버깅 기술: 158. Visual Studio로 디버깅 시 sos.dll 확장 명령어를 (비롯한 windbg의 다양한 기능을) 수행하는 방법
12115정성태1/14/202010849디버깅 기술: 157. C# - PEB.ProcessHeap을 이용해 디버깅 중인지 확인하는 방법파일 다운로드1
12114정성태1/13/202012696디버깅 기술: 156. C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거 [1]파일 다운로드3
12113정성태1/12/202013315오류 유형: 590. Visual C++ 빌드 오류 - fatal error LNK1104: cannot open file 'atls.lib' [1]
12112정성태1/12/20209919오류 유형: 589. PowerShell - 원격 Invoke-Command 실행 시 "WinRM cannot complete the operation" 오류 발생
12111정성태1/12/202013150디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [16]파일 다운로드1
12110정성태1/11/202011783디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례 [5]파일 다운로드1
12109정성태1/10/20209709오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/20209712오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/202010675.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/202012094VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/202010723디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/7/202011443DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [4]
12103정성태1/7/202014120DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제 [1]파일 다운로드2
... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...