작업 기반 비동기 패턴 구현Implementing the Task-based Asynchronous Pattern

TAP(작업 기반 비동기 패턴)는 세 가지 방식으로 구현할 수 있습니다. 즉 Visual Studio에서 C# 또는 Visual Basic 컴파일러를 사용하여 구현하거나, 수동으로 구현하거나, 컴파일러와 수동 방식을 함께 사용하여 구현할 수 있습니다.You can implement the Task-based Asynchronous Pattern (TAP) in three ways: by using the C# and Visual Basic compilers in Visual Studio, manually, or through a combination of the compiler and manual methods. 다음 섹션에서는 각 방법에 대해 자세히 설명합니다.The following sections discuss each method in detail. TAP 패턴을 사용하면 계산 바운드 및 I/O 바운드 비동기 작업을 모두 구현할 수 있습니다.You can use the TAP pattern to implement both compute-bound and I/O-bound asynchronous operations. 각 작업 유형에 대해서는 워크로드 섹션에서 설명합니다.The Workloads section discusses each type of operation.

TAP 메서드 생성Generating TAP methods

컴파일러 사용Using the compilers

.NET Framework 4.5부터는 async 키워드(Visual Basic의 경우 Async)로 특성이 지정된 메서드가 비동기 메서드로 간주되며, C# 및 Visual Basic 컴파일러는 TAP를 사용해 비동기로 메서드를 구현하는 데 필요한 변환을 수행합니다.Starting with .NET Framework 4.5, any method that is attributed with the async keyword (Async in Visual Basic) is considered an asynchronous method, and the C# and Visual Basic compilers perform the necessary transformations to implement the method asynchronously by using TAP. 비동기 메서드는 System.Threading.Tasks.Task 또는 System.Threading.Tasks.Task<TResult> 개체를 반환해야 합니다.An asynchronous method should return either a System.Threading.Tasks.Task or a System.Threading.Tasks.Task<TResult> object. 두 번째 개체의 경우 함수 본문은 TResult를 반환해야 하며 컴파일러는 결과 작업 개체를 통해 이 결과가 제공되는지를 확인합니다.For the latter, the body of the function should return a TResult, and the compiler ensures that this result is made available through the resulting task object. 마찬가지로 메서드 본문 내에서 처리되지 않는 모든 예외는 출력 작업으로 마샬링되므로 결과 작업이 TaskStatus.Faulted 상태로 종료됩니다.Similarly, any exceptions that go unhandled within the body of the method are marshaled to the output task and cause the resulting task to end in the TaskStatus.Faulted state. 단, OperationCanceledException 또는 파생 형식이 처리되지 않는 경우에는 결과 작업이 TaskStatus.Canceled 상태로 종료됩니다.The exception is when an OperationCanceledException (or derived type) goes unhandled, in which case the resulting task ends in the TaskStatus.Canceled state.

수동으로 TAP 메서드 생성Generating TAP methods manually

구현을 보다 효율적으로 제어하기 위해 TAP 패턴을 수동으로 구현할 수 있습니다.You may implement the TAP pattern manually for better control over implementation. 컴파일러는 System.Threading.Tasks 네임스페이스의 지원 형식 및 System.Runtime.CompilerServices 네임스페이스에서 노출되는 공개 노출 영역을 사용합니다.The compiler relies on the public surface area exposed from the System.Threading.Tasks namespace and supporting types in the System.Runtime.CompilerServices namespace. TAP를 직접 구현하려면 TaskCompletionSource<TResult> 개체를 만들고 비동기 작업을 수행한 다음 작업이 완료되면 SetResult, SetException 또는 SetCanceled 메서드나 이러한 메서드 중 하나의 Try 버전을 호출합니다.To implement the TAP yourself, you create a TaskCompletionSource<TResult> object, perform the asynchronous operation, and when it completes, call the SetResult, SetException, or SetCanceled method, or the Try version of one of these methods. TAP 메서드를 수동으로 구현할 때는 표시된 비동기 작업이 완료되면 결과 작업을 완료해야 합니다.When you implement a TAP method manually, you must complete the resulting task when the represented asynchronous operation completes. 예:For example:

public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
    var tcs = new TaskCompletionSource<int>();
    stream.BeginRead(buffer, offset, count, ar =>
    {
        try { tcs.SetResult(stream.EndRead(ar)); }
        catch (Exception exc) { tcs.SetException(exc); }
    }, state);
    return tcs.Task;
}
<Extension()>
Public Function ReadTask(stream As Stream, buffer() As Byte, 
                         offset As Integer, count As Integer, 
                         state As Object) As Task(Of Integer)
    Dim tcs As New TaskCompletionSource(Of Integer)()
    stream.BeginRead(buffer, offset, count, Sub(ar)
               Try  
                  tcs.SetResult(stream.EndRead(ar)) 
               Catch exc As Exception 
                  tcs.SetException(exc)
               End Try
            End Sub, state)
    Return tcs.Task
End Function   

혼합 방식Hybrid approach

TAP 패턴은 수동으로 구현하되 구현의 핵심 논리를 컴파일러에 위임하면 유용할 수 있습니다.You may find it useful to implement the TAP pattern manually but to delegate the core logic for the implementation to the compiler. 예를 들어 예외가 System.Threading.Tasks.Task 개체를 통해 노출되는 대신 메서드의 직접 호출자로 이스케이프될 수 있도록 컴파일러 생성 비동기 메서드 외부에서 인수를 확인하려는 경우 혼합 방식을 사용할 수 있습니다.For example, you may want to use the hybrid approach when you want to verify arguments outside a compiler-generated asynchronous method so that exceptions can escape to the method’s direct caller rather than being exposed through the System.Threading.Tasks.Task object:

public Task<int> MethodAsync(string input)
{
    if (input == null) throw new ArgumentNullException("input");
    return MethodAsyncInternal(input);
}

private async Task<int> MethodAsyncInternal(string input)
{

   // code that uses await goes here

   return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
    If input Is Nothing Then Throw New ArgumentNullException("input")

    Return MethodAsyncInternal(input)
End Function

Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)

   ' code that uses await goes here
   
   return value
End Function

또한 빠른 경로 최적화를 구현하며 캐시된 작업을 반환하려는 경우에도 이러한 위임이 유용합니다.Another case where such delegation is useful is when you're implementing fast-path optimization and want to return a cached task.

워크로드Workloads

컴퓨트 바운드 및 I/O 바운드 비동기 작업을 모두 TAP 메서드로 구현할 수 있습니다.You may implement both compute-bound and I/O-bound asynchronous operations as TAP methods. 그러나 TAP 메서드를 라이브러리에서 공개적으로 노출할 때는 I/O 바운드 연산이 포함된 작업에만 해당 메서드를 제공해야 합니다. 이러한 작업은 계산도 포함할 수 있지만 순수한 계산 작업이어서는 안 됩니다.However, when TAP methods are exposed publicly from a library, they should be provided only for workloads that involve I/O-bound operations (they may also involve computation, but should not be purely computational). 순수한 계산 바운드 메서드의 경우에는 동기 구현으로만 표시되어야 합니다.If a method is purely compute-bound, it should be exposed only as a synchronous implementation. 그러면 해당 메서드를 사용하는 코드는 작업을 다른 스레드로 오프로드하거나 병렬 처리 기능을 제공하기 위해 해당 동기 메서드의 호출을 작업으로 래핑할지 여부를 선택할 수 있습니다.The code that consumes it may then choose whether to wrap an invocation of that synchronous method into a task to offload the work to another thread or to achieve parallelism. 그리고 메서드가 I/O 바운드인 경우에는 비동기 구현으로만 표시되어야 합니다.And if a method is I/O-bound, it should be exposed only as an asynchronous implementation.

계산 바운드 작업Compute-bound tasks

System.Threading.Tasks.Task 클래스는 계산을 많이 수행하는 작업에 가장 적합합니다.The System.Threading.Tasks.Task class is ideally suited for representing computationally intensive operations. 이 클래스는 기본적으로 ThreadPool 클래스 내의 특수 지원을 활용하여 효율적 실행을 제공하며 비동기 계산 실행 시기, 위치 및 방법에 대한 상당한 제어 기능도 제공합니다.By default, it takes advantage of special support within the ThreadPool class to provide efficient execution, and it also provides significant control over when, where, and how asynchronous computations execute.

다음과 같은 방법으로 컴퓨트 바인딩 작업을 생성할 수 있습니다.You can generate compute-bound tasks in the following ways:

  • .NET Framework 4에서 TaskFactory.StartNew 메서드를 사용합니다. 이 메서드는 비동기식으로 실행할 대리자(일반적으로 Action<T> 또는 Func<TResult>)를 수락합니다.In the .NET Framework 4, use the TaskFactory.StartNew method, which accepts a delegate (typically an Action<T> or a Func<TResult>) to be executed asynchronously. Action<T> 대리자를 제공하는 경우 메서드는 해당 대리자의 비동기 실행을 나타내는 System.Threading.Tasks.Task 개체를 반환합니다.If you provide an Action<T> delegate, the method returns a System.Threading.Tasks.Task object that represents the asynchronous execution of that delegate. Func<TResult> 대리자를 제공하는 경우 메서드는 System.Threading.Tasks.Task<TResult> 개체를 반환합니다.If you provide a Func<TResult> delegate, the method returns a System.Threading.Tasks.Task<TResult> object. StartNew 메서드의 오버로드는 취소 토큰(CancellationToken), 작업 만들기 옵션(TaskCreationOptions) 및 작업 스케줄러(TaskScheduler)를 수락합니다. 이러한 모든 항목은 작업 예약과 실행에 대한 세분화된 제어 기능을 제공합니다.Overloads of the StartNew method accept a cancellation token (CancellationToken), task creation options (TaskCreationOptions), and a task scheduler (TaskScheduler), all of which provide fine-grained control over the scheduling and execution of the task. 현재 작업 스케줄러를 대상으로 하는 팩터리 인스턴스는 Factory 클래스의 정적 속성(Task)으로 사용 가능합니다. 예를 들면 Task.Factory.StartNew(…)와 같습니다.A factory instance that targets the current task scheduler is available as a static property (Factory) of the Task class; for example: Task.Factory.StartNew(…).

  • .NET Framework 4.5 이상 버전(.NET Core 및 .NET Standard 포함)에서는 정적 Task.Run 메서드를 TaskFactory.StartNew의 바로 가기로 사용합니다.In the .NET Framework 4.5 and later versions (including .NET Core and .NET Standard), use the static Task.Run method as a shortcut to TaskFactory.StartNew. Run을 사용하여 스레드 풀을 대상으로 하는 컴퓨트 바운드 작업을 쉽게 시작할 수 있습니다.You may use Run to easily launch a compute-bound task that targets the thread pool. .NET Framework 4.5 이상 버전에서는 기본적으로 이 메커니즘을 통해 계산 바운드 작업을 시작합니다.In the .NET Framework 4.5 and later versions, this is the preferred mechanism for launching a compute-bound task. 작업에 대해 더욱 세분화된 제어가 필요할 때만 StartNew를 직접 사용합니다.Use StartNew directly only when you want more fine-grained control over the task.

  • 작업을 개별적으로 생성하고 예약하려는 경우 Task 형식 또는 Start 메서드의 생성자를 사용합니다.Use the constructors of the Task type or the Start method if you want to generate and schedule the task separately. 공용 메서드는 이미 시작된 작업만 반환해야 합니다.Public methods must only return tasks that have already been started.

  • Task.ContinueWith 메서드의 오버로드를 사용합니다.Use the overloads of the Task.ContinueWith method. 이 메서드는 다른 작업이 완료되면 예약된 새 작업을 만듭니다.This method creates a new task that is scheduled when another task completes. 일부 ContinueWith 오버로드는 연속 작업 예약과 실행을 효율적으로 제어할 수 있도록 취소 토큰, 연속 옵션 및 작업 스케줄러를 수락합니다.Some of the ContinueWith overloads accept a cancellation token, continuation options, and a task scheduler for better control over the scheduling and execution of the continuation task.

  • TaskFactory.ContinueWhenAllTaskFactory.ContinueWhenAny 메서드를 사용합니다.Use the TaskFactory.ContinueWhenAll and TaskFactory.ContinueWhenAny methods. 이러한 메서드는 제공된 작업 집합이 모두 완료되거나 하나라도 완료되면 예약된 새 작업을 만듭니다.These methods create a new task that is scheduled when all or any of a supplied set of tasks completes. 이러한 메서드는 작업 예약과 실행을 제어하기 위한 오버로드도 제공합니다.These methods also provide overloads to control the scheduling and execution of these tasks.

컴퓨트 바운드 작업에서 시스템은 작업 실행을 시작하기 전에 취소 요청을 받으면 예약된 작업 실행을 차단할 수 있습니다.In compute-bound tasks, the system can prevent the execution of a scheduled task if it receives a cancellation request before it starts running the task. 따라서 취소 토큰(CancellationToken 개체)을 제공하는 경우 토큰을 모니터링하는 비동기 코드로 해당 토큰을 전달할 수 있습니다.As such, if you provide a cancellation token (CancellationToken object), you can pass that token to the asynchronous code that monitors the token. 또한 StartNew 런타임도 토큰을 모니터링할 수 있도록 Run 또는 Task과 같은 앞에서 설명한 메서드 중 하나에 토큰을 제공할 수도 있습니다.You can also provide the token to one of the previously mentioned methods such as StartNew or Run so that the Task runtime may also monitor the token.

예를 들어 이미지를 렌더링하는 비동기 메서드의 경우를 고려해 보겠습니다.For example, consider an asynchronous method that renders an image. 작업 본문은 렌더링 중에 취소 요청이 도착하는 경우 코드를 일찍 종료할 수 있도록 취소 토큰을 폴링할 수 있습니다.The body of the task can poll the cancellation token so that the code may exit early if a cancellation request arrives during rendering. 또한 렌더링을 시작하기 전에 취소 요청이 도착하면 렌더링 작업을 차단할 수 있습니다.In addition, if the cancellation request arrives before rendering starts, you'll want to prevent the rendering operation:

internal Task<Bitmap> RenderAsync(
              ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for(int y=0; y<data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for(int x=0; x<data.Width; x++)
            {
                // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As _
                            CancellationToken) As Task(Of Bitmap)
    Return Task.Run( Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 to data.Height - 1
                           cancellationToken.ThrowIfCancellationRequested()
                           For x As Integer = 0 To data.Width - 1
                             ' render pixel [x,y] into bmp
                           Next
                        Next
                        Return bmp
                     End Function, cancellationToken)
End Function

컴퓨트 바운드 작업은 다음 조건 중 하나 이상에 해당하는 경우 Canceled에서 종료됩니다.Compute-bound tasks end in a Canceled state if at least one of the following conditions is true:

  • 작업이 CancellationToken 상태로 전환되기 전에 StartNew 또는 Run과 같은 creation 메서드에 대한 인수로 제공되는 Running 개체를 통해 취소 요청이 도착하는 경우A cancellation request arrives through the CancellationToken object, which is provided as an argument to the creation method (for example, StartNew or Run) before the task transitions to the Running state.

  • OperationCanceledException 예외가 작업의 본문 내에서 처리되지 않고, 작업으로 전달되는 것과 같은 CancellationToken이 예외에 포함되어 있으며, 토큰에 취소가 요청되었음이 표시되는 경우An OperationCanceledException exception goes unhandled within the body of such a task, that exception contains the same CancellationToken that is passed to the task, and that token shows that cancellation is requested.

다른 예외가 작업의 본문 내에서 처리되지 않는 경우 작업은 Faulted 상태로 종료되며 작업에서 대기하거나 결과에 액세스하려고 하면 예외가 throw됩니다.If another exception goes unhandled within the body of the task, the task ends in the Faulted state, and any attempts to wait on the task or access its result causes an exception to be thrown.

I/O 바운드 작업I/O-bound tasks

스레드를 통해 전체 실행을 직접 지원해서는 안 되는 작업을 만들려면 TaskCompletionSource<TResult> 형식을 사용합니다.To create a task that should not be directly backed by a thread for the entirety of its execution, use the TaskCompletionSource<TResult> type. 이 형식은 연결된 Task 인스턴스를 반환하는 Task<TResult> 속성을 노출합니다.This type exposes a Task property that returns an associated Task<TResult> instance. 이 작업의 수명 주기는 TaskCompletionSource<TResult>, SetResult, SetException 등의 SetCanceled 메서드와 해당 TrySet 변형을 통해 제어됩니다.The life cycle of this task is controlled by TaskCompletionSource<TResult> methods such as SetResult, SetException, SetCanceled, and their TrySet variants.

지정된 시간이 지나면 완료되는 작업을 만든다고 가정해 보겠습니다.Let's say that you want to create a task that will complete after a specified period of time. 예를 들어 사용자 인터페이스에서 작업을 지연시킬 수 있습니다.For example, you may want to delay an activity in the user interface. System.Threading.Timer 클래스는 지정된 시간이 지나면 대리자를 비동기식으로 호출하는 기능을 이미 제공합니다. TaskCompletionSource<TResult>를 사용하면 다음과 같이 타이머 앞에 Task<TResult>를 배치할 수 있습니다.The System.Threading.Timer class already provides the ability to asynchronously invoke a delegate after a specified period of time, and by using TaskCompletionSource<TResult> you can put a Task<TResult> front on the timer, for example:

   public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
   {
       TaskCompletionSource<DateTimeOffset> tcs = null;
       Timer timer = null;

       timer = new Timer(delegate
       {
           timer.Dispose();
           tcs.TrySetResult(DateTimeOffset.UtcNow);
       }, null, Timeout.Infinite, Timeout.Infinite);

       tcs = new TaskCompletionSource<DateTimeOffset>(timer);
       timer.Change(millisecondsTimeout, Timeout.Infinite);
       return tcs.Task;
   }
   Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset) 
       Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
       Dim timer As Timer = Nothing
       
       timer = New Timer( Sub(obj)
                             timer.Dispose()
                             tcs.TrySetResult(DateTimeOffset.UtcNow)
                          End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

       tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
       timer.Change(millisecondsTimeout, Timeout.Infinite)
       Return tcs.Task
   End Function

.NET Framework 4.5부터는 이러한 용도로 Task.Delay 메서드가 제공되며, 예를 들어 다른 비동기 메서드 내에서 해당 메서드를 사용하여 비동기 폴링 루프를 구현할 수 있습니다.Starting with the .NET Framework 4.5, the Task.Delay method is provided for this purpose, and you can use it inside another asynchronous method, for example, to implement an asynchronous polling loop:

public static async Task Poll(Uri url, CancellationToken cancellationToken, 
                              IProgress<bool> progress)
{
    while(true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken, 
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            await DownloadStringAsync(url)
            success = true
        Catch
           ' ignore errors
        End Try   
        progress.Report(success)
    Loop
End Function

TaskCompletionSource<TResult> 클래스에는 상응하는 제네릭이 아닌 클래스가 없습니다.The TaskCompletionSource<TResult> class doesn't have a non-generic counterpart. 그러나 Task<TResult>Task에서 파생되므로 작업만 반환하는 I/O 바운드 메서드에 대해 제네릭 TaskCompletionSource<TResult> 개체를 사용할 수 있습니다.However, Task<TResult> derives from Task, so you can use the generic TaskCompletionSource<TResult> object for I/O-bound methods that simply return a task. 이렇게 하려면 더미 TResult가 포함된 소스를 사용할 수 있습니다. (Boolean을 사용하는 것이 좋기는 하지만, Task 사용자가 해당 항목을 Task<TResult>로 다운캐스트할 수도 있는 경우에는 개인 TResult 형식을 대신 사용할 수 있습니다.To do this, you can use a source with a dummy TResult (Boolean is a good default choice, but if you're concerned about the user of the Task downcasting it to a Task<TResult>, you can use a private TResult type instead). 예를 들어 이전 예제의 Delay 메서드는 결과 오프셋(Task<DateTimeOffset>)과 함께 현재 시간을 반환합니다.For example, the Delay method in the previous example returns the current time along with the resulting offset (Task<DateTimeOffset>). 이러한 결과 값이 필요하지 않다면 대신 메서드를 다음과 같이 코딩할 수 있습니다. 이 경우 반환 값이 변경되며 인수는 TrySetResult로 변경됩니다.If such a result value is unnecessary, the method could instead be coded as follows (note the change of return type and the change of argument to TrySetResult):

  public static Task<bool> Delay(int millisecondsTimeout)
  {
       TaskCompletionSource<bool> tcs = null;
       Timer timer = null;

       timer = new Timer(delegate
       {
           timer.Dispose();
           tcs.TrySetResult(true);
       }, null, Timeout.Infinite, Timeout.Infinite);

       tcs = new TaskCompletionSource<bool>(timer);
       timer.Change(millisecondsTimeout, Timeout.Infinite);
       return tcs.Task;
  }
  Public Function Delay(millisecondsTimeout As Integer) As Task(Of Boolean)
       Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
       Dim timer As Timer = Nothing

       Timer = new Timer( Sub(obj)
                             timer.Dispose()
                             tcs.TrySetResult(True)
                          End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

       tcs = New TaskCompletionSource(Of Boolean)(timer)
       timer.Change(millisecondsTimeout, Timeout.Infinite)
       Return tcs.Task
  End Function

계산 바운드 및 I/O 바운드 혼합 작업Mixed compute-bound and I/O-bound tasks

비동기 메서드에서는 컴퓨트 바운드 또는 I/O 바운드 작업 중 하나만 사용할 수 있는 것이 아니라 두 작업을 혼합하여 사용할 수도 있습니다.Asynchronous methods are not limited to just compute-bound or I/O-bound operations but may represent a mixture of the two. 실제로 여러 비동기 작업이 큰 혼합 작업으로 결합되는 경우가 많습니다.In fact, multiple asynchronous operations are often combined into larger mixed operations. 예를 들어, 일부 입력 RenderAsync를 기반으로 이미지를 렌더링하기 위해 계산을 많이 하는 작업을 수행한 이전의 예에 있는 imageData 메서드를 고려합니다.For example, the RenderAsync method in a previous example performed a computationally intensive operation to render an image based on some input imageData. imageData는 비동기 방식으로 액세스하는 웹 서비스에서 제공된 것일 수 있습니다.This imageData could come from a web service that you asynchronously access:

public async Task<Bitmap> DownloadDataAndRenderImageAsync(
    CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(
             cancellationToken As CancellationToken) As Task(Of Bitmap) 
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

또한 이 예제에서는 여러 비동기 작업을 통해 단일 취소 토큰 스레드를 만드는 방법도 보여 줍니다.This example also demonstrates how a single cancellation token may be threaded through multiple asynchronous operations. 자세한 내용은 작업 기반 비동기 패턴 사용의 취소 사용 섹션을 참조하세요.For more information, see the cancellation usage section in Consuming the Task-based Asynchronous Pattern.

참고 항목See also