Microsoft MVP성태의 닷넷 이야기
.NET Framework: 718. AsyncTaskMethodBuilder.Create() 메서드 동작 방식 [링크 복사], [링크+제목 복사],
조회: 19420
글쓴 사람
정성태 (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일 수 없습니다.
정성태

... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...
NoWriterDateCnt.TitleFile(s)
11300정성태9/10/201721133.NET Framework: 681. dotnet.exe - run, exec, build, restore, publish 차이점 [3]
11299정성태9/9/201719788개발 환경 구성: 330. Hyper-V VM의 Internal Network를 Private 유형으로 만드는 방법
11298정성태9/8/201723130VC++: 119. EnumProcesses / EnumProcessModules API 사용 시 주의점 [1]
11297정성태9/8/201719785디버깅 기술: 96. windbg - 풀 덤프에 포함된 모든 닷넷 모듈을 파일로 저장하는 방법
11296정성태9/8/201722925웹: 36. Edge - "이 웹 사이트는 이전 기술에서 실행되며 Internet Explorer에서만 작동합니다." 끄는 방법
11295정성태9/7/201720413디버깅 기술: 95. Windbg - .foreach 사용법
11294정성태9/4/201720075개발 환경 구성: 329. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 [1]
11293정성태9/4/201720665개발 환경 구성: 328. Visual Studio(devenv.exe)를 배치 파일(.bat)을 통해 실행하는 방법
11292정성태9/4/201718880오류 유형: 419. Cannot connect to WMI provider - Invalid class [0x80041010]
11291정성태9/3/201720708개발 환경 구성: 327. 아파치 서버 2.4를 위한 mod_aspdotnet 마이그레이션
11290정성태9/3/201723954개발 환경 구성: 326. 아파치 서버에서 ASP.NET을 실행하는 mod_aspdotnet 모듈 [2]
11289정성태9/3/201721619개발 환경 구성: 325. GAC에 어셈블리 등록을 위해 gacutil.exe을 사용하는 경우 주의 사항
11288정성태9/3/201718391개발 환경 구성: 324. 윈도우용 XAMPP의 아파치 서버 구성 방법
11287정성태9/1/201727589.NET Framework: 680. C# - 작업자(Worker) 스레드와 UI 스레드 [11]
11286정성태8/28/201714961기타: 67. App Privacy Policy
11285정성태8/28/201723506.NET Framework: 679. C# - 개인 키 보안의 SFTP를 이용한 파일 업로드파일 다운로드1
11284정성태8/27/201721530.NET Framework: 678. 데스크톱 윈도우 응용 프로그램에서 UWP 라이브러리를 이용한 비디오 장치 열람하는 방법 [1]파일 다운로드1
11283정성태8/27/201717306오류 유형: 418. CSS3117: @font-face failed cross-origin request. Resource access is restricted.
11282정성태8/26/201719747Math: 22. 행렬로 바라보는 피보나치 수열
11281정성태8/26/201721562.NET Framework: 677. Visual Studio 2017 - NuGet 패키지를 직접 참조하는 PackageReference 지원 [2]
11280정성태8/24/201718578디버깅 기술: 94. windbg - 풀 덤프에 포함된 모든 모듈을 파일로 저장하는 방법
11279정성태8/23/201730179.NET Framework: 676. C# Thread가 Running 상태인지 아는 방법
11278정성태8/23/201718376오류 유형: 417. TFS - Warning - Unable to refresh ... because you have a pending edit. [1]
11277정성태8/23/201719628오류 유형: 416. msbuild - error MSB4062: The "TransformXml" task could not be loaded from the assembly
11276정성태8/23/201723917.NET Framework: 675. C# - (파일) 확장자와 연결된 실행 파일 경로 찾기 [2]파일 다운로드1
11275정성태8/23/201732923개발 환경 구성: 323. Visual Studio 설치 없이 빌드 환경 구성 - Visual Studio 2017용 Build Tools [1]
... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...