성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>Task를 포함하지 않는 async 메서드의 동작 방식</h1> <p> 다음의 글을 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# 컴파일러 대신 직접 구현하는 비동기(async/await) 코드 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11351'>http://www.sysnet.pe.kr/2/0/11351</a> </pre> <br /> 다음의 TaskMethod 메서드에 대해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static void Main(string[] args) { Program pg = new Program(); pg.TaskMethod(); } async Task TaskMethod() { Console.WriteLine("TaskMethod"); } </pre> <br /> C#은 이런 StateMachine 클래스를 생성합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private Task TaskMethod() { CallAsync_StateMachine stateMachine = new CallAsync_StateMachine { _this = this, _builder = AsyncTaskMethodBuilder.Create(), _state = -1, }; stateMachine._builder.Start(ref stateMachine); return <span style='color: blue; font-weight: bold'>stateMachine._builder.Task</span>; } class CallAsync_StateMachine : IAsyncStateMachine { public int _state; // 1 public AsyncTaskMethodBuilder _builder; public Program _this; // 4 void IAsyncStateMachine.MoveNext() { int num = this._state; try { Console.WriteLien("VoidMethod"); } catch (Exception e) { this._state = -2; this._builder.SetException(e); return; } this._state = -2; this._builder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } } </pre> <br /> 보는 바와 같이 Task를 반환합니다. 그런데 TaskMethod 자체에는 아무런 Task를 생성한 것이 없습니다. 그럼 도대체 어떤 Task를 반환하는 걸까요? 지난 글에서 이에 대해 설명했습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > AsyncTaskMethodBuilder.Create() 메서드 동작 방식 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11416'>http://www.sysnet.pe.kr/2/0/11416</a> </pre> <br /> 즉 다음의 Task 인스턴스가 Create() 시점부터 상태 머신의 m_builder.Task가 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}" </pre> <br /> 그리고 저 Task 객체는 async 메서드의 상태 머신에 끝까지 남아 있게 됩니다. 단지, 달라지는 것은 Task의 Status와 (반환 값이 있다면) Result입니다. 달라지는 시점은 _builder.SetException, _builder.SetResult 메서드의 호출인데, 예외가 발생하지 않은 걸로 가정하고 _builder.SetResult() 호출을 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [__DynamicallyInvokable] public void SetResult() { this.m_builder.SetResult(s_cachedCompleted); } internal void SetResult(Task<TResult> completedTask) { if (this.m_task == null) { this.m_task = completedTask; } else { // 반환값이 없는 async 메서드의 경우 TResult 타입은 System.Threading.Tasks.VoidTaskResult <span style='color: blue; font-weight: bold'>this.SetResult(default(TResult));</span> } } </pre> <br /> this.m_task가 AsyncTaskMethodBuilder.Create()에 의해 할당된 상태이므로 단순히 (반환값이 없는 경우) VoidTaskResult의 기본값으로 호출됩니다. (VoidTaskResult는 구현이 없는 빈 struct 타입입니다.) 참고로, 위의 코드에서 s_cachedCompleted는 정적 필드로 AsyncTaskMethodBuilder 타입의 static 생성자에서 정의하고 있으며,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // System.Runtime.CompilerServices.AsyncTaskMethodBuilder static AsyncTaskMethodBuilder() { <span style='color: blue; font-weight: bold'>s_cachedCompleted</span> = AsyncTaskMethodBuilder<VoidTaskResult>.<span style='color: blue; font-weight: bold'>s_defaultResultTask</span>; } // System.Runtime.CompilerServices.AsyncTaskMethodBuilder<TResult> static AsyncTaskMethodBuilder() { AsyncTaskMethodBuilder<TResult>.<span style='color: blue; font-weight: bold'>s_defaultResultTask</span> = AsyncTaskCache.<span style='color: blue; font-weight: bold'>CreateCacheableTask<TResult>(default(TResult))</span>; } // System.Runtime.CompilerServices.AsyncTaskCache internal static Task<TResult> CreateCacheableTask<TResult>(TResult result) // result == default(TResult) == default(VoidTaskResult) == null { return <span style='color: blue; font-weight: bold'>new Task<TResult>(false, result, 0x4000, new CancellationToken())</span>; } // System.Threading.Tasks.Task<TResult> internal Task(bool canceled, TResult result, TaskCreationOptions creationOptions, CancellationToken ct) : <span style='color: blue; font-weight: bold'>base(canceled, creationOptions, ct)</span> { if (!canceled) // canceled == false { this.m_result = result; // result == null } } // System.Threading.Tasks.Task internal Task(bool canceled, TaskCreationOptions creationOptions, CancellationToken ct) { int num = (int) creationOptions; // creationOptions = 0x4000 if (canceled) // canceled == false { ContingentProperties properties; this.m_stateFlags = 0x500000 | num; this.m_contingentProperties = properties = new ContingentProperties(); properties.m_cancellationToken = ct; properties.m_internalCancellationRequested = 1; } else { <span style='color: blue; font-weight: bold'>this.m_stateFlags = 0x1000000 | num</span>; // m_stateFlags = 0x1000000 | 0x4000 = 0x1004000 } } </pre> <br /> m_stateFlags 상태 값이 0x1004000로 초기화된 Task입니다. 부가적으로 Task 타입의 IsCompleted 속성은 작업 완료를 다음과 같이 알아냅니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [__DynamicallyInvokable] public bool get_IsCompleted() { return IsCompletedMethod(this.m_stateFlags); } private static bool IsCompletedMethod(int flags) { return (<span style='color: blue; font-weight: bold'>(flags & 0x1600000) > 0</span>); // 0x1004000 & 0x1600000 > 0 == true } </pre> <br /> 결국 s_cachedCompleted라는 Task는 "취소되지 않았으면서 이미 종료된 Task" 객체를 의미합니다.<br /> <br /> 다시 본래의 이야기로 돌아와서 SetResult는 다음의 메서드로 연결됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [__DynamicallyInvokable] public void SetResult(TResult result) { Task<TResult> task = this.m_task; if (task == null) { this.m_task = this.GetTaskForResult(result); } else { if (AsyncCausalityTracer.LoggingOn) { AsyncCausalityTracer.TraceOperationCompletion(CausalityTraceLevel.Required, task.Id, AsyncCausalityStatus.Completed); } if (Task.s_asyncDebuggingEnabled) { Task.RemoveFromActiveTasks(task.Id); } if (<span style='color: blue; font-weight: bold'>!task.TrySetResult(result)</span>) { throw new InvalidOperationException(Environment.GetResourceString("TaskT_TransitionToFinal_AlreadyCompleted")); } } } </pre> <br /> 나머지는 디버깅이나 로깅을 위한 코드이므로 SetResult 메서드가 실제적으로 실행하는 코드는 task.TrySetResult입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal bool TrySetResult(TResult result) { if (base.IsCompleted) // 처음 TrySetResult 시에는 IsCompletedMethod(m_stateFlags) == false // (flags & 0x1600000) > 0 == false { return false; } if (!base.AtomicStateUpdate(0x4000000, 0x5600000)) { return false; } <span style='color: blue; font-weight: bold'>this.m_result = result;</span> <span style='color: blue; font-weight: bold'>Interlocked.Exchange(ref this.m_stateFlags, base.m_stateFlags | 0x1000000);</span> Task.ContingentProperties contingentProperties = base.m_contingentProperties; if (contingentProperties != null) { contingentProperties.SetCompleted(); } base.FinishStageThree(); return true; } </pre> <br /> 그렇습니다. 바로 저 라인에서 base.m_stateFlags | 0x1000000 값이 설정되면서, 그리고 m_result가 할당되면서 AsyncTaskMethodBuilder.Create()로 생성되었던 Task는 상태가 이렇게 바뀝니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Id = 1, Status = RanToCompletion, Method = "{null}", Result = "System.Threading.Tasks.VoidTaskResult" </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그럼 마지막으로 예외가 발생했을 때 호출하는 this._builder.SetException(exception)을 추적해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [__DynamicallyInvokable] public void SetException(Exception exception) { this.m_builder.SetException(exception); } [__DynamicallyInvokable] public void SetException(Exception exception) { if (exception == null) { throw new ArgumentNullException("exception"); } Task<TResult> task = this.m_task; if (task == null) { task = this.Task; } OperationCanceledException cancellationException = exception as OperationCanceledException; if (!((cancellationException != null) ? <span style='color: blue; font-weight: bold'>task.TrySetCanceled</span>(cancellationException.CancellationToken, cancellationException) : <span style='color: blue; font-weight: bold'>task.TrySetException</span>(exception))) { throw new InvalidOperationException(Environment.GetResourceString("TaskT_TransitionToFinal_AlreadyCompleted")); } } </pre> <br /> 위의 코드에서 당연히 m_task는 AsyncTaskMethodBuilder.Create()로 생성되었던 그 Task입니다. 만약 CancellationToken의 취소 동작으로 인한 것이라면 TrySetCanceled을 호출하고, 그 외에 코드 예외라면 TrySetException을 호출합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal bool TrySetException(object exceptionObject) { bool flag = false; base.EnsureContingentPropertiesInitialized(true); if (<span style='color: blue; font-weight: bold'>base.AtomicStateUpdate(0x4000000, 0x5600000)</span>) { base.AddException(exceptionObject); <span style='color: blue; font-weight: bold'>base.Finish(false);</span> flag = true; } return flag; } </pre> <br /> AtomicStateUpdate로 인해 기존 this.m_stateFlags == 0x2000400 값에 0x4000000 값이 OR 연산으로 합쳐져 this.m_stateFlags == 0x6000400 값이 되지만 역시 IsCompleted는 false입니다. 하지만, 그 이후 base.Finish 메서드를 거치면서 결국에는 IsCompleted == true인 상태로 진행합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그렇다면 결국 우리가 작성하게 되는 다음의 코드는 어떤 처리를 하게 되는 걸까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private <span style='color: blue; font-weight: bold'>async</span> void CallAsync() { <span style='color: blue; font-weight: bold'>await TaskMethod();</span> Console.WriteLine("CallAsync"); } public async Task TaskMethod() { Console.WriteLine("TaskMethod"); } </pre> <br /> CallAsync 내에서의 await TaskMethod()는 CallAsync 자체가 async 메서드이기 때문에 생성되는 상태 머신 클래스의 MoveNext에서 다음과 같이 처리됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > void IAsyncStateMachine.MoveNext() { TaskAwaiter awaiter; int num = this._state; try { if (num != 0) { awaiter = <span style='color: blue; font-weight: bold'>_this.TaskMethod()</span>.GetAwaiter(); <span style='color: blue; font-weight: bold'>// TaskMethod가 반환한 Task 및 그와 연관된 TaskAwaiter는 항상 SetResult가 호출된 상태이므로 IsCompleted는 언제나 true를 반환</span> <span style='color: blue; font-weight: bold'>if (!awaiter.IsCompleted)</span> { // 따라서 다음의 코드는 절대로 실행되지 않고, this._state = num = 0; this._awaiter = awaiter; CallAsync_StateMachine stateMachine = this; this._builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } } else { awaiter = this._awaiter; this._awaiter = new TaskAwaiter(); this._state = -1; } <span style='color: blue; font-weight: bold'>// 이곳의 코드가 실행됨.</span> awaiter.GetResult(); Console.WriteLine("CallAsync"); } catch (Exception e) { this._state = -2; this._builder.SetException(e); } this._state = -2; <span style='color: blue; font-weight: bold'>this._builder.SetResult(); // 그리고 이어서 이 코드도 실행됨</span> } </pre> <br /> 여기서 재미있는 것은 _this.TaskMethod()가 반환하는 Task는 async 메서드인 TaskMethod 내에 생성된 상태 머신의 AsyncTaskMethodBuilder.Create()로 생성된 객체라는 점입니다.<br /> <br /> 결국, 모든 async 메서드들이 await [TargetMethod](); 호출 시마다 그것의 [TargetMethod]내에 생성했던 상태 머신 스스로 생성한 Task로 연결되는 것에 불과합니다. 게다가 그것은 이 글에서도 살펴봤지만 SetResult(또는 SetException) 호출로 인해 이미 종료된 Task입니다. 따라서 그 코드들 어떤 것에도 별도의 스레드는 관여하지 않고 단일 스레드로 모두 처리합니다. 즉 다음과 같이 코드가 작성되었다고 해서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > async Task AsyncMethod1() { await AsyncMethod2(); Console.WriteLine("Run on thread pool or calling thread); } </pre> <br /> await 이후의 Console.WriteLine 메서드 실행이 호출과는 다른 스레드에서 실행될 거라고 가정해서는 안됩니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1753
(왼쪽의 숫자를 입력해야 합니다.)