성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
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'>C# - async/await 그리고 스레드 (2) MyTask의 호출 흐름</h1> <p> 자, 그럼 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13055'>지난번 글</a>에 이어서, 왜 "Thread.Sleep(16)"을 두었는지 설명해 보겠습니다. 우선, Task.Run을 사용한 예제에서 Sleep 코드를 제거해볼까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; internal class Program { static async Task Main(string[] args) { Console.WriteLine($"1단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); await DoAsync(); Console.WriteLine($"2단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); } private static Task DoAsync() { return Task.Run( () => { Console.WriteLine($"DoAsync called! (threadid == {Thread.CurrentThread.ManagedThreadId})"); }); } } </pre> <br /> 위의 코드를 수행하면 보통 다음과 같은 출력을 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1단계: threadid == 1 DoAsync called! (threadid == 7) 2단계: threadid == 7 </pre> <br /> 하지만 가끔씩, 이런 식의 출력도 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1단계: threadid == 1 DoAsync called! (threadid == 6) 2단계: threadid == 7 </pre> <br /> 아니, Task.Run 스레드가 await 이후의 코드를 수행한다면서요? 그런데, 위의 결과는 (스레드 풀에 있던 것으로 짐작되는) 별도의 스레드가 await 이후의 코드를 실행하고 있습니다. 이유가 뭘까요?<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이것을 밝혀내려면 (물론, Task.cs 소스 코드를 분석해도 되지만) 우리가 만든 <a href='https://www.sysnet.pe.kr/2/0/13055#mytask'>MyTask의 소스 코드</a>에서 마찬가지로 Thread.Sleep을 제거해 봐야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Runtime.CompilerServices; internal class Program { static async Task Main(string[] args) { Console.WriteLine($"1단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); await new MyDummy().DoAsync(); Console.WriteLine($"2단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); } } class MyDummy { public MyTask DoAsync() { MyTask _task = new MyTask(() => { Console.WriteLine($"DoAsync called! (threadid == {Thread.CurrentThread.ManagedThreadId})"); }); return _task; } } public class MyTask { Action? _continuation; bool _completed = false; public bool Completed => _completed; public MyTask(Action action) { <span style='color: blue; font-weight: bold'>ParameterizedThreadStart taskAction = (arg) => { action(); this.SetComplete(); };</span> Thread t = new Thread(taskAction); t.Start(); } public MyTaskAwaiter GetAwaiter() { MyTaskAwaiter ta = new MyTaskAwaiter(this); return ta; } public void SetComplete() { _completed = true; if (_continuation != null) { _continuation(); } } private void SetContinuation(Action continuation) { _continuation = continuation; } public struct MyTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion { MyTask _task; public MyTaskAwaiter(MyTask task) { _task = task; } public bool IsCompleted { get { return _task.Completed; } } public void GetResult() { } public void OnCompleted(Action continuation) { } public void UnsafeOnCompleted(Action continuation) { _task.SetContinuation(continuation); } } } </pre> <br /> 이렇게 바꾸고 실행하면, 이제는 다음과 같이 await 이후의 코드가 실행되지 않는 것을 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1단계: threadid == 1 DoAsync called! (threadid == 7) </pre> <br /> 무엇이 문제일까요? 간단합니다. _continuation을 호출하지 못하는 상황이 발생하기 때문입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public MyTask(Action action) { ParameterizedThreadStart taskAction = (arg) => { action(); <span style='color: blue; font-weight: bold'>this.SetComplete();</span> }; Thread t = new Thread(taskAction); t.Start(); } public void SetComplete() { _completed = true; <span style='color: blue; font-weight: bold'>if (_continuation != null) { _continuation(); }</span> } </pre> <br /> 위의 동작과 함께 원래의 await 코드가 다음과 같이 실행되는 형식이라는 것을 감안하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Task task = new MyDummy().DoAsync(); if (task.IsCompleted == true) { // "분할 2" 코드 실행 } else { task.GetAwaiter().UnsafeOnCompleted(...[분할 2] 코드...); } </pre> <br /> Thread.Start 실행은 되었지만 아직 SetComplete를 호출하기 전이라고 가정해 보겠습니다. 그럼, if (task.IsCompleted == true)는 아니기 때문에 "분할 2" 코드는 실행되지 않고, 향후 IsCompleted가 true인 상황에서 실행되도록 UnsafeOnCompleted를 통해 보관하는 else 절을 타게 됩니다.<br /> <br /> 하지만, else 절까지 이동했지만 그사이 SetComplete의 코드가 실행되는 경우라면 어떨까요? 그럼 _completed = true가 되지만 아직 _continuation이 들어온 것은 아니므로 SetComplete 메서드에서는 실행이 안 될 것입니다. 결국 "분할 2" 코드는 어디에서도 실행이 안 된 것입니다.<br /> <br /> 따라서, 저런 식의 상황을 만들지 않기 위해 일부러 Thread.Sleep(16) 정도의 시간을 주면 우리가 원하는 대로 항상 스레드가 _continuation 코드를 실행할 수 있었던 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> (확실하게 분석한 것은 아니지만) 닷넷의 Task는 이런 문제를 해결하기 위해 실행되지 않는 UnsafeOnCompleted의 action 코드를 스레드 풀의 스레드가 실행하는 대비책을 마련해두었습니다.<br /> <br /> 이를 이용해 우리가 만든 MyTask의 동작을 다음과 같이 변경할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class MyTask { // ...[생략]... bool _executed = false; public void SetComplete() { _completed = true; <span style='color: blue; font-weight: bold'>_executed = _continuation != null; if (_executed == true) { _continuation!(); }</span> } private void SetContinuation(Action continuation) { _continuation = continuation; <span style='color: blue; font-weight: bold'>if (_completed == true && _executed == false) { ThreadPool.QueueUserWorkItem((obj) => { _continuation(); }); }</span> } // ...[생략]... } </pre> <br /> 이렇게 하면, SetComplete 시점에 _continuation을 실행할 수 없어도 (UnsafeOnCompleted를 경유한) SetContinuation에서 스레드 풀에 넣어 두는 작업을 한 번 거치기 때문에 이런 실행 결과를 보입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1단계: threadid == 1 DoAsync called! (threadid == 7) 2단계: threadid == 8 </pre> <br /> <hr style='width: 50%' /><br /> <br /> 유의할 점은, 제가 이 글에서 구현하는 MyTask와 MyTaskAwaiter는 임의로 재현을 위해 코드를 만든 것일 뿐 이 동작이 BCL의 Task, TaskAwaiter와 정확히 같지는 않습니다. 그냥 전체적인 흐름이 그런 식으로 된다는 정도로 이해하시면 되겠습니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1931&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1585
(왼쪽의 숫자를 입력해야 합니다.)