성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Creating Docker multi-arch images f...
[정성태] BinaryFormatter removed from .NET 9...
[정성태] Extending the Windows Shell Progres...
[우광현] 와..... 범위를 잡았으니 클라이언트가 해당 범위를 확인해본다...
[정성태] 딱히, 그것 이상으로 더 설명할 내용이 없습니다. 동적 포...
[정성태] If Windows 3.11 required a 32-bit p...
[정성태] What is a hard error, and what make...
[괴물신인] 질문작성자인데 이 글을 이제봤네요 ㄷㄷ 이 글처럼 타입별로 인...
[정성태] 호오 기대되네요. ^^
[JunSeo Lee] 호오 신기하네요 .한번 추적해볼까
글쓰기
제목
이름
암호
전자우편
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 그리고 스레드 (3) Task.Delay 재현</h1> <p> 지난 글(<a target='tab' href='https://www.sysnet.pe.kr/2/0/13055'>1</a>, <a target='tab' href='https://www.sysnet.pe.kr/2/0/13056'>2</a>)을 통해, 기본적인 비동기 호출에 대한 동작 방식을 우리가 직접 MyTask를 구현하면서 알아봤습니다.<br /> <br /> 자, 그럼 이해를 돕기 위해 또 하나의 실습을 해볼까요? 적당한 걸로, 기존의 <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay'>Task.Delay</a>와 같은 기능을 구현해 보겠습니다. Task.Delay는 <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.sleep'>Thread.Sleep</a>의 <a target='tab' href='https://www.sysnet.pe.kr/2/0/11456#awaitable'>awaitable</a> 버전인데요, 다음과 같은 식으로 사용합니다.<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($"[{DateTime.Now}] 1단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); <span style='color: blue; font-weight: bold'>await Task.Delay(2000);</span> // 2초의 비동기 sleep 후, 아래의 코드 실행 Console.WriteLine($"[{DateTime.Now}] 2단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); } } /* 출력 결과 [2022-05-11 오후 10:12:<span style='color: blue; font-weight: bold'>12</span>] 1단계: threadid == <span style='color: blue; font-weight: bold'>1</span> [2022-05-11 오후 10:12:<span style='color: blue; font-weight: bold'>14</span>] 2단계: threadid == <span style='color: blue; font-weight: bold'>4</span> */ </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep'>Sleep은 Win32 API로 제공</a>은 되지만 엄밀히 비동기 버전의 SleepAsync API는 없습니다. 그렇다면 Task.Delay는 도대체 어떻게 구현을 한 것이고, await 이후의 코드는 어떤 스레드에서 실행이 되는 것일까요?<br /> <br /> 이를 알아보기 위해 우리가 만든 MyTask에도 Delay 기능을 넣어보겠습니다. 여기서 구현의 핵심은, (누누이 말했지만) await 이후로 분리된 코드를 실행할 스레드가 있어야 (<a target='tab' href='https://www.sysnet.pe.kr/2/0/11431'>혹은 없다면 동기 호출</a>) 한다는 점입니다.<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;' > internal class Program { static async Task Main(string[] args) { Console.WriteLine($"1단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); <span style='color: blue; font-weight: bold'>await MyTask.Delay(1000);</span> Console.WriteLine($"2단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); } } public class MyTask { // ...[생략]... <span style='color: blue; font-weight: bold'>public static MyTask Delay(int milliSeconds) { Action action = () => { Thread.Sleep(milliSeconds); }; return new MyTask(action); }</span> } /* 출력 결과 [2022-05-11 오후 10:14:<span style='color: blue; font-weight: bold'>24</span>] 1단계: threadid == <span style='color: blue; font-weight: bold'>1</span> [2022-05-11 오후 10:14:<span style='color: blue; font-weight: bold'>26</span>] 2단계: threadid == <span style='color: blue; font-weight: bold'>9</span> */ </pre> <br /> 비록, 구현은 했지만 한 가지 아쉬운 점이 있습니다. 바로 Sleep 시간 동안 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13055#mytask_thread'>(MyTask 내에서 새로 생성하는) 스레드 하나</a>가 점유된다는 것입니다. Sleep이 비동기 I/O 호출을 지원하지도 않으므로 얼핏 보면 저게 최선일 듯한데요, 다행히 방법이 있습니다. 바로 <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/api/system.threading.timer'>System.Threading.Timer</a>를 이용해 스레드 점유를 없애고 CLR ThreadPool을 활용하는 것입니다.<br /> <a name='task_delay'></a> <br /> 실제로 이 방법은 Task.Delay가 이용하고 있는데요, 우리도 다음과 같이 구현할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal class Program { static async Task Main(string[] args) { Console.WriteLine($"[{DateTime.Now}] 1단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); await MyTask.DelayAsync(2000); Console.WriteLine($"[{DateTime.Now}] 2단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); } } <span style='color: blue; font-weight: bold'>public class DelayTask : MyTask { System.Threading.Timer _timer; public DelayTask(int milliSeconds) { _timer = new Timer(expiredCallback, null, milliSeconds, 0); } private void expiredCallback(object? state) { _timer.Dispose(); this.SetComplete(); } }</span> public class MyTask { // ...[생략]... public MyTask() { } // ...[생략]... <span style='color: blue; font-weight: bold'>public static DelayTask DelayAsync(int milliSeconds) { return new DelayTask(milliSeconds); }</span> // ...[생략]... } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > /* 실행 결과 */ [2022-05-11 오후 10:14:<span style='color: blue; font-weight: bold'>39</span>] 1단계: threadid == <span style='color: blue; font-weight: bold'>1</span> [2022-05-11 오후 10:14:<span style='color: blue; font-weight: bold'>41</span>] 2단계: threadid == <span style='color: blue; font-weight: bold'>4</span> </pre> <br /> 간단하죠? ^^ 스레드 걱정을 할 필요가 없는 이유는, Timer 타입의 생성자에 전달하는 callback 메서드를 (지정된 시간 이후에) CLR ThreadPool로부터 선택된 스레드가 실행해 주기 때문입니다. 그리고, 자연스럽게 그 과정에서 MyTask.SetComplete의 호출로 인해 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13055#await_code'>await 이후의 "분할 2" 코드</a>까지 실행이 됩니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1933&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 자, 이렇게 해서 (<a target='tab' href='https://www.sysnet.pe.kr/2/0/13062'>다음 편</a>에서 다룰) 비동기 I/O를 제외하고는 기존의 Task에서 제공하던 Delay나 Task.Run을 이용한 비동기 호출 방식을 다뤄봤습니다. 보는 바와 같이, 마법은 없습니다. 단지 C# 컴파일러가 분리한 await 이후의 코드를 1) 사용자가 제공하든, 2) CLR ThreadPool에서 제공하든, 3) 설령 그것이 동기 또는 비동기이든 특정 스레드로 하여금 실행하도록 만들어야 한다는 사실에는 변함이 없습니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
9949
(왼쪽의 숫자를 입력해야 합니다.)