.NET 작업을 WinRT 비동기 작업으로 노출하기

WinRT와 await에 대한 고찰이라는 블로그 글에서는 C# 및 Visual Basic의 새로운 asyncawait 키워드를 소개하고 이러한 키워드를 Windows 런타임(WinRT) 비동기 작업에 사용하는 방법을 설명했습니다.

.NET BCL(기본 클래스 라이브러리)을 일정 부분 활용하면 이러한 키워드를 사용하여 비동기 작업, 즉 사용할 다른 언어에 내장된 다른 구성 요소에 대해서 WinRT를 통해 이후 노출되는 작업을 개발할 수도 있습니다. 이 글에서는 그러한 비동기 작업을 개발하는 방법을 알아보겠습니다. C#이나 Visual Basic을 사용하여 WinRT 구성 요소를 구현하는 방법에 대한 자세한 내용은 C# 및 Visual Basic을 통한 Windows 런타임 구성 요소 생성을 참조하세요.

그럼 WinRT에서 비동기 API가 어떤 모습인지 먼저 살펴보겠습니다.

WinRT 비동기 인터페이스

WinRT에는 비동기 작업과 관련한 몇 가지 인터페이스가 있습니다. 첫 번째, IAsyncInfo는 유효한 모든 WinRT 비동기 작업에서 구현되어 작업의 현재 상태, 실패한 작업의 오류, 작업의 ID, 취소 요청을 할 수 있는지 여부 등 비동기 작업과 관련한 일반적인 기능을 표시합니다.

 public interface IAsyncInfo
{
    AsyncStatus Status { get; }
    Exception ErrorCode { get; }
    uint Id { get; }

    void Cancel();
    void Close();
}

모든 WinRT 비동기 작업은 IAsyncInfo를 구현하는 것 외에 IAsyncAction, IAsyncActionWithProgress<TProgress> , IAsyncOperation<TResult> 또는 IAsyncOperationWithProgress<TResult,TProgress> 의 네 가지 추가 인터페이스 중 하나도 구현합니다. 이러한 추가 인터페이스는 소비자가 비동기 작업이 완료되면 호출되는 콜백을 설정하고 필요에 따라 결과를 표시하거나 진행률 보고서를 볼 수 있게 합니다.

 // No results, no progress
public interface IAsyncAction : IAsyncInfo
{
    AsyncActionCompletedHandler Completed { get; set; }
    void GetResults();
}

// No results, with progress
public interface IAsyncActionWithProgress<TProgress> : IAsyncInfo
{
    AsyncActionWithProgressCompletedHandler<TProgress> Completed { get; set; }
    AsyncActionProgressHandler<TProgress> Progress { get; set; }
    void GetResults();
}

// With results, no progress
public interface IAsyncOperation<TResult> : IAsyncInfo
{
    AsyncOperationCompletedHandler<TResult> Completed { get; set; }
    TResult GetResults();
}

// With results, with progress
public interface IAsyncOperationWithProgress<TResult,TProgress> : IAsyncInfo
{
    AsyncOperationWithProgressCompletedHandler<TResult,TProgress> Completed { get; set; }
    AsyncOperationProgressHandler<TResult,TProgress> Progress { get; set; }
    TResult GetResults();
}

(IAsyncAction은 반환할 결과가 없더라도 GetResults 메서드를 제공합니다. 따라서 비동기 작업에서 오류가 발생한 경우 모든 소비자가 IAsyncInfoErrorCode 속성에 액세스하게 되는 것이 아니라 예외가 반환됩니다. C# 및 Visual Basic에서 GetResult 메서드가 'void'를 반환하도록 형식화된 경우에도 awaiter 형식이 GetResult 메서드를 노출하는 것과 유사합니다.)

WinRT 라이브러리를 작성할 때는 해당 라이브러리에서 공개적으로 노출된 모든 비동기 작업이 이 네 가지 인터페이스 중 하나를 반환하도록 강력하게 형식화됩니다. 반면, .NET 라이브러리에서 노출되는 새로운 비동기 작업은 작업 기반 비동기 패턴(TAP)을 따르며, 결과를 반환하지 않는 작업의 경우 Task를, 결과를 반환하는 작업의 경우 Task<TResult> 를 반환합니다.

TaskTask<TResult> 는 이러한 WinRT 인터페이스를 구현하지 않고, WinRT Windows.Foundation.Uri 형식이나 BCL System.Uri 형식 같은 일부 형식의 경우와 달리 CLR(공용 언어 런타임)에서 암시적으로 차이점을 숨기지도 않습니다. 대신 서로 간에 명시적으로 변환해야 합니다. WinRT와 await에 대한 고찰이란 글에서 BCL의 AsTask 확장 메서드를 사용하여 WinRT 동기 인터페이스를 .NET 작업으로 변환하는 명시적인 캐스트 형식의 메커니즘을 제공하는 방법을 설명했습니다. BCL은 이와 반대로 메서드를 사용해 .NET 작업을 WinRT 비동기 인터페이스로 변환하는 기능도 지원합니다.

AsAsyncAction 및 AsAsyncOperation을 사용한 변환

좀 더 쉽게 설명하기 위해 DownloadStringAsyncInternal이라는 .NET 비동기 메서드가 있다고 가정해 보도록 하겠습니다. 이 메서드를 웹 페이지의 URL로 전달하면 해당 페이지의 콘텐츠를 비동기식으로 다운로드하여 문자열로 반환합니다.

 internal static Task<string> DownloadStringAsyncInternal(string url);

이 메서드를 구현하는 방법은 중요하지 않습니다. 그보다는 이 작업을 WinRT 비동기 작업으로 매핑하여 앞서 언급한 네 가지 인터페이스 중 하나를 반환하는 메서드로 만드는 것이 중요합니다. 이 작업에는 결과(문자열)가 있고 진행률 보고 기능은 지원하지 않으므로 WinRT 비동기 작업에서는 IAsyncOperation<string> 이 반환됩니다.

 public static IAsyncOperation<string> DownloadStringAsync(string url);

이 메서드를 구현하려면 DownloadStringAsyncInternal 메서드를 호출하여 그 결과로 Task<string> 을 얻으면 됩니다. 그런 다음 해당 작업을 필요한 IAsyncOperation<string> 으로 변환해야 합니다. 이 변환을 실행하려면 어떻게 해야 할까요?

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    Task<string> from = DownloadStringAsyncInternal(url);
    IAsyncOperation<string> to = ...; // TODO: how do we convert 'from'?
    return to;
}

이 문제를 해결하기 위해 .NET 4.5의 System.Runtime.WindowsRuntime.dll 어셈블리에는 필요한 변환을 지원하는 TaskTask<TResult> 용 확장 메서드가 포함되었습니다.

 // in System.Runtime.WindowsRuntime.dll
public static class WindowsRuntimeSystemExtensions 
{
    public static IAsyncAction AsAsyncAction(
        this Task source);
    public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
        this Task<TResult> source);
    ...
}

이러한 메서드는 각각 제공된 Task 또는 Task<TResult> 를 래핑하는 새로운 IAsyncAction 또는 IAsyncOperation<TResult> 인스턴스를 반환합니다. Task<TResult>Task에서 파생되므로 두 메서드 모두 Task<TResult> 에 사용할 수 있지만 결과를 반환하는 비동기 메서드와 함께 AsAsyncAction을 사용하는 경우는 매우 드뭅니다. 논리적으로, 이러한 작업은 명시적인 동기 캐스트 또는 디자인 패턴의 관점에서 어댑터와 같다고 할 수 있습니다. 이들 확장 메서드는 기본 작업을 나타내지만 WinRT에 필요한 서피스를 노출하는 인스턴스를 반환합니다. 이러한 확장 메서드를 사용하면 DownloadStringAsync를 구현할 수 있습니다.

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    Task<string> from = DownloadStringAsyncInternal(url);
    IAsyncOperation<string> to = from.AsAsyncOperation();
    return to;
}

작업이 얼마나 캐스트와 유사한지를 강조하면서 보다 간결하게 코드를 작성할 수도 있습니다.

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    return DownloadStringAsyncInternal(url).AsAsyncOperation();
}

AsAsyncOperation을 호출하기 전에 항상 DownloadStringAsyncInternal이 호출됩니다. 따라서 DownloadStringAsync 래퍼 메서드가 정상적으로 응답하는지 확인할 수 있도록 DownloadStringAsyncInternal에 대한 동기 호출이 신속하게 반환되어야 합니다. 어떤 이유에서든 수행하는 동기 작업이 너무 오래 걸릴 것 같거나, 스레드 풀에 대한 호출을 명시적으로 오프로드하려는 경우에는 Task.Run을 사용하고 반환된 작업에 대해 AsAsyncOperation을 호출하면 됩니다.

 public static IAsyncOperation<string> DownloadStringAsync(string url)
{
    return Task.Run(()=>DownloadStringAsyncInternal(url)).AsAsyncOperation();
}

AsyncInfo.Run을 사용하여 유연한 코드 작성

기본 제공되는 이러한 AsAsyncActionAsAsyncOperation 확장 메서드는 Task에서 IAsyncAction로, Task<TResult> 에서 IAsyncOperation<TResult> 로의 간단한 변환을 수행하는 데 매우 유용합니다. 하지만 좀 더 복잡한 변환을 수행하려면 어떻게 해야 할까요?

System.Runtime.WindowsRuntime.dll에는 보다 유연한 변환을 지원하는 AsyncInfo라는 형식이 System.Runtime.InteropServices.WindowsRuntime 네임스페이스에 포함되어 있습니다. AsyncInfo는 정적 Run 메서드의 네 가지 오버로드를 노출합니다. 이러한 오버로드는 네 가지 WinRT 비동기 인터페이스에 각각 사용됩니다.

 // in System.Runtime.WindowsRuntime.dll
public static class AsyncInfo 
{
    // No results, no progress
    public static IAsyncAction Run(
        Func<CancellationToken, 
             Task> taskProvider); 

    // No results, with progress
    public static IAsyncActionWithProgress<TProgress> Run<TProgress>(
        Func<CancellationToken, 
             IProgress<TProgress>, 
             Task> taskProvider);


    // With results, no progress
    public static IAsyncOperation<TResult> Run<TResult>(
        Func<CancellationToken, 
             Task<TResult>> taskProvider);

    // With results, with progress
    public static IAsyncOperationWithProgress<TResult, TProgress> Run<TResult, TProgress>(
        Func<CancellationToken, 
             IProgress<TProgress>, 
             Task<TResult>> taskProvider);
}

앞서 설명한 AsAsyncAction 메서드와 AsAsyncOperation 메서드는 Task를 인수로 사용합니다. 반면 이러한 Run 메서드는 Task를 반환하는 함수 대리자를 사용하는데, TaskFunc<…,Task> 간의 이러한 차이 덕분에 보다 복잡한 작업을 구현하는 데 필요한 유연성을 얻을 수 있습니다.

논리적으로, AsAsyncActionAsAsyncOperation은 보다 복잡한 AsyncInfo.Run을 기반으로 한 도우미라고 할 수 있습니다.

 public static IAsyncAction AsAsyncAction(
    this Task source)
{
    return AsyncInfo.Run(_ => source);
}

public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
    this Task<TResult> source)
{
    return AsyncInfo.Run(_ => source);
}

.NET 4.5에서 이들 메서드가 정확히 이러한 식으로 구현되는 것은 아니지만 기능적으로는 이와 같이 작동하므로 원리를 파악하고 기본 지원과 고급 지원 간의 차이를 이해하는 데에는 도움이 됩니다. 간단한 변환의 경우에는 AsAsyncActionAsAsyncOperation을 이용하면 되지만 AsyncInfo.Run을 사용하여 복잡한 변환을 수행해야 하는 경우도 있습니다.

취소

AsyncInfo.Run을 사용할 경우 WinRT 비동기 메서드를 취소하도록 지원할 수 있습니다.

다운로드 예제에서 CancellationToken을 사용하는 다른 DownloadStringAsyncInternal이 있다고 가정해 보도록 하겠습니다.

 internal static Task<string> DownloadStringAsyncInternal(
    string url, CancellationToken cancellationToken);

CancellationToken은 구성 가능한 방식으로 협업적 취소를 지원하는 .NET Framework 형식입니다. 원하는 수의 메서드 호출에 단일 토큰을 전달하면, 해당 토큰을 생성한 CancellationTokenSource를 통해 취소가 요청될 경우 사용 중인 모든 작업에서 취소 요청을 인식하게 됩니다. 이 방식은 개별 IAsyncInfo마다 자체적으로 Cancel 메서드를 노출하는 WinRT 비동기 방식과 약간 차이가 있습니다. 그렇다면 IAsyncOperation<string> 에 대한 Cancel 호출을 통해 DownloadStringAsyncInternal로 전달된 CancellationToken의 취소 요청을 하려면 어떻게 해야 할까요? 이 경우에는 AsAsyncOperation을 사용할 수 없습니다.

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
     return DownloadStringAsyncInternal(uri, … /* what goes here?? */)
            .AsAsyncOperation();
}

IAsyncOperation<string> 에서 취소가 요청되었음을 인식하려면 인스턴스가 어떤 식으로든 수신기에 Cancel 메서드가 호출되었음을 알려야 합니다. 예를 들어 DownloadStringAsyncInternal로 전달한 CancellationToken의 취소를 요청할 수 있습니다. 하지만 이전의 "catch-22"에서는 DownloadStringAsyncInternal을 호출할 때까지 AsAsyncOperation을 호출할 Task<string> 이 반환되지 않습니다. 그리고 DownloadStringAsyncInternal을 호출하는 시점에는 이미 AsAsyncOperation에서 제공할 CancellationToken이 이미 제공된 상태여야 합니다.

이 문제는 AsyncInfo.Run에서 사용되는 해결책을 비롯한 여러 가지 방법으로 해결할 수 있습니다. Run 메서드는 IAsyncOperation<string> 을 구성하는 역할을 하는데, 이 인스턴스는 비동기 작업의 Cancel 메서드가 호출된 경우에 역시 이 메서드가 생성하는 CancellationToken의 취소를 요청하기 위해 생성하는 것입니다. 그리고 Run으로 전달된 사용자가 제공한 대리자를 호출할 때 이 토큰을 전달하여 앞서 언급한 순환 문제가 발생하지 않게 합니다.

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(cancellationToken => 
        DownloadStringAsyncInternal(uri, cancellationToken));
}

람다 및 익명 메서드

AsyncInfo.Run은 람다 함수를 간편하게 사용할 수 있게 하고 익명 메서드는 WinRT 비동기 메서드를 구현합니다.

예를 들어 DownloadStringAsyncInternal 메서드가 아직 없었다면 이 메서드를 구현했을 것이고 이때 DownloadStringAsync는 다음과 같습니다.

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(delegate(CancellationToken cancellationToken)
    {
        return DownloadStringAsyncInternal(uri, cancellationToken));
    });
}

private static async Task<string> DownloadStringAsyncInternal(
    string uri, CancellationToken cancellationToken)
{
    var response = await new HttpClient().GetAsync(
        uri, cancellationToken);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

비동기 익명 메서드 작성을 지원하는 C#과 Visual Basic의 기능을 이용하면 이 두 메서드를 하나로 결합하여 구현을 간소화할 수 있습니다.

 public static IAsyncOperation<string> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(async delegate(CancellationToken cancellationToken)
    {
        var response = await new HttpClient().GetAsync(
            uri, cancellationToken);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    });
}    

진행률

AsyncInfo.Run은 WinRT 비동기 메서드를 통해 진행률을 보고하는 기능도 지원합니다.

DownloadStringAsync 메서드가 IAsyncOperation<string> 을 반환하지 않고 IAsyncOperationWithProgress<string,int> 를 반환하도록 하려고 한다고 가정해 보도록 하겠습니다.

 public static IAsyncOperationWithProgress<string,int> DownloadStringAsync(string uri);

이 경우 DownloadStringAsync가 정수 데이터로 진행률 업데이트를 제공할 수 있습니다. 그러면 소비자가 대리자를 인터페이스의 Progress 속성으로 설정하여 진행률의 변화에 대한 알림을 수신할 수 있습니다.

AsyncInfo.RunFunc<CancellationToken,IProgress<TProgress>,Task<TResult>> 를 사용하는 오버로드를 제공합니다. AsyncInfo.RunCancel 메서드가 호출된 경우 취소를 요청할 CancellationToken을 대리자에 전달하는 것과 마찬가지로, Report 메서드가 소비자의 Progress 대리자 호출을 트리거하는 IProgress<TProgress> 인스턴스를 전달할 수도 있습니다. 예를 들어, 시작 시 0% 진행률을 보고하고 응답이 반환되면 50% 진행률을, 응답을 문자열로 구문 분석한 후에는 100% 진행률을 각각 보고하도록 이전 예제를 다음과 같이 수정할 수 있습니다.

 public static IAsyncOperationWithProgress<string,int> DownloadStringAsync(string uri)
{
    return AsyncInfo.Run(async delegate(
        CancellationToken cancellationToken, IProgress<int> progress)
    {
        progress.Report(0);
        try
        {
            var response = await new HttpClient().GetAsync(uri, cancellationToken);
            progress.Report(50);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        finally { progress.Report(100); }
    });
}

심층 해부

지금까지 설명한 모든 작동 원리를 쉽게 이해하기 위해 AsAsyncOperationAsyncInfo.Run의 구현 예제를 살펴보겠습니다. .NET 4.5의 구현 방식과는 차이가 있고 강력한 구현 방법도 아닙니다. 그보다는 작동 원리를 심층적으로 이해하는 데 유용한 대략적인 예제라고 할 수 있으며, 프로덕션 환경에서는 사용할 수 없습니다.

AsAsyncOperation

AsAsyncOperation 메서드는 Task<TResult> 를 받아 IAsyncOperation<TResult> 를 반환합니다.

 public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
    this Task<TResult> source);

이 메서드를 구현하려면 IAsyncOperation<TResult> 를 구현하고 제공된 작업을 래핑하는 형식을 만들어야 합니다.

 public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(
    this Task<TResult> source)
{
    return new TaskAsAsyncOperationAdapter<TResult>(source);
}

internal class TaskAsAsyncOperationAdapter<TResult> : IAsyncOperation<TResult>
{
    private readonly Task<TResult> m_task;

    public TaskAsAsyncOperationAdapter(Task<TResult> task) { m_task = task; }

    ...
}

이 형식의 인터페이스 메서드의 각 구현은 래핑된 작업의 기능에 위임됩니다. 가장 간단한 멤버부터 살펴보겠습니다.

첫째로, IAsyncInfo.Close 메서드는 완료된 비동기 작업에서 사용이 끝난 리소스를 과감하게 정리하는 역할을 합니다. 하지만 우리 예제에서는 그러한 리소스가 없기 때문에(개체가 작업을 래핑) 구현 코드가 비어 있게 됩니다.

 public void Close() { /* NOP */ }

IAsyncInfo.Cancel 메서드를 사용하면 비동기 작업의 소비자가 작업 취소를 요청할 수 있습니다. 이는 순전히 요청일 뿐이며 작업을 강제로 종료시킬 수는 없습니다. 여기서는 CancellationTokenSource를 사용하여 요청이 발생했음을 기억합니다.

 private readonly CancellationTokenSource m_canceler =
    new CancellationTokenSource();

public void Cancel() { m_canceler.Cancel(); }

IAsyncInfo.Status 속성은 AsyncStatus를 반환하여 비동기 수명 주기와 관련하여 작업의 현재 상태를 나타냅니다. 현재 상태는 Started, Completed, Error 또는 Canceled의 네 가지 값으로 표시됩니다. 대부분의 경우 기본 TaskStatus 속성을 위임하고 반환된 TaskStatus에서 필요한 AsyncStatus로 매핑하기만 하면 됩니다.

From TaskStatus

To AsyncStatus

RanToCompletion

Completed

Faulted

Error

Canceled

Canceled

All other values & cancellation was requested

Canceled

All other values & cancellation was not requested

Started

Task가 아직 완료되지 않았는데 취소가 요청된 경우에는 Started 대신 Canceled를 반환해야 합니다. 즉, TaskStatus.Canceled는 터미널 상태일 뿐이지만 AsyncStatus.Canceled는 터미널 상태일 수도 있고 비터미널 상태일 수도 있기 때문에 IAsyncInfoCanceled 상태로 끝나거나 Canceled 상태의 IAsyncInfoAsyncStatus.Completed 또는 AsyncStatus.Error로 전환될 수 있습니다.

 public AsyncStatus Status
{
    get
    {
        switch (m_task.Status)
        {
            case TaskStatus.RanToCompletion: 
                return AsyncStatus.Completed;
            case TaskStatus.Faulted: 
                return AsyncStatus.Error;
            case TaskStatus.Canceled: 
                return AsyncStatus.Canceled;
            default: 
                return m_canceler.IsCancellationRequested ? 
                    AsyncStatus.Canceled : 
                    AsyncStatus.Started;
        }
    }
}

IAsyncInfo.Id 속성은 작업에 대한 UInt32 식별자를 반환합니다. Task 자체는 이미 그러한 식별자를 Int32로서 노출하기 때문에 기본 Task 속성에 위임하기만 하면 이 속성을 구현할 수 있습니다.

 public uint Id { get { return (uint)m_task.Id; } }

WinRT는 HResult를 반환하도록 IAsyncInfo.ErrorCode 속성을 정의합니다. 하지만 CLR은 내부적으로 WinRT HResult를 .NET Exception에 매핑하여 관리되는 프로젝션을 통해 우리에게 표시합니다. Task 자체는 Exception 속성을 노출하므로 이를 통해서 위임하면 됩니다. TaskFaulted 상태로 끝난 경우 첫 번째 예외(Task.WhenAll에서 반환된 Task의 경우와 같이 여러 예외가 발생하여 Task가 실패할 수 있음)를 반환하고 그렇지 않으면 'null'을 반환합니다.

 public Exception ErrorCode
{
    get { return m_task.IsFaulted ? m_task.Exception.InnerException : null; }
}

이것으로 IAsyncInfo의 구현은 끝났습니다. 이제 IAsyncOperation<TResult> 에서 제공되는 두 가지 멤버, GetResultsCompleted를 추가로 구현해야 합니다.

소비자는 작업이 성공적으로 완료되거나 예외가 발생하면 GetResults를 호출합니다. 전자의 경우 계산된 결과를 반환하고 후자의 경우 관련 예외를 반환합니다. 작업이 Canceled 상태로 끝나거나 아직 끝나지 않은 경우 GetResults를 호출할 수 없습니다. 따라서 작업 awaiter의 GetResult 메서드를 사용하여 작업이 성공적으로 완료된 경우 결과를 반환하고, 작업이 Faulted 상태로 끝난 경우 올바른 예외를 전파하도록 다음과 같이 GetResults를 구현할 수 있습니다.

 public TResult GetResults()
{
    switch (m_task.Status)
    {
        case TaskStatus.RanToCompletion:
        case TaskStatus.Faulted:
            return m_task.GetAwaiter().GetResult();
        default:
            throw new InvalidOperationException("Invalid GetResults call.");
    }
}

마지막으로, Completed 속성이 있습니다. Completed는 작업이 완료되었을 때 호출해야 하는 대리자를 나타냅니다. Completed가 설정되었을 때 작업이 이미 완료된 상태라면 제공된 대리자를 즉시 호출하거나 예약해야 합니다. 또한 소비자는 이 속성을 한 번만 설정할 수 있으며, 여러 번 설정할 경우 예외가 발생합니다. 작업이 완료된 후에는 메모리 누설이 발생하지 않도록 이 구현에서 참조를 대리자에게 이양해야 합니다. TaskContinueWith 메서드를 사용하면 이러한 동작을 대부분 구현할 수 있지만 ContinueWith는 동일한 Task 인스턴스에 여러 번 사용될 수 있기 때문에 보다 제한적인 "1회 설정" 동작을 수동으로 구현해야 합니다.

 private AsyncOperationCompletedHandler<TResult> m_handler;
private int m_handlerSet;

public AsyncOperationCompletedHandler<TResult> Completed
{
    get { return m_handler; }
    set
    {
        if (value == null) 
            throw new ArgumentNullException("value");
        if (Interlocked.CompareExchange(ref m_handlerSet, 1, 0) != 0)
            throw new InvalidOperationException("Handler already set.");

        m_handler = value;

        var sc = SynchronizationContext.Current;
        m_task.ContinueWith(delegate {
            var handler = m_handler;
            m_handler = null;
            if (sc == null)
                handler(this, this.Status);
            else
                sc.Post(delegate { handler(this, this.Status); }, null);
       }, CancellationToken.None, 
          TaskContinuationOptions.ExecuteSynchronously, 
          TaskScheduler.Default);
    }
}

이것으로 AsAsyncOperation의 구현이 완료되었습니다. 이제 Task<TResult> 를 반환하는 메서드를 IAsyncOperation<TResult> 메서드로 사용할 수 있습니다.

AsyncInfo.Run

좀 더 복잡한 AsyncInfo.Run의 경우에는 어떨까요?

 public static IAsyncOperation<TResult> Run<TResult>(
        Func<CancellationToken,Task<TResult>> taskProvider);

Run 오버로드를 지원하려면 방금 만든 것과 같은 TaskAsAsyncOperationAdapter<TResult> 형식을 기존 구현 방법의 변화 없이 그대로 사용하면 됩니다. Task<TResult> 와 관련해서만 작동하는 것이 아니라 Func<CancellationToken,Task<TResult>> 와 관련해서도 작동하도록 기능을 확장하기만 하면 됩니다. 이러한 대리자가 제공되면 앞서 정의한 'm_canceler' CancellationTokenSource를 전달하고 반환되는 작업을 저장하는 방법으로 이 대리자를 동기식으로 호출하면 됩니다.

 internal class TaskAsAsyncOperationAdapter<TResult> : IAsyncOperation<TResult>
{
    private readonly Task<TResult> m_task;

    public TaskAsAsyncOperationAdapter(Task<TResult> task) { m_task = task; }

    public TaskAsAsyncOperationAdapter(
        Func<CancellationToken,Task<TResult>> func)
    {
        m_task = func(m_canceler.Token);
    }
    ...
}

public static class AsyncInfo
{
    public static IAsyncOperation<TResult> Run<TResult>(
        Func<CancellationToken, Task<TResult>> taskProvider)
    {
        return new TaskAsAsyncOperationAdapter<TResult>(taskProvider);
    }
    …
}

이 구현 예제는 구현 방법에 있어서 특별한 비밀이 없음을 잘 보여 줍니다. AsAsyncAction, AsAsyncOperation, AsyncInfo.Run은 모두 이 상용구를 직접 작성할 필요가 없도록 하는 구현 도우미일 뿐입니다.

결론

이제 AsAsyncAction, AsAsyncOperation, AsyncInfo.Run의 역할을 잘 이해했으리라 생각합니다. 그 역할이란 Task 또는 Task<TResult>IAsyncAction, IAsyncOperation<TResult> , IAsyncActionWithProgress<TProgress> 또는 IAsyncOperationWithProgress<TResult,TProgress> 으로 노출하는 것입니다. C#과 Visual Basic에서 asyncawait 키워드와 함께 사용하면 새로운 WinRT 비동기 작업을 관리 코드로 손쉽게 구현할 수 있습니다.

노출하려는 기능이 Task 또는 Task<TResult> 로 제공된다면, 변환을 수행할 때 WInRT 비동기 인터페이스를 수동으로 구현하는 대신 이러한 기본 제공 기능을 사용하는 것이 바람직합니다. 노출하려는 기능이 아직 Task 또는 Task<TResult> 로 제공되지 않을 경우에는 Task 또는 Task<TResult> 로 먼저 노출한 다음 기본 제공 변환을 사용해야 합니다. WinRT 비동기 인터페이스 구현과 관련한 모든 의미 체계를 정확하게 이해하기는 쉽지 않습니다. 이러한 변환 기능이 존재하는 것도 그 때문입니다. C++에서 WinRT 비동기 작업을 구현할 때도 Windows 런타임 라이브러리의 기본 AsyncBase 클래스 또는 PPL(Parallel Pattern Library)create_async 함수를 통해 비슷한 기능이 지원됩니다.

지금까지 살펴본 내용이 많은 도움이 되었기를 바랍니다.

- Visual Studio, Stephen Toub