C# - 동기 방식이면서 비동기 메서드(awaitable)처럼 구현한 사례
일반 메서드를 필요하다면 (awaitable) Async로 만드는 방법은 Task 객체를 반환하도록 하면 됩니다. 가령, 5초 동안 스레드를 중지하는 다음의 메서드에 대해,
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
new MyClass().Test();
}
}
class MyClass
{
public void Test()
{
Thread.Sleep(1000);
}
}
}
TestAsync 메서드를 다음과 같이 만들어 줄 수 있습니다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static async Task Main(string[] args)
{
await new MyClass().TestAsync();
Console.WriteLine("End");
}
}
class MyClass
{
public Task TestAsync()
{
Task t = new Task(() => Thread.Sleep(1000));
t.Start();
return t;
}
}
}
그런데, Task 객체를 (직접 명시적으로) 만들지 않으면서 Async 기능을 구현한 것처럼 만들 수도 있습니다. 이럴 때 TaskCompletionSource 객체를 사용하면 됩니다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static async Task Main(string[] args)
{
await new MyClass().TestAsync2();
Console.WriteLine("End: TestAsync2");
}
}
class MyClass
{
public Task TestAsync2()
{
TaskCompletionSource<object> src = new TaskCompletionSource<object>();
try
{
Console.WriteLine("TestAsync2 called!");
src.SetResult(null);
}
catch (Exception e)
{
src.SetException(e);
}
return src.Task;
}
}
}
뭐 저런 쓸모 없는 코드가 다 있나... 할 수 있습니다. 그런데 이런 쓸모없는 코드를 .NET BCL에서 찾아 볼 수 있습니다.
// System.Data.dll
// System.Data.Common.DbConnection
public virtual Task OpenAsync(CancellationToken cancellationToken)
{
TaskCompletionSource<object> source = new TaskCompletionSource<object>();
if (cancellationToken.IsCancellationRequested)
{
source.SetCanceled();
}
else
{
try
{
this.Open();
source.SetResult(null);
}
catch (Exception exception)
{
source.SetException(exception);
}
}
return source.Task;
}
위에서 보는 바와 같이 System.Data.Common.DbConnection은 OpenAsync 메서드를 제공하지만, 이름과는 달리 내부적으로 처리는 완전하게 동기로 하고 있습니다.
이런 바보 같은 코드가 왜 나왔을까요? 사실 DbConnection은 추상 클래스이기 때문에 기본적인 기능만 제공하는 정도입니다. 그리고 실질적인 기능들은 이를 상속받은 타입들이 하게 되는데, 자식 클래스들에게 비동기를 강제로(?) 구현하게 짐을 지우는 것이 어떤 경우에는 부담이 될 수 있습니다. 그래서, 인터페이스 측면에서 비동기 기능을 제공하는 가상 메서드는 제공하지만 동기처럼 동작하는 저런 구현이 나온 것입니다.
실제로 System.Data.SqlClient의 SqlConnection은 OpenAsync를 다시 구현하면서 비동기 기능을 완전하게 제공합니다.
그 외에도, 일정에 쫓겨 비동기 기능을 구현하진 못했지만 언젠가(?) 꼭 구현하리라 다짐하고 Async 메서드를 만들어 둔다면 저런 식으로 할 수 있을 것입니다. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]