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

... 76  77  78  79  80  81  82  [83]  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11735정성태10/12/201819744VS.NET IDE: 129. Visual Studio - 특정 문자(열)를 개행 문자로 바꾸는 방법
11734정성태10/10/201814863Linux: 4. Synology NAS(DS216+II)에 FTDI 장치 연결 후 C#(.NET Core)으로 DTR 제어파일 다운로드1
11733정성태10/10/201817197Linux: 3. Synology NAS(DS216+II)에서 FTDI 장치를 C/C++로 제어
11732정성태10/10/201816892디버깅 기술: 119. windbg 분석 사례 - 종료자(Finalizer)에서 예외가 발생한 경우 비정상 종료(Crash) 발생파일 다운로드1
11731정성태10/9/201816656개발 환경 구성: 409. C# - REST API를 이용해 Azure Kudu 서비스 이용 - 웹 앱 확장 처리파일 다운로드1
11730정성태10/9/201815447개발 환경 구성: 408. C# - REST API를 이용해 Azure Kudu 서비스 이용 - 파일 처리파일 다운로드1
11729정성태10/9/201817588Windows: 150. 윈도우에서 ARP Cache 목록 확인 및 삭제하는 방법
11728정성태10/9/201816025사물인터넷: 50. Audio Jack 커넥터의 IR 적외선 송신기 [1]
11727정성태10/8/201816951오류 유형: 491. Visual Studio의 리눅스 SSH 원격 연결 - "Connectivity Failure. Please make sure host name and port number are correct."
11726정성태10/7/201819393사물인터넷: 49. 라즈베리 파이를 이용해 원격 컴퓨터의 전원 스위치 제어파일 다운로드1
11724정성태10/5/201818105개발 환경 구성: 407. 유니코드와 한글 - "Hangul Compatibility Jamo"파일 다운로드1
11723정성태10/4/201814156개발 환경 구성: 406. "Docker for Windows" 컨테이너 내의 .NET Core 응용 프로그램에서 직렬 포트(Serial Port, COM Port) 사용 방법
11722정성태10/4/201816730.NET Framework: 798. C# - Hyper-V 가상 머신의 직렬 포트와 연결된 Named Pipe 간의 통신파일 다운로드1
11721정성태10/4/201817217.NET Framework: 797. Linux 환경의 .NET Core 응용 프로그램에서 직렬 포트(Serial Port, COM Port) 사용 방법파일 다운로드1
11720정성태10/4/201818656개발 환경 구성: 405. Hyper-V 가상 머신에서 직렬 포트(Serial Port, COM Port) 사용
11719정성태10/4/201819572.NET Framework: 796. C# - 인증서를 윈도우에 설치하는 방법
11718정성태10/4/201814544개발 환경 구성: 404. (opkg가 설치된) Synology NAS(DS216+II)에 cmake 설치
11717정성태10/3/201817473사물인터넷: 48. 넷두이노의 C# 네트워크 프로그램 [1]
11716정성태10/3/201817748사물인터넷: 47. Raspberry PI Zero (W)에 FTDI 장치 연결 후 C/C++로 DTR 제어파일 다운로드1
11715정성태10/3/201816929사물인터넷: 46. Raspberry PI Zero (W)에 docker 설치
11714정성태10/2/201815976사물인터넷: 45. Raspberry PI에 ping을 hostname으로 하는 방법
11713정성태10/2/201818742개발 환경 구성: 403. Synology NAS(DS216+II)에 docker 설치 후 .NET Core 2.1 응용 프로그램 실행하는 방법
11712정성태10/2/201823635.NET Framework: 795. C# - 폰트 목록을 한글이 아닌 영문으로 구하는 방법 [3]
11711정성태10/2/201819192오류 유형: 490. 윈도우 라이선스 키 입력 오류 0xc004f050, 0xc004e028
11710정성태10/2/201817628.NET Framework: 794. C# - 같은 모양, 다른 값의 한글 자음을 비교하는 호환 분해 [5]
11709정성태9/30/201815757개발 환경 구성: 402. .NET Core 콘솔 응용 프로그램을 docker로 실행/디버깅하는 방법 [1]
... 76  77  78  79  80  81  82  [83]  84  85  86  87  88  89  90  ...