성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
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# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법</h1> <p> 일반적으로 await을 사용해 비동기 작업을 하려면 다음과 같이 Task를 반환하는 메서드를 대상으로 하게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class Program { static async Task Main(string[] args) { await CallAsync(); } static async Task<string> CallAsync() { return await new TaskFactory().StartNew(() => { Thread.Sleep(5000); return "test"; }); } } </pre> <br /> 그런데 <a href='https://www.sysnet.pe.kr/2/0/11417'>지난 글</a>에 설명한 C# 컴파일러의 async 메서드에 대한 자동 생성 코드를 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>TaskAwaiter<string> awaiter;</span> if (num != 0) { this.<getStringTask>5__1 = new TaskFactory().StartNew<string>(Program.<>c.<>9__2_0 ?? (Program.<>c.<>9__2_0 = new Func<string>(Program.<>c.<>9.<GetFileContents>b__2_0))); <span style='color: blue; font-weight: bold'>awaiter = this.<getStringTask>5__1.GetAwaiter();</span> if (!awaiter.IsCompleted) { this.<>1__state = num = 0; this.<>u__1 = awaiter; Program.<GetFileContents>d__2 stateMachine = this; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<GetFileContents>d__2>(ref awaiter, ref stateMachine); return; } } </pre> <br /> 특이하게도 요구 사항이 단지 GetAwaiter() 메서드가 제공되느냐에 대한 여부일 뿐입니다. 따라서, 굳이 Task를 반환하지 않아도 우리가 구현하는 메서드의 반환 타입이 GetAwaiter 메서드만 제공해 주고 있다면 C#은 정상적으로 컴파일할 수 있습니다. 예를 들어 다음과 같이 코드를 구성하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class Program { static async Task Main(string[] args) { await TestAwait(); } private static MyTask TestAwait() { return new MyTask(); } } public class MyTask { <span style='color: blue; font-weight: bold'>public TaskAwaiter GetAwaiter()</span> { <span style='color: blue; font-weight: bold'>TaskAwaiter ta</span>; // struct 타입이므로. return ta; } } </pre> <br /> 위의 코드만으로도 C# 컴파일러는 문제없이 바이너리를 잘 생성해 줍니다. 하지만 실행 시 오류가 발생합니다.<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;' > System.NullReferenceException HResult=0x80004003 Message=Object reference not set to an instance of an object. Source=mscorlib StackTrace: at System.Runtime.CompilerServices.TaskAwaiter.get_IsCompleted() at ConsoleApp1.Program.<Main>d__0.MoveNext() in E:\task_continue\ConsoleApp1\ConsoleApp1\Program.cs:line 19 at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at ConsoleApp1.Program.<Main>(String[] args) </pre> <br /> 원인은 async의 자동 생성 코드에서 IsCompleted를 호출하기 때문인데, TaskAwaiter 타입의 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 IsCompleted { [__DynamicallyInvokable] get { return this.m_task.IsCompleted; // m_task 값이 null임. } } </pre> <br /> 당연히 null 참조 예외가 발생하는 것입니다. 이를 해결하려면 TaskAwaiter의 생성자에 Task 타입을 전달해 생성해야 하는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public TaskAwaiter GetAwaiter() { TaskAwaiter ta = new TaskAwaiter( new TaskFactory().StartNew(() => { }) ); return ta; } </pre> <br /> 아쉽게도 Task 인자를 받는 TaskAwaiter의 생성자는 internal 접근자가 지정되어 있어 외부에서 사용할 수 없습니다. 따라서 우리는 TaskAwaiter를 재사용하는 코드를 작성할 수 없습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 제 책에서도 설명하고 있지만 C# 7.0의 신규 기능에 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# 7의 새로운 기능 ; <a target='tab' href='https://learn.microsoft.com/ko-kr/dotnet/csharp/whats-new/csharp-7'>https://learn.microsoft.com/ko-kr/dotnet/csharp/whats-new/csharp-7</a> </pre> <br /> "일반화된 비동기 반환 형식"이라고 해서 Task 대신 ValueTask 형식이 나옵니다. 그리고 그 ValueTask는 System.Threading.Tasks.Extensions 어셈블리에서 제공하고 있는데, 여기서 궁금함이 생깁니다. 어떻게 ValueTask는 TaskAwaiter를 정상적으로 구현할 수 있었을까요?<br /> <br /> 재미있는 것은, ValueTask는 TaskAwaiter를 사용하지 않고 자신만의 ValueTaskAwaiter 타입을 구현해 GetAwaiter 메서드에서 반환합니다. 처음에 봤을 때는, 이게 뭔가 싶었습니다. TaskAwaiter는 struct 타입이기 때문에 상속이 안되므로 당연히 ValueTaskAwaiter는 기존의 TaskAwaiter에 형변환이 안 됩니다. 자동 생성 코드를 다시 보면, C#은 async 메서드에 대해 TaskAwaiter를 사용합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>TaskAwaiter<string> awaiter;</span> if (num != 0) { this.<getStringTask>5__1 = new TaskFactory().StartNew<string>(Program.<>c.<>9__2_0 ?? (Program.<>c.<>9__2_0 = new Func<string>(Program.<>c.<>9.<GetFileContents>b__2_0))); <span style='color: blue; font-weight: bold'>awaiter = this.<getStringTask>5__1.GetAwaiter();</span> // ...[생략]... } </pre> <br /> 그런데 ValueTask는 ValueTaskAwaiter를 반환하는데 이대로라면 컴파일 시 오류가 발생하는 것이 맞습니다. 하지만... 잘 컴파일이 된다는 것은 뭔가 또 다른 비밀이 있다는 것입니다. 결국 ValueTask를 사용한 소스 코드를 역어셈블해 살펴보니 그 비밀이 풀립니다.<br /> <br /> 보면, C# 컴파일러는 GetAwaiter 메서드가 반환하는 타입을 자동 생성된 코드에서 사용해 줍니다. 즉, 위의 자동 생성 코드는 ValueTask에서 다음과 같이 바뀝니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>ValueTaskAwaiter</span><string> awaiter; if (num != 0) { this.<getStringTask>5__1 = new TaskFactory().StartNew<string>(Program.<>c.<>9__2_0 ?? (Program.<>c.<>9__2_0 = new Func<string>(Program.<>c.<>9.<GetFileContents>b__2_0))); <span style='color: blue; font-weight: bold'>awaiter = this.<getStringTask>5__1.GetAwaiter();</span> // ...[생략]... } </pre> <br /> 그렇습니다. Task를 대체하는 사용자 정의 await 기능을 제공하고 싶다면 GetAwaiter 메서드의 구현과 함께 그것이 반환하는 타입도 우리가 제공하면 됩니다. 따라서, 다음과 같이 구현할 수 있습니다.<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; using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static async Task Main(string[] args) { <span style='color: blue; font-weight: bold'>await TestAwait();</span> } private static <span style='color: blue; font-weight: bold'>MyTask</span> TestAwait() { MyTask task = new MyTask(); return task; } } <span style='color: blue; font-weight: bold'>public class MyTask</span> { public <span style='color: blue; font-weight: bold'>MyTaskAwaiter</span> GetAwaiter() { <span style='color: blue; font-weight: bold'>MyTaskAwaiter ta = new MyTaskAwaiter();</span> return ta; } } <span style='color: blue; font-weight: bold'>public struct MyTaskAwaiter</span> : ICriticalNotifyCompletion, INotifyCompletion { public bool IsCompleted { get { return true; } } public void GetResult() { } public void OnCompleted(Action continuation) { } public void UnsafeOnCompleted(Action continuation) { } } } </pre> <a name='awaitable'></a> <br /> 이렇게 await 대상으로 사용할 수 있는 타입을 "awaitable"하다고 표현합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그럼, 뼈대는 완성되었으니 Task 없이 단순히 Thread 타입만을 사용해 기존 await 동작과 유사하게 실행되도록 다음과 같이 코딩을 완성할 수 있습니다.<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; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static async Task Main(string[] args) { await TestAwait(); Console.WriteLine("test"); } private static MyTask TestAwait() { MyTask task = new MyTask( () => { Thread.Sleep(5000); }); return task; } } public class MyTask { Thread _t; bool _isCompleted; List<Action> _continuation = new List<Action>(); public void AddContinuation(Action action) { _continuation.Add(action); } public bool IsCompleted { get { return _isCompleted; } } public MyTask(Action action) { _t = new Thread( (ThreadStart)( () => { action(); _isCompleted = true; foreach (var item in _continuation) { item(); } } ) ); _t.Start(); } public MyTaskAwaiter GetAwaiter() { MyTaskAwaiter ta = new MyTaskAwaiter(this); return ta; } } public struct MyTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion { MyTask _task; public MyTaskAwaiter(MyTask t) { _task = t; } public bool IsCompleted { get { return _task.IsCompleted; } } public void GetResult() { } public void OnCompleted(Action continuation) { _task.AddContinuation(continuation); } public void UnsafeOnCompleted(Action continuation) { _task.AddContinuation(continuation); } } } </pre> <br /> 실행하면, 위의 코드는 기존 Task 기반으로 구현한 다음의 코드와 유사하게 동작하는 것을 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static async Task Main(string[] args) { await TaskAwait(); Console.WriteLine("test"); } private static Task TaskAwait() { return new TaskFactory().StartNew(() => { Thread.Sleep(5000); }); } </pre> <br /> (<a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1233&boardid=331301885'>첨부 파일은 이 글의 예제를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> <span style='text-decoration: line-through'> 이 글을 쓰면서 한 가지 궁금한 것이 생겼습니다. 마이크로소프트는 C# 7.0을 내놓으면서 신규 기능으로 "일반화된 비동기 반환 형식"을 ValueTask와 함께 예를 들어 소개하고 있는데, 엄밀히 말해서</span> C# 5.0 컴파일러에서도 이 글에서 구현한 MyTask / MyTaskAwaiter 타입은 잘 컴파일이 됩니다. <span style='text-decoration: line-through'>즉, 이것은 C# 7.0의 신규 기능이라고 볼 수 없는데 왜? 마이크로소프트는 그런 식으로 소개를 했느냐입니다.<br /> <br /> 게다가 ValueTask 역시 C#과는 무관하게 NuGet을 통해 별도 배포되는 System.Threading.Tasks.Extensions 어셈블리에서 구현하고 있을 뿐입니다.<br /> <br /> 이 때문에, 제 책에 다음과 같이 썼던 내용이 틀리게 됩니다.<br /> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> C# 5.0부터 구현된 async 예약어가 붙는 메서드는 반환 타입이 반드시 void, Task, Task<T> 중의 하나여야만 했다. 달리 말하면 수많은 비동기 시나리오에서 개발자가 최적화할 수 있는 여지를 닫아 버린 것이다. ...[중간생략]... C# 7.0부터는 사용자 정의 Task 타입을 구현하고 이를 async의 반환 타입으로 사용할 수 있도록 허용한다.<br /> </div><br /> <br /> 즉, 개발자가 최적화할 수 있는 여지는 C# 5.0부터 이미 열려 있었기 때문에 "일반화된 비동기 반환 형식"에 따라 사용자 정의 Task 타입을 정의할 수 있었습니다.<br /> </p><br /> </span> [업데이트: 2018-03-10] await 대상에 대한 사용자 정의 타입이 가능한 것과 <a href='http://www.sysnet.pe.kr/2/0/11484'>async 메서드의 반환으로 사용자 정의 타입을 사용할 수 있다는 것</a>은 의미가 다름.<br /><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1164
(왼쪽의 숫자를 입력해야 합니다.)