C# - async를 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법
이전 글에서, await의 대상이 되는 메서드의 반환값을 우리가 정의한 타입으로 바꾸는 방법을 설명했습니다.
C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법
; https://www.sysnet.pe.kr/2/0/11456
이번엔 저 글의 예제에 사용된 MyTask를 async 메서드의 반환 타입으로도 사용할 수 있도록 확장해 보겠습니다.
지난번 예제에서 Task와 MyTask를 반환하는 메서드를 각각 async 예약어를 붙여 코드를 수정하면,
private static async Task TaskAwait()
{
await new TaskFactory().StartNew(() => { Thread.Sleep(5000); });
}
// CS1983 컴파일 에러 발생
private static async MyTask TestAwait()
{
MyTask task = new MyTask( () =>
{
Thread.Sleep(5000);
});
await task;
}
빌드 시 MyTask를 반환하는 코드에 대해 다음과 같은 컴파일 오류가 발생합니다.
CS1983 The return type of an async method must be void, Task or Task<T>
의미인즉, async 메서드는 반환 타입이 반드시 void, Task, Task<T> 중의 하나여야 한다는 것입니다. 하지만
C# 7.0부터 "일반화된 비동기 반환 형식"을 지원하면서 그 외의 타입으로도 가능하게 바뀌었습니다. 일례로
ValueTask<T> 타입이 그것입니다.
사용자 정의 타입을 async 메서드의 반환 타입으로 쓰려면,
Return Any (Task-Like) Type From An Async Method
; http://blog.i3arnon.com/2016/07/25/arbitrary-async-returns/
다음의 2가지 타입을 더 구현해야 합니다.
- 사용자 정의 Method Builder 타입 정의
- 사용자 정의 Awaiter 타입 정의
따라서 우리가 구현한 MyTask에 대한 사용자 정의 Method Builder를 다음과 같이 구현할 수 있습니다.
public struct AsyncMyTaskMethodBuilder
{
AsyncTaskMethodBuilder _methodBuilder;
public MyTask Task
{
get
{
return new MyTask(_methodBuilder.Task);
}
}
public static AsyncMyTaskMethodBuilder Create()
{
return new AsyncMyTaskMethodBuilder { _methodBuilder = AsyncTaskMethodBuilder.Create() };
}
public void SetException(Exception exception)
{
_methodBuilder.SetException(exception);
}
public void SetResult()
{
_methodBuilder.SetResult();
}
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine
{
_methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);
}
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine
{
_methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
_methodBuilder.SetStateMachine(stateMachine);
}
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
_methodBuilder.Start(ref stateMachine);
}
}
그다음 Awaiter를 이런 식으로 정의해 주면 됩니다.
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)
{
if (_task.Task == null)
{
_task.AddContinuation(continuation);
}
else
{
_task.Task.GetAwaiter().OnCompleted(continuation);
}
}
public void UnsafeOnCompleted(Action continuation)
{
if (_task.Task == null)
{
_task.AddContinuation(continuation);
}
else
{
_task.Task.GetAwaiter().UnsafeOnCompleted(continuation);
}
}
}
마지막으로 우리가 정의한 MyTask 타입에 새롭게 정의한 Method Builder 타입을 연결하고 기타 작업을 추가해야 합니다.
[AsyncMethodBuilder(typeof(AsyncMyTaskMethodBuilder))]
public class MyTask
{
Thread _t;
Task _task;
internal Task Task
{
get { return _task; }
}
bool _isCompleted;
List<Action> _continuation = new List<Action>();
public void AddContinuation(Action action)
{
if (_t == null)
{
_task.ContinueWith((t) => { action(); });
}
else
{
_continuation.Add(action);
}
}
public bool IsCompleted
{
get
{
if (_t == null)
{
return _task.IsCompleted;
}
return _isCompleted;
}
}
public MyTask(Task task)
{
_task = task;
}
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;
}
}
이렇게 변경한 후, 다시 빌드하면 컴파일 오류가 발생했던 TestAwait 메서드가 정상적으로 빌드되는 것을 확인할 수 있습니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
구현을 보면 알겠지만, 사실 async/await 관련 구현은 Task 객체를 기반으로 사용자 정의 타입을 정의했을 때 부드럽게 연결이 됩니다. 즉, async 메서드에 사용자 정의 타입을 반환할 수 있다고는 하지만 결국 Task 타입을 기반으로 한 사용자 정의 타입인 경우에만 구현이 용이하게 되어 있습니다.
그나저나, "
C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법" 글을 쓸 때에도 마지막에 C# 5.0부터 가능했었다는 말을 했었는데요. 하지만, C# 7.0 문서에서는 await이 아닌 async 메서드를 대상으로 "일반화된 비동기 반환 형식"이 가능하다고 했기 때문에 일단 저 글에서는 마지막 내용을 취소선으로 표시했습니다.
그런데, 이번 글에 쓴 코드 역시 C# 5.0 컴파일러에서도 잘 빌드가 됩니다. 즉, "일반화된 비동기 반환 형식"은 C# 7.0부터 제공한 기능이라기보다는 그냥 7.0부터 본격적으로 소개를 한 것입니다. 사실 이에 대해서는 ValueTask의 소개 시점을 유추해 보면 좀 이상하긴 했습니다. 왜냐하면 최초 ValueTask가,
System.Threading.Tasks.Extensions
; https://www.nuget.org/packages/System.Threading.Tasks.Extensions/
릴리스된 날짜가 2016년 5월인 반면 C# 7.0은 2017년 3월이었기 때문에 C# 7.0의 "
Generalized async return types" 덕분에 구현이 가능했던 것은 아니기 때문입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]