연속 작업을 사용하여 작업 연결Chaining tasks using continuation tasks

비동기 프로그래밍에서는 한 비동기 작업이 완료 시 두 번째 작업을 호출하는 것이 일반적입니다.In asynchronous programming, it's common for one asynchronous operation, on completion, to invoke a second operation. 연속 작업을 사용하면 후행 작업이 첫 번째 작업의 결과를 사용할 수 있습니다.Continuations allow decedent operations to consume the results of the first operation. 일반적으로 연속 작업은 콜백 메서드를 통해 수행되었습니다.Traditionally, continuations have been done by using callback methods. 작업 병렬 라이브러리에서는 연속 작업 이 동일한 기능을 제공합니다.In the Task Parallel Library, the same functionality is provided by continuation tasks. 연속 작업(연속이라고도 함)은 선행 작업이라고도 하는 다른 작업이 완료 시 호출하는 비동기 작업입니다.A continuation task (also known just as a continuation) is an asynchronous task that's invoked by another task, known as the antecedent, when the antecedent finishes.

연속은 비교적 사용이 용이하지만 강력하고 유연합니다.Continuations are relatively easy to use, but are nevertheless powerful and flexible. 예를 들어 다음 작업을 할 수 있습니다.For example, you can:

  • 선행 작업의 데이터를 연속 작업에 전달합니다.Pass data from the antecedent to the continuation.
  • 연속 작업이 호출되거나 호출되지 않는 정확한 조건을 지정합니다.Specify the precise conditions under which the continuation will be invoked or not invoked.
  • 시작되기 전이나 실행 중일 때 함께 연속 작업을 취소합니다.Cancel a continuation either before it starts or cooperatively as it is running.
  • 연속 작업을 예약하는 방법에 대한 힌트를 제공합니다.Provide hints about how the continuation should be scheduled.
  • 동일한 선행 작업에서 여러 개의 연속 작업을 호출합니다.Invoke multiple continuations from the same antecedent.
  • 여러 선행 작업 중 하나 또는 모두가 완료되면 하나의 연속 작업을 호출합니다.Invoke one continuation when all or any one of multiple antecedents complete.
  • 연속 작업을 임의 길이까지 차례로 연결합니다.Chain continuations one after another to any arbitrary length.
  • 연속 작업을 사용하여 선행 작업에서 발생한 예외를 처리합니다.Use a continuation to handle exceptions thrown by the antecedent.

연속 작업 정보About continuations

연속 작업은 WaitingForActivation 상태로 만들어지는 작업입니다.A continuation is a task that is created in the WaitingForActivation state. 선행 작업이 완료되면 자동으로 활성화됩니다.It is activated automatically when its antecedent task or tasks complete. 사용자 코드에서 연속 작업에 대해 Task.Start 를 호출하면 System.InvalidOperationException 예외가 발생합니다.Calling Task.Start on a continuation in user code throws an System.InvalidOperationException exception.

연속 작업 자체는 Task 이며 작업이 시작된 스레드를 차단하지 않습니다.A continuation is itself a Task and does not block the thread on which it is started. 연속 작업이 완료될 때까지 차단하려면 Task.Wait 메서드를 호출합니다.Call the Task.Wait method to block until the continuation task finishes.

단일 선행 작업에 대한 연속 작업 만들기Create a continuation for a single antecedent

Task.ContinueWith 메서드를 호출하여 선행 작업이 완료되었을 때 실행되는 연속 작업을 만듭니다.You create a continuation that executes when its antecedent has completed by calling the Task.ContinueWith method. 다음 예제에서는 기본 패턴을 보여줍니다(이해하기 쉽도록 예외 처리는 생략됨).The following example shows the basic pattern (for clarity, exception handling is omitted). 현재 요일의 이름을 나타내는 taskA개체를 반환하는 선행 작업 DayOfWeek 를 실행합니다.It executes an antecedent task, taskA, that returns a DayOfWeek object that indicates the name of the current day of the week. 선행 작업이 완료되면 연속 작업 continuation에 선행 작업이 전달되고 해당 결과를 포함하는 문자열을 표시합니다.When the antecedent completes, the continuation task, continuation, is passed the antecedent and displays a string that includes its result.

참고

이 문서의 C# 샘플은 Main 메서드에서 async 한정자를 사용합니다.The C# samples in this article make use of the async modifier on the Main method. 해당 기능은 C# 7.1 이상에서 사용할 수 있습니다.That feature is available in C# 7.1 and later. 이전 버전은 이 샘플 코드를 컴파일하는 경우 CS5001을 생성합니다.Previous versions generate CS5001 when compiling this sample code. 언어 버전을 C# 7.1 이상으로 설정해야 합니다.You'll need to set the language version to C# 7.1 or newer. 언어 버전 구성에 관한 문서에서 언어 버전을 구성하는 방법을 알아볼 수 있습니다.You can learn how to configure the language version in the article on configure language version.

using System;
using System.Threading.Tasks;

public class SimpleExample
{
    public static async Task Main()
    {
        // Declare, assign, and start the antecedent task.
        Task<DayOfWeek> taskA = Task.Run(() => DateTime.Today.DayOfWeek);

        // Execute the continuation when the antecedent finishes.
        await taskA.ContinueWith(antecedent => Console.WriteLine($"Today is {antecedent.Result}."));
    }
}
// The example displays the following output:
//       Today is Monday.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        ' Execute the antecedent.
        Dim taskA As Task(Of DayOfWeek) = Task.Run(Function() DateTime.Today.DayOfWeek)

        ' Execute the continuation when the antecedent finishes.
        Dim continuation As Task = taskA.ContinueWith(Sub(antecedent)
                                                          Console.WriteLine("Today is {0}.", antecedent.Result)
                                                      End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays output like the following output:
'       Today is Monday.

여러 선행 작업에 대한 연속 작업 만들기Create a continuation for multiple antecedents

작업 그룹 중 하나 또는 모두가 완료되었을 때 실행되는 연속 작업을 만들 수도 있습니다.You can also create a continuation that will run when any or all of a group of tasks has completed. 모든 선행 작업이 완료되었을 때 연속 작업을 실행하려면 static(Visual Basic에서는Shared ) Task.WhenAll 메서드 또는 인스턴스 TaskFactory.ContinueWhenAll 메서드를 호출합니다.To execute a continuation when all antecedent tasks have completed, you call the static (Shared in Visual Basic) Task.WhenAll method or the instance TaskFactory.ContinueWhenAll method. 선행 작업 중 하나가 완료되었을 때 연속 작업을 실행하려면 static(Visual Basic에서는Shared ) Task.WhenAny 메서드 또는 인스턴스 TaskFactory.ContinueWhenAny 메서드를 호출합니다.To execute a continuation when any of the antecedent tasks has completed, you call the static (Shared in Visual Basic) Task.WhenAny method or the instance TaskFactory.ContinueWhenAny method.

Task.WhenAllTask.WhenAny 오버로드 호출은 호출 스레드를 차단하지 않습니다.Calls to the Task.WhenAll and Task.WhenAny overloads do not block the calling thread. 그러나 일반적으로는 Task.WhenAll(IEnumerable<Task>)Task.WhenAll(Task[]) 메서드를 제외하고 모두 호출하여 반환된 Task<TResult>.Result 속성을 검색하므로, 호출 스레드가 차단됩니다.However, you typically call all but the Task.WhenAll(IEnumerable<Task>) and Task.WhenAll(Task[]) methods to retrieve the returned Task<TResult>.Result property, which does block the calling thread.

다음 예제에서는 Task.WhenAll(IEnumerable<Task>) 메서드를 호출하여 10개 선행 작업의 결과를 반영하는 연속 작업을 만듭니다.The following example calls the Task.WhenAll(IEnumerable<Task>) method to create a continuation task that reflects the results of its 10 antecedent tasks. 각 선행 작업은 1에서 10까지의 인덱스 값을 제곱합니다.Each antecedent task squares an index value that ranges from one to 10. 선행 작업이 성공적으로 완료될 경우( Task.Status 속성이 TaskStatus.RanToCompletion임) 연속 작업의 Task<TResult>.Result 속성은 각 선행 작업에서 반환된 Task<TResult>.Result 값의 배열입니다.If the antecedents complete successfully (their Task.Status property is TaskStatus.RanToCompletion), the Task<TResult>.Result property of the continuation is an array of the Task<TResult>.Result values returned by each antecedent. 예제에서는 값을 더하여 1과 10 사이의 모든 숫자의 제곱 합계를 컴퓨팅합니다.The example adds them to compute the sum of squares for all numbers between one and 10.

using System.Collections.Generic;
using System;
using System.Threading.Tasks;

public class WhenAllExample
{
    public static async Task Main()
    {
        var tasks = new List<Task<int>>();
        for (int ctr = 1; ctr <= 10; ctr++)
        {
            int baseValue = ctr;
            tasks.Add(Task.Factory.StartNew(b => (int)b * (int)b, baseValue));
        }

        var results = await Task.WhenAll(tasks);

        int sum = 0;
        for (int ctr = 0; ctr <= results.Length - 1; ctr++)
        {
            var result = results[ctr];
            Console.Write($"{result} {((ctr == results.Length - 1) ? "=" : "+")} ");
            sum += result;
        }

        Console.WriteLine(sum);
    }
}
// The example displays the similar output:
//    1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100 = 385
Imports System.Collections.Generic
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim tasks As New List(Of Task(Of Integer))()
        For ctr As Integer = 1 To 10
            Dim baseValue As Integer = ctr
            tasks.Add(Task.Factory.StartNew(Function(b)
                                                Dim i As Integer = CInt(b)
                                                Return i * i
                                            End Function, baseValue))
        Next
        Dim continuation = Task.WhenAll(tasks)

        Dim sum As Long = 0
        For ctr As Integer = 0 To continuation.Result.Length - 1
            Console.Write("{0} {1} ", continuation.Result(ctr),
                          If(ctr = continuation.Result.Length - 1, "=", "+"))
            sum += continuation.Result(ctr)
        Next
        Console.WriteLine(sum)
    End Sub
End Module
' The example displays the following output:
'       1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100 = 385

연속 옵션Continuation options

단일 작업 연속을 만드는 경우 ContinueWith 열거형 값을 받는 System.Threading.Tasks.TaskContinuationOptions 오버로드를 사용하여 연속 작업이 시작되는 조건을 지정할 수 있습니다.When you create a single-task continuation, you can use a ContinueWith overload that takes a System.Threading.Tasks.TaskContinuationOptions enumeration value to specify the conditions under which the continuation starts. 예를 들어 선행 작업이 성공적으로 완료되거나 오류 상태로 완료되는 경우에만 연속 작업이 실행되도록 지정할 수 있습니다.For example, you can specify that the continuation is to run only if the antecedent completes successfully, or only if it completes in a faulted state. 선행 작업이 연속 작업을 호출할 준비가 되었을 때 조건이 true가 아니면 연속 작업이 TaskStatus.Canceled 상태로 바로 전환되며 그 후에 시작할 수 없습니다.If the condition is not true when the antecedent is ready to invoke the continuation, the continuation transitions directly to the TaskStatus.Canceled state and subsequently cannot be started.

TaskFactory.ContinueWhenAll 메서드 오버로드와 같은 많은 다중 작업 연속 메서드에는 System.Threading.Tasks.TaskContinuationOptions 매개 변수도 포함됩니다.A number of multi-task continuation methods, such as overloads of the TaskFactory.ContinueWhenAll method, also include a System.Threading.Tasks.TaskContinuationOptions parameter. 그러나 모든 System.Threading.Tasks.TaskContinuationOptions 열거형 멤버의 하위 집합만 유효합니다.Only a subset of all System.Threading.Tasks.TaskContinuationOptions enumeration members are valid, however. System.Threading.Tasks.TaskContinuationOptions 열거형에 해당 항목이 있는 System.Threading.Tasks.TaskCreationOptions 값(예: TaskContinuationOptions.AttachedToParent, TaskContinuationOptions.LongRunningTaskContinuationOptions.PreferFairness)을 지정할 수 있습니다.You can specify System.Threading.Tasks.TaskContinuationOptions values that have counterparts in the System.Threading.Tasks.TaskCreationOptions enumeration, such as TaskContinuationOptions.AttachedToParent, TaskContinuationOptions.LongRunning, and TaskContinuationOptions.PreferFairness. 다중 작업 연속에 NotOn 또는 OnlyOn 옵션을 지정하는 경우 런타임에 ArgumentOutOfRangeException 예외가 발생합니다.If you specify any of the NotOn or OnlyOn options with a multi-task continuation, an ArgumentOutOfRangeException exception will be thrown at run time.

작업 연속 옵션에 대한 자세한 내용은 TaskContinuationOptions 항목을 참조하세요.For more information on task continuation options, see the TaskContinuationOptions topic.

연속 작업에 데이터 전달Pass data to a continuation

Task.ContinueWith 메서드는 연속 작업의 사용자 대리자에게 선행 작업에 대한 참조를 인수로 전달합니다.The Task.ContinueWith method passes a reference to the antecedent to the user delegate of the continuation as an argument. 선행 작업이 System.Threading.Tasks.Task<TResult> 개체이고 작업이 완료될 때까지 실행된 경우 연속 작업이 해당 작업의 Task<TResult>.Result 속성에 액세스할 수 있습니다.If the antecedent is a System.Threading.Tasks.Task<TResult> object, and the task ran until it was completed, then the continuation can access the Task<TResult>.Result property of the task.

Task<TResult>.Result 속성은 작업이 완료될 때까지 차단됩니다.The Task<TResult>.Result property blocks until the task has completed. 그러나 작업이 취소되거나 오류가 발생한 경우 Result 속성에 액세스하려고 하면 AggregateException 예외가 발생합니다.However, if the task was canceled or faulted, attempting to access the Result property throws an AggregateException exception. 다음 예제와 같이 OnlyOnRanToCompletion 옵션을 사용하여 이 문제를 방지할 수 있습니다.You can avoid this problem by using the OnlyOnRanToCompletion option, as shown in the following example.

using System;
using System.Threading.Tasks;

public class ResultExample
{
    public static async Task Main()
    {
       var task = Task.Run(
           () =>
           {
                DateTime date = DateTime.Now;
                return date.Hour > 17
                    ? "evening"
                    : date.Hour > 12
                        ? "afternoon"
                        : "morning";
            });
        
        await task.ContinueWith(
            antecedent =>
            {
                Console.WriteLine($"Good {antecedent.Result}!");
                Console.WriteLine($"And how are you this fine {antecedent.Result}?");
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
   }
}
// The example displays the similar output:
//       Good afternoon!
//       And how are you this fine afternoon?
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim dat As DateTime = DateTime.Now
                             If dat = DateTime.MinValue Then
                                 Throw New ArgumentException("The clock is not working.")
                             End If

                             If dat.Hour > 17 Then
                                 Return "evening"
                             Else If dat.Hour > 12 Then
                                 Return "afternoon"
                             Else
                                 Return "morning"
                             End If
                         End Function)
        Dim c = t.ContinueWith(Sub(antecedent)
                                   Console.WriteLine("Good {0}!",
                                                     antecedent.Result)
                                   Console.WriteLine("And how are you this fine {0}?",
                                                     antecedent.Result)
                               End Sub, TaskContinuationOptions.OnlyOnRanToCompletion)
        c.Wait()
    End Sub
End Module
' The example displays output like the following:
'       Good afternoon!
'       And how are you this fine afternoon?

선행 작업이 성공적으로 완료될 때까지 실행되지 않은 경우에도 연속 작업을 실행하려면 예외로부터 보호해야 합니다.If you want the continuation to run even if the antecedent did not run to successful completion, you must guard against the exception. 한 가지 방법은 선행 작업의 Task.Status 속성을 테스트하고 상태가 Result 또는 Faulted 가 아닌 경우에만 Canceled속성에 액세스하는 것입니다.One approach is to test the Task.Status property of the antecedent, and only attempt to access the Result property if the status is not Faulted or Canceled. 선행 작업의 Exception 속성을 검사할 수도 있습니다.You can also examine the Exception property of the antecedent. 자세한 내용은 예외 처리를 참조하세요.For more information, see Exception Handling. 다음 예제에서는 상태가 Task<TResult>.Result 인 경우에만 선행 작업의 TaskStatus.RanToCompletion속성에 액세스하도록 이전 예제를 수정합니다.The following example modifies the previous example to access antecedent's Task<TResult>.Result property only if its status is TaskStatus.RanToCompletion.

using System;
using System.Threading.Tasks;

public class ResultTwoExample
{
    public static async Task Main() =>
        await Task.Run(
            () =>
            {
                DateTime date = DateTime.Now;
                return date.Hour > 17
                   ? "evening"
                   : date.Hour > 12
                       ? "afternoon"
                       : "morning";
            })
            .ContinueWith(
                antecedent =>
                {
                    if (antecedent.Status == TaskStatus.RanToCompletion)
                    {
                        Console.WriteLine($"Good {antecedent.Result}!");
                        Console.WriteLine($"And how are you this fine {antecedent.Result}?");
                    }
                    else if (antecedent.Status == TaskStatus.Faulted)
                    {
                        Console.WriteLine(antecedent.Exception.GetBaseException().Message);
                    }
                });
}
// The example displays output like the following:
//       Good afternoon!
//       And how are you this fine afternoon?
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim dat As DateTime = DateTime.Now
                             If dat = DateTime.MinValue Then
                                 Throw New ArgumentException("The clock is not working.")
                             End If

                             If dat.Hour > 17 Then
                                 Return "evening"
                             Else If dat.Hour > 12 Then
                                 Return "afternoon"
                             Else
                                 Return "morning"
                             End If
                         End Function)
        Dim c = t.ContinueWith(Sub(antecedent)
                                   If t.Status = TaskStatus.RanToCompletion Then
                                       Console.WriteLine("Good {0}!",
                                                         antecedent.Result)
                                       Console.WriteLine("And how are you this fine {0}?",
                                                         antecedent.Result)
                                   Else If t.Status = TaskStatus.Faulted Then
                                       Console.WriteLine(t.Exception.GetBaseException().Message)
                                   End If
                               End Sub)
    End Sub
End Module
' The example displays output like the following:
'       Good afternoon!
'       And how are you this fine afternoon?

연속 작업 취소Cancel a continuation

다음과 같은 경우 연속 작업의 Task.Status 속성이 TaskStatus.Canceled 로 설정됩니다.The Task.Status property of a continuation is set to TaskStatus.Canceled in the following situations:

작업과 해당 연속 작업이 동일한 논리 작업의 두 부분을 나타내는 경우 다음 예제와 같이 두 작업에 모두 동일한 취소 토큰을 전달할 수 있습니다.If a task and its continuation represent two parts of the same logical operation, you can pass the same cancellation token to both tasks, as shown in the following example. 취소 토큰은 33으로 나눌 수 있는 정수 목록을 생성하는 선행 작업으로 구성되며 연속 작업에 전달됩니다.It consists of an antecedent that generates a list of integers that are divisible by 33, which it passes to the continuation. 그런 다음 연속 작업이 목록을 표시합니다.The continuation in turn displays the list. 선행 작업과 연속 작업은 모두 임의 간격 동안 정기적으로 일시 중지됩니다.Both the antecedent and the continuation pause regularly for random intervals. 또한 System.Threading.Timer 개체는 5초 시간 제한 간격 후에 Elapsed 메서드를 실행하는 데 사용됩니다.In addition, a System.Threading.Timer object is used to execute the Elapsed method after a five-second timeout interval. 이 예제에서는 현재 실행 중인 작업이 CancellationToken.ThrowIfCancellationRequested 메서드를 호출하게 하는 CancellationTokenSource.Cancel 메서드를 호출합니다.This example calls the CancellationTokenSource.Cancel method, which causes the currently executing task to call the CancellationToken.ThrowIfCancellationRequested method. 선행 작업이나 해당 연속 작업이 실행 중일 때 CancellationTokenSource.Cancel 메서드를 호출할지 여부는 임의로 생성된 일시 중지 기간에 따라 달라집니다.Whether the CancellationTokenSource.Cancel method is called when the antecedent or its continuation is executing depends on the duration of the randomly generated pauses. 선행 작업이 취소되면 연속 작업은 시작되지 않습니다.If the antecedent is canceled, the continuation will not start. 선행 작업이 취소되지 않은 경우에도 토큰을 사용하여 연속 작업을 취소할 수 있습니다.If the antecedent is not canceled, the token can still be used to cancel the continuation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public class CancellationExample
{
    static readonly Random s_random = new Random((int)DateTime.Now.Ticks);

    public static async Task Main()
    {
        using var cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        var timer = new Timer(Elapsed, cts, 5000, Timeout.Infinite);

        var task = Task.Run(
            async () =>
            {
                var product33 = new List<int>();
                for (int index = 1; index < short.MaxValue; index++)
                {
                    if (token.IsCancellationRequested)
                    {
                        Console.WriteLine("\nCancellation requested in antecedent...\n");
                        token.ThrowIfCancellationRequested();
                    }
                    if (index % 2000 == 0)
                    {
                        int delay = s_random.Next(16, 501);
                        await Task.Delay(delay);
                    }
                    if (index % 33 == 0)
                    {
                        product33.Add(index);
                    }
                }

                return product33.ToArray();
            }, token);

        Task<double> continuation = task.ContinueWith(
            async antecedent =>
            {
                Console.WriteLine("Multiples of 33:\n");
                int[] array = antecedent.Result;
                for (int index = 0; index < array.Length; index++)
                {
                    if (token.IsCancellationRequested)
                    {
                        Console.WriteLine("\nCancellation requested in continuation...\n");
                        token.ThrowIfCancellationRequested();
                    }
                    if (index % 100 == 0)
                    {
                        int delay = s_random.Next(16, 251);
                        await Task.Delay(delay);
                    }

                    Console.Write($"{array[index]:N0}{(index != array.Length - 1 ? ", " : "")}");

                    if (Console.CursorLeft >= 74)
                    {
                        Console.WriteLine();
                    }
                }
                Console.WriteLine();
                return array.Average();
            }, token).Unwrap();

        try
        {
            await task;
            double result = await continuation;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        Console.WriteLine("\nAntecedent Status: {0}", task.Status);
        Console.WriteLine("Continuation Status: {0}", continuation.Status);
    }

    static void Elapsed(object state)
    {
        if (state is CancellationTokenSource cts)
        {
            cts.Cancel();
            Console.WriteLine("\nCancellation request issued...\n");
        }
    }
}
// The example displays the similar output:
//     Multiples of 33:
//     
//     33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495, 528,
//     561, 594, 627, 660, 693, 726, 759, 792, 825, 858, 891, 924, 957, 990, 1,023,
//     1,056, 1,089, 1,122, 1,155, 1,188, 1,221, 1,254, 1,287, 1,320, 1,353, 1,386,
//     1,419, 1,452, 1,485, 1,518, 1,551, 1,584, 1,617, 1,650, 1,683, 1,716, 1,749,
//     1,782, 1,815, 1,848, 1,881, 1,914, 1,947, 1,980, 2,013, 2,046, 2,079, 2,112,
//     2,145, 2,178, 2,211, 2,244, 2,277, 2,310, 2,343, 2,376, 2,409, 2,442, 2,475,
//     2,508, 2,541, 2,574, 2,607, 2,640, 2,673, 2,706, 2,739, 2,772, 2,805, 2,838,
//     2,871, 2,904, 2,937, 2,970, 3,003, 3,036, 3,069, 3,102, 3,135, 3,168, 3,201,
//     3,234, 3,267, 3,300, 3,333, 3,366, 3,399, 3,432, 3,465, 3,498, 3,531, 3,564,
//     3,597, 3,630, 3,663, 3,696, 3,729, 3,762, 3,795, 3,828, 3,861, 3,894, 3,927,
//     3,960, 3,993, 4,026, 4,059, 4,092, 4,125, 4,158, 4,191, 4,224, 4,257, 4,290,
//     4,323, 4,356, 4,389, 4,422, 4,455, 4,488, 4,521, 4,554, 4,587, 4,620, 4,653,
//     4,686, 4,719, 4,752, 4,785, 4,818, 4,851, 4,884, 4,917, 4,950, 4,983, 5,016,
//     5,049, 5,082, 5,115, 5,148, 5,181, 5,214, 5,247, 5,280, 5,313, 5,346, 5,379,
//     5,412, 5,445, 5,478, 5,511, 5,544, 5,577, 5,610, 5,643, 5,676, 5,709, 5,742,
//     Cancellation request issued...
//
//     5,775,
//     Cancellation requested in continuation...
//       
//     The operation was canceled.
//       
//     Antecedent Status: RanToCompletion
//     Continuation Status: Canceled
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim rnd As New Random()
        Dim lockObj As New Object()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token
        Dim timer As New Timer(AddressOf Elapsed, cts, 5000, Timeout.Infinite)

        Dim t = Task.Run(Function()
                             Dim product33 As New List(Of Integer)()
                             For ctr As Integer = 1 To Int16.MaxValue
                                 ' Check for cancellation.
                                 If token.IsCancellationRequested Then
                                     Console.WriteLine("\nCancellation requested in antecedent...\n")
                                     token.ThrowIfCancellationRequested()
                                 End If
                                 ' Introduce a delay.
                                 If ctr Mod 2000 = 0 Then
                                     Dim delay As Integer
                                     SyncLock lockObj
                                         delay = rnd.Next(16, 501)
                                     End SyncLock
                                     Thread.Sleep(delay)
                                 End If

                                 ' Determine if this is a multiple of 33.
                                 If ctr Mod 33 = 0 Then product33.Add(ctr)
                             Next
                             Return product33.ToArray()
                         End Function, token)

        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Multiples of 33:" + vbCrLf)
                                              Dim arr = antecedent.Result
                                              For ctr As Integer = 0 To arr.Length - 1
                                                  If token.IsCancellationRequested Then
                                                      Console.WriteLine("{0}Cancellation requested in continuation...{0}",
                                                                        vbCrLf)
                                                      token.ThrowIfCancellationRequested()
                                                  End If

                                                  If ctr Mod 100 = 0 Then
                                                      Dim delay As Integer
                                                      SyncLock lockObj
                                                          delay = rnd.Next(16, 251)
                                                      End SyncLock
                                                      Thread.Sleep(delay)
                                                  End If
                                                  Console.Write("{0:N0}{1}", arr(ctr),
                                                                If(ctr <> arr.Length - 1, ", ", ""))
                                                  If Console.CursorLeft >= 74 Then Console.WriteLine()
                                              Next
                                              Console.WriteLine()
                                          End Sub, token)

        Try
            continuation.Wait()
        Catch e As AggregateException
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}: {1}", ie.GetType().Name,
                                  ie.Message)
            Next
        Finally
            cts.Dispose()
        End Try

        Console.WriteLine(vbCrLf + "Antecedent Status: {0}", t.Status)
        Console.WriteLine("Continuation Status: {0}", continuation.Status)
    End Sub

    Private Sub Elapsed(state As Object)
        Dim cts As CancellationTokenSource = TryCast(state, CancellationTokenSource)
        If cts Is Nothing Then return

        cts.Cancel()
        Console.WriteLine("{0}Cancellation request issued...{0}", vbCrLf)
    End Sub
End Module
' The example displays output like the following:
'    Multiples of 33:
'
'    33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495, 528,
'    561, 594, 627, 660, 693, 726, 759, 792, 825, 858, 891, 924, 957, 990, 1,023,
'    1,056, 1,089, 1,122, 1,155, 1,188, 1,221, 1,254, 1,287, 1,320, 1,353, 1,386,
'    1,419, 1,452, 1,485, 1,518, 1,551, 1,584, 1,617, 1,650, 1,683, 1,716, 1,749,
'    1,782, 1,815, 1,848, 1,881, 1,914, 1,947, 1,980, 2,013, 2,046, 2,079, 2,112,
'    2,145, 2,178, 2,211, 2,244, 2,277, 2,310, 2,343, 2,376, 2,409, 2,442, 2,475,
'    2,508, 2,541, 2,574, 2,607, 2,640, 2,673, 2,706, 2,739, 2,772, 2,805, 2,838,
'    2,871, 2,904, 2,937, 2,970, 3,003, 3,036, 3,069, 3,102, 3,135, 3,168, 3,201,
'    3,234, 3,267, 3,300, 3,333, 3,366, 3,399, 3,432, 3,465, 3,498, 3,531, 3,564,
'    3,597, 3,630, 3,663, 3,696, 3,729, 3,762, 3,795, 3,828, 3,861, 3,894, 3,927,
'    3,960, 3,993, 4,026, 4,059, 4,092, 4,125, 4,158, 4,191, 4,224, 4,257, 4,290,
'    4,323, 4,356, 4,389, 4,422, 4,455, 4,488, 4,521, 4,554, 4,587, 4,620, 4,653,
'    4,686, 4,719, 4,752, 4,785, 4,818, 4,851, 4,884, 4,917, 4,950, 4,983, 5,016,
'    5,049, 5,082, 5,115, 5,148, 5,181, 5,214, 5,247, 5,280, 5,313, 5,346, 5,379,
'    5,412, 5,445, 5,478, 5,511, 5,544, 5,577, 5,610, 5,643, 5,676, 5,709, 5,742,
'    5,775, 5,808, 5,841, 5,874, 5,907, 5,940, 5,973, 6,006, 6,039, 6,072, 6,105,
'    6,138, 6,171, 6,204, 6,237, 6,270, 6,303, 6,336, 6,369, 6,402, 6,435, 6,468,
'    6,501, 6,534, 6,567, 6,600, 6,633, 6,666, 6,699, 6,732, 6,765, 6,798, 6,831,
'    6,864, 6,897, 6,930, 6,963, 6,996, 7,029, 7,062, 7,095, 7,128, 7,161, 7,194,
'    7,227, 7,260, 7,293, 7,326, 7,359, 7,392, 7,425, 7,458, 7,491, 7,524, 7,557,
'    7,590, 7,623, 7,656, 7,689, 7,722, 7,755, 7,788, 7,821, 7,854, 7,887, 7,920,
'    7,953, 7,986, 8,019, 8,052, 8,085, 8,118, 8,151, 8,184, 8,217, 8,250, 8,283,
'    8,316, 8,349, 8,382, 8,415, 8,448, 8,481, 8,514, 8,547, 8,580, 8,613, 8,646,
'    8,679, 8,712, 8,745, 8,778, 8,811, 8,844, 8,877, 8,910, 8,943, 8,976, 9,009,
'    9,042, 9,075, 9,108, 9,141, 9,174, 9,207, 9,240, 9,273, 9,306, 9,339, 9,372,
'    9,405, 9,438, 9,471, 9,504, 9,537, 9,570, 9,603, 9,636, 9,669, 9,702, 9,735,
'    9,768, 9,801, 9,834, 9,867, 9,900, 9,933, 9,966, 9,999, 10,032, 10,065, 10,098,
'    10,131, 10,164, 10,197, 10,230, 10,263, 10,296, 10,329, 10,362, 10,395, 10,428,
'    10,461, 10,494, 10,527, 10,560, 10,593, 10,626, 10,659, 10,692, 10,725, 10,758,
'    10,791, 10,824, 10,857, 10,890, 10,923, 10,956, 10,989, 11,022, 11,055, 11,088,
'    11,121, 11,154, 11,187, 11,220, 11,253, 11,286, 11,319, 11,352, 11,385, 11,418,
'    11,451, 11,484, 11,517, 11,550, 11,583, 11,616, 11,649, 11,682, 11,715, 11,748,
'    11,781, 11,814, 11,847, 11,880, 11,913, 11,946, 11,979, 12,012, 12,045, 12,078,
'    12,111, 12,144, 12,177, 12,210, 12,243, 12,276, 12,309, 12,342, 12,375, 12,408,
'    12,441, 12,474, 12,507, 12,540, 12,573, 12,606, 12,639, 12,672, 12,705, 12,738,
'    12,771, 12,804, 12,837, 12,870, 12,903, 12,936, 12,969, 13,002, 13,035, 13,068,
'    13,101, 13,134, 13,167, 13,200, 13,233, 13,266,
'    Cancellation requested in continuation...
'
'
'    Cancellation request issued...
'
'    TaskCanceledException: A task was canceled.
'
'    Antecedent Status: RanToCompletion
'    Continuation Status: Canceled

연속 작업을 만들 때 TaskContinuationOptions.NotOnCanceled 옵션을 지정하면 연속 작업에 취소 토큰을 제공하지 않고 선행 작업이 취소된 경우에도 연속 작업이 실행되지 않도록 할 수 있습니다.You can also prevent a continuation from executing if its antecedent is canceled without supplying the continuation a cancellation token by specifying the TaskContinuationOptions.NotOnCanceled option when you create the continuation. 다음은 간단한 예제입니다.The following is a simple example.

using System;
using System.Threading;
using System.Threading.Tasks;

public class CancellationTwoExample
{
    public static async Task Main()
    {
        using var cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        cts.Cancel();

        var task = Task.FromCanceled(token);
        Task continuation =
            task.ContinueWith(
                antecedent => Console.WriteLine("The continuation is running."),
                TaskContinuationOptions.NotOnCanceled);

        try
        {
            await task;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
            Console.WriteLine();
        }

        Console.WriteLine($"Task {task.Id}: {task.Status:G}");
        Console.WriteLine($"Task {continuation.Id}: {continuation.Status:G}");
    }
}
// The example displays the similar output:
//       TaskCanceledException: A task was canceled.
//
//       Task 1: Canceled
//       Task 2: Canceled
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token
        cts.Cancel()

        Dim t As Task = Task.FromCanceled(token)
        Dim continuation As Task = t.ContinueWith(Sub(antecedent)
                                                      Console.WriteLine("The continuation is running.")
                                                  End Sub, TaskContinuationOptions.NotOnCanceled)
        Try
            t.Wait()
        Catch e As AggregateException
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}: {1}", ie.GetType().Name, ie.Message)
            Next
            Console.WriteLine()
        Finally
            cts.Dispose()
        End Try

        Console.WriteLine("Task {0}: {1:G}", t.Id, t.Status)
        Console.WriteLine("Task {0}: {1:G}", continuation.Id,
                          continuation.Status)
    End Sub
End Module
' The example displays the following output:
'       TaskCanceledException: A task was canceled.
'
'       Task 1: Canceled
'       Task 2: Canceled

연속 작업이 Canceled 상태로 전환된 후 연속 작업에 대해 지정된 TaskContinuationOptions 에 따라 이후의 연속 작업에 영향을 줄 수 있습니다.After a continuation goes into the Canceled state, it may affect continuations that follow, depending on the TaskContinuationOptions that were specified for those continuations.

삭제된 연속 작업은 시작되지 않습니다.Continuations that are disposed will not start.

연속 작업 및 자식 작업Continuations and child tasks

연속 작업은 선행 작업 및 연결된 모든 자식 작업이 완료될 때까지 실행되지 않습니다.A continuation does not run until the antecedent and all of its attached child tasks have completed. 연속 작업은 분리된 자식 작업이 완료되기를 기다리지 않습니다.The continuation does not wait for detached child tasks to finish. 다음 두 예제에서는 연속 작업을 만드는 선행 작업에 연결된 자식 작업과 분리된 자식 작업을 보여 줍니다.The following two examples illustrate child tasks that are attached to and detached from an antecedent that creates a continuation. 다음 예제에서는 모든 자식 작업이 완료된 후에만 연속 작업이 실행되며 예제를 여러 번 실행해도 매번 동일한 출력이 생성됩니다.In the following example, the continuation runs only after all child tasks have completed, and running the example multiple times produces identical output each time. 기본적으로 Task.Run 메서드는 기본 작업 생성 옵션이 TaskCreationOptions.DenyChildAttach인 부모 작업을 만들기 때문에 예제에서는 TaskFactory.StartNew 메서드를 호출하여 선행 작업을 시작합니다.The example launches the antecedent by calling the TaskFactory.StartNew method, since by default the Task.Run method creates a parent task whose default task creation option is TaskCreationOptions.DenyChildAttach.

using System;
using System.Threading.Tasks;

public class AttachedExample
{
    public static async Task Main()
    {
        await Task.Factory
                  .StartNew(
            () =>
            {
                Console.WriteLine($"Running antecedent task {Task.CurrentId}...");
                Console.WriteLine("Launching attached child tasks...");
                for (int ctr = 1; ctr <= 5; ctr++)
                {
                    int index = ctr;
                    Task.Factory.StartNew(async value =>
                    {
                        Console.WriteLine($"   Attached child task #{value} running");
                        await Task.Delay(1000);
                    }, index, TaskCreationOptions.AttachedToParent);
                }
                Console.WriteLine("Finished launching attached child tasks...");
            }).ContinueWith(
                antecedent =>
                    Console.WriteLine($"Executing continuation of Task {antecedent.Id}"));
    }
}
// The example displays the similar output:
//     Running antecedent task 1...
//     Launching attached child tasks...
//     Finished launching attached child tasks...
//        Attached child task #1 running
//        Attached child task #5 running
//        Attached child task #3 running
//        Attached child task #2 running
//        Attached child task #4 running
//     Executing continuation of Task 1
Imports System.Threading
Imports System.Threading.Tasks

Public Module Example
    Public Sub Main()
        Dim t = Task.Factory.StartNew(Sub()
                                          Console.WriteLine("Running antecedent task {0}...",
                                                            Task.CurrentId)
                                          Console.WriteLine("Launching attached child tasks...")
                                          For ctr As Integer = 1 To 5
                                              Dim index As Integer = ctr
                                              Task.Factory.StartNew(Sub(value)
                                                                        Console.WriteLine("   Attached child task #{0} running",
                                                                                          value)
                                                                        Thread.Sleep(1000)
                                                                    End Sub, index, TaskCreationOptions.AttachedToParent)
                                          Next
                                          Console.WriteLine("Finished launching attached child tasks...")
                                      End Sub)
        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Executing continuation of Task {0}",
                                                                antecedent.Id)
                                          End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays the following output:
'       Running antecedent task 1...
'       Launching attached child tasks...
'       Finished launching attached child tasks...
'          Attached child task #5 running
'          Attached child task #1 running
'          Attached child task #2 running
'          Attached child task #3 running
'          Attached child task #4 running
'       Executing continuation of Task 1

그러나 자식 작업이 선행 작업에서 분리된 경우 자식 작업의 상태에 관계없이 선행 작업이 종료된 즉시 연속 작업이 실행됩니다.If child tasks are detached from the antecedent, however, the continuation runs as soon as the antecedent has terminated, regardless of the state of the child tasks. 따라서 다음 예제를 여러 번 실행하면 작업 스케줄러가 각 자식 작업을 처리한 방식에 따라 다른 출력이 생성될 수 있습니다.As a result, multiple runs of the following example can produce variable output that depends on how the task scheduler handled each child task.

using System;
using System.Threading.Tasks;

public class DetachedExample
{
    public static async Task Main()
    {
        Task task =
            Task.Factory.StartNew(
                () =>
                {
                    Console.WriteLine($"Running antecedent task {Task.CurrentId}...");
                    Console.WriteLine("Launching attached child tasks...");
                    for (int ctr = 1; ctr <= 5; ctr++)
                    {
                        int index = ctr;
                        Task.Factory.StartNew(
                            async value =>
                            {
                                Console.WriteLine($"   Attached child task #{value} running");
                                await Task.Delay(1000);
                            }, index);
                    }
                    Console.WriteLine("Finished launching detached child tasks...");
                }, TaskCreationOptions.DenyChildAttach);

        Task continuation =
            task.ContinueWith(
                antecedent =>
                    Console.WriteLine($"Executing continuation of Task {antecedent.Id}"));

        await continuation;

        Console.ReadLine();
    }
}
// The example displays the similar output:
//     Running antecedent task 1...
//     Launching attached child tasks...
//     Finished launching detached child tasks...
//     Executing continuation of Task 1
//        Attached child task #1 running
//        Attached child task #5 running
//        Attached child task #2 running
//        Attached child task #3 running
//        Attached child task #4 running
Imports System.Threading
Imports System.Threading.Tasks

Public Module Example
    Public Sub Main()
        Dim t = Task.Factory.StartNew(Sub()
                                          Console.WriteLine("Running antecedent task {0}...",
                                                            Task.CurrentId)
                                          Console.WriteLine("Launching attached child tasks...")
                                          For ctr As Integer = 1 To 5
                                              Dim index As Integer = ctr
                                              Task.Factory.StartNew(Sub(value)
                                                                        Console.WriteLine("   Attached child task #{0} running",
                                                                                          value)
                                                                        Thread.Sleep(1000)
                                                                    End Sub, index)
                                          Next
                                          Console.WriteLine("Finished launching detached child tasks...")
                                      End Sub, TaskCreationOptions.DenyChildAttach)
        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Executing continuation of Task {0}",
                                                                antecedent.Id)
                                          End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays output like the following:
'       Running antecedent task 1...
'       Launching attached child tasks...
'       Finished launching detached child tasks...
'          Attached child task #1 running
'          Attached child task #2 running
'          Attached child task #5 running
'          Attached child task #3 running
'       Executing continuation of Task 1
'          Attached child task #4 running

선행 작업의 최종 상태는 연결된 자식 작업의 최종 상태에 따라 달라집니다.The final status of the antecedent task depends on the final status of any attached child tasks. 분리된 자식 작업의 상태는 부모에 영향을 주지 않습니다.The status of detached child tasks does not affect the parent. 자세한 내용은 연결된 자식 작업과 분리된 자식 작업을 참조하세요.For more information, see Attached and Detached Child Tasks.

연속 작업에 상태 연결Associate state with continuations

연속 작업에 임의 상태를 연결할 수 있습니다.You can associate arbitrary state with a task continuation. ContinueWith 메서드는 각각 연속 상태를 나타내는 Object 값을 받는 오버로드된 버전을 제공합니다.The ContinueWith method provides overloaded versions that each take an Object value that represents the state of the continuation. 나중에 Task.AsyncState 속성을 사용하여 이 상태 개체에 액세스할 수 있습니다.You can later access this state object by using the Task.AsyncState property. 값을 제공하지 않을 경우 이 상태 개체는 null 입니다.This state object is null if you do not provide a value.

연속 상태는 APM(비동기 프로그래밍 모델) 을 사용하는 기존 코드를 TPL을 사용하도록 변환하는 경우에 유용합니다.Continuation state is useful when you convert existing code that uses the Asynchronous Programming Model (APM) to use the TPL. APM에서는 일반적으로 BeginMethod 메서드에 개체 상태를 제공하고 나중에 IAsyncResult.AsyncState 속성을 통해 해당 상태에 액세스합니다.In the APM, you typically provide object state in the BeginMethod method and later access that state by using the IAsyncResult.AsyncState property. ContinueWith 메서드를 사용하면 APM을 사용하는 코드를 TPL을 사용하도록 변환할 때 이 상태를 보존할 수 있습니다.By using the ContinueWith method, you can preserve this state when you convert code that uses the APM to use the TPL.

Visual Studio 디버거에서 Task 개체로 작업하는 경우에도 연속 상태가 유용할 수 있습니다.Continuation state can also be useful when you work with Task objects in the Visual Studio debugger. 예를 들어 병렬 작업 창의 작업 열에는 각 작업에 대한 상태 개체의 문자열 표현이 표시됩니다.For example, in the Parallel Tasks window, the Task column displays the string representation of the state object for each task. 병렬 작업 창에 대한 자세한 내용은 작업 창 사용을 참조하세요.For more information about the Parallel Tasks window, see Using the Tasks Window.

다음 예제에서는 연속 상태를 사용하는 방법을 보여 줍니다.The following example shows how to use continuation state. 연속 작업 체인을 만듭니다.It creates a chain of continuation tasks. 각 작업은 DateTime 메서드의 state 매개 변수에 대해 현재 시간인 ContinueWith 개체를 제공합니다.Each task provides the current time, a DateTime object, for the state parameter of the ContinueWith method. DateTime 개체는 연속 작업이 만들어진 시간을 나타냅니다.Each DateTime object represents the time at which the continuation task is created. 각 작업은 작업 완료 시간을 나타내는 두 번째 DateTime 개체를 결과로 생성합니다.Each task produces as its result a second DateTime object that represents the time at which the task finishes. 이 예제에서는 모든 작업이 완료된 후 만든 시간과 각 연속 작업의 완료 시간이 표시됩니다.After all tasks finish, this example displays the creation time and the time at which each continuation task finishes.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

class ContinuationStateExample
{
    static DateTime DoWork()
    {
        Thread.Sleep(2000);

        return DateTime.Now;
    }

    static async Task Main()
    {
        Task<DateTime> task = Task.Run(() => DoWork());

        var continuations = new List<Task<DateTime>>();
        for (int i = 0; i < 5; i++)
        {
            task = task.ContinueWith((antecedent, _) => DoWork(), DateTime.Now);
            continuations.Add(task);
        }

        await task;

        foreach (Task<DateTime> continuation in continuations)
        {
            DateTime start = (DateTime)continuation.AsyncState;
            DateTime end = continuation.Result;

            Console.WriteLine($"Task was created at {start.TimeOfDay} and finished at {end.TimeOfDay}.");
        }

        Console.ReadLine();
    }
}
// The example displays the similar output:
//     Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062.
//     Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646.
//     Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230.
//     Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883.
//     Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083.
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

' Demonstrates how to associate state with task continuations.
Public Module ContinuationState
    ' Simluates a lengthy operation and returns the time at which
    ' the operation completed.
    Public Function DoWork() As Date
        ' Simulate work by suspending the current thread 
        ' for two seconds.
        Thread.Sleep(2000)

        ' Return the current time.
        Return Date.Now
    End Function

    Public Sub Main()
        ' Start a root task that performs work.
        Dim t As Task(Of Date) = Task(Of Date).Run(Function() DoWork())

        ' Create a chain of continuation tasks, where each task is
        ' followed by another task that performs work.
        Dim continuations As New List(Of Task(Of DateTime))()
        For i As Integer = 0 To 4
            ' Provide the current time as the state of the continuation.
            t = t.ContinueWith(Function(antecedent, state) DoWork(), DateTime.Now)
            continuations.Add(t)
        Next

        ' Wait for the last task in the chain to complete.
        t.Wait()

        ' Display the creation time of each continuation (the state object)
        ' and the completion time (the result of that task) to the console.
        For Each continuation In continuations
            Dim start As DateTime = CDate(continuation.AsyncState)
            Dim [end] As DateTime = continuation.Result

            Console.WriteLine("Task was created at {0} and finished at {1}.",
               start.TimeOfDay, [end].TimeOfDay)
        Next
    End Sub
End Module
' The example displays output like the following:
'       Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062.
'       Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646.
'       Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230.
'       Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883.
'       Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083.

작업 유형을 반환하는 연속 작업Continuations that return Task types

Task 형식을 반환하는 연속 작업을 연결해야 하는 경우도 있습니다.Sometimes you may need to chain a continuation that returns a Task type. 이러한 작업은 중첩된 작업이라고 하며 일반적입니다.These are referred to as nested tasks, and they are common. 부모 작업이 Task<TResult>.ContinueWith을 호출하고 작업을 반환하는 continuationFunction을 제공하는 경우 Unwrap을 호출하여 <Task<Task<T>>> 또는 Task(Of Task(Of T))(Visual Basic)의 비동기 작업을 나타내는 프록시 작업을 만듭니다.When a parent task calls Task<TResult>.ContinueWith, and provides a continuationFunction that is task returning you call Unwrap to create a proxy task that represents the asynchronous operation of the <Task<Task<T>>> or Task(Of Task(Of T)) (Visual Basic).

다음 예제에서는 추가 작업 반환 함수를 래핑하는 연속 작업을 사용하는 방법을 보여 줍니다.The following example shows how to use continuations that wrap additional task returning functions. 각 연속 작업을 래핑 해제하여 래핑된 내부 작업을 노출할 수 있습니다.Each continuation can be unwrapped, exposing the inner task that was wrapped.

using System;
using System.Threading;
using System.Threading.Tasks;

public class UnwrapExample
{
    public static async Task Main()
    {
        Task<int> taskOne = RemoteIncrement(0);
        Console.WriteLine("Started RemoteIncrement(0)");

        Task<int> taskTwo = RemoteIncrement(4)
            .ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap().ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap().ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap();

        Console.WriteLine("Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)");

        try
        {
            await taskOne;
            Console.WriteLine("Finished RemoteIncrement(0)");

            await taskTwo;
            Console.WriteLine("Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)");
        }
        catch (Exception e)
        {
            Console.WriteLine($"A task has thrown the following (unexpected) exception:\n{e}");
        }
    }

    static Task<int> RemoteIncrement(int number) =>
        Task<int>.Factory.StartNew(
            obj =>
            {
                Thread.Sleep(1000);

                int x = (int)obj;
                Console.WriteLine("Thread={0}, Next={1}", Thread.CurrentThread.ManagedThreadId, ++x);
                return x;
            },
            number);
}

// The example displays the similar output:
//     Started RemoteIncrement(0)
//     Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)
//     Thread=4, Next=1
//     Finished RemoteIncrement(0)
//     Thread=5, Next=5
//     Thread=6, Next=6
//     Thread=6, Next=7
//     Thread=6, Next=8
//     Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)
Imports System.Threading

Module UnwrapExample
    Sub Main()
        Dim taskOne As Task(Of Integer) = RemoteIncrement(0)
        Console.WriteLine("Started RemoteIncrement(0)")

        Dim taskTwo As Task(Of Integer) = RemoteIncrement(4).
            ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap().ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap().ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap()

        Console.WriteLine("Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)")

        Try
            taskOne.Wait()
            Console.WriteLine("Finished RemoteIncrement(0)")

            taskTwo.Wait()
            Console.WriteLine("Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)")
        Catch e As AggregateException
            Console.WriteLine($"A task has thrown the following (unexpected) exception:{vbLf}{e}")
        End Try
    End Sub

    Function RemoteIncrement(ByVal number As Integer) As Task(Of Integer)
        Return Task(Of Integer).Factory.StartNew(
            Function(obj)
                Thread.Sleep(1000)

                Dim x As Integer = CInt(obj)
                Console.WriteLine("Thread={0}, Next={1}", Thread.CurrentThread.ManagedThreadId, Interlocked.Increment(x))
                Return x
            End Function, number)
    End Function
End Module

' The example displays the similar output:
'     Started RemoteIncrement(0)
'     Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)
'     Thread=4, Next=1
'     Finished RemoteIncrement(0)
'     Thread=5, Next=5
'     Thread=6, Next=6
'     Thread=6, Next=7
'     Thread=6, Next=8
'     Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)

Unwrap을 사용하는 방법에 대한 자세한 내용은 방법: 중첩된 작업 래핑 취소를 참조하세요.For more information on using Unwrap, see How to: Unwrap a nested Task.

연속 작업에서 throw된 예외 처리Handle exceptions thrown from continuations

선행-연속 관계는 부모-자식 관계가 아닙니다.An antecedent-continuation relationship is not a parent-child relationship. 연속 작업에서 발생한 예외는 선행 작업으로 전파되지 않습니다.Exceptions thrown by continuations are not propagated to the antecedent. 따라서 다른 작업에서 처리하는 것처럼 연속 작업에서 발생한 예외를 다음과 같이 처리합니다.Therefore, handle exceptions thrown by continuations as you would handle them in any other task, as follows:

  • Wait, WaitAll또는 WaitAny 메서드나 해당하는 제네릭 항목을 사용하여 연속 작업을 기다릴 수 있습니다.You can use the Wait, WaitAll, or WaitAny method, or its generic counterpart, to wait on the continuation. 다음 예제와 같이 동일한 try 문에서 선행 작업과 해당 연속 작업을 기다릴 수 있습니다.You can wait for an antecedent and its continuations in the same try statement, as shown in the following example.
using System;
using System.Threading.Tasks;

public class ExceptionExample
{
    public static async Task Main()
    {
        Task<int> task = Task.Run(
            () =>
            {
                Console.WriteLine($"Executing task {Task.CurrentId}");
                return 54;
            });

        var continuation = task.ContinueWith(
            antecedent =>
            {
                Console.WriteLine($"Executing continuation task {Task.CurrentId}");
                Console.WriteLine($"Value from antecedent: {antecedent.Result}");

                throw new InvalidOperationException();
            });

        try
        {
            await task;
            await continuation;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}
// The example displays the similar output:
//       Executing task 1
//       Executing continuation task 2
//       Value from antecedent: 54
//       Operation is not valid due to the current state of the object.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task(Of Integer).Run(Function()
                                             Console.WriteLine("Executing task {0}",
                                                               Task.CurrentId)
                                             Return 54
                                         End Function)
        Dim continuation = task1.ContinueWith(Sub(antecedent)
                                                  Console.WriteLine("Executing continuation task {0}",
                                                                    Task.CurrentId)
                                                  Console.WriteLine("Value from antecedent: {0}",
                                                                    antecedent.Result)
                                                  Throw New InvalidOperationException()
                                              End Sub)

        Try
            task1.Wait()
            continuation.Wait()
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine(ex.Message)
            Next
        End Try
    End Sub
End Module
' The example displays the following output:
'       Executing task 1
'       Executing continuation task 2
'       Value from antecedent: 54
'       Operation is not valid due to the current state of the object.
  • 두 번째 연속 작업을 사용하여 첫 번째 연속 작업의 Exception 속성을 관찰할 수 있습니다.You can use a second continuation to observe the Exception property of the first continuation. 다음 예제에서는 작업이 존재하지 않는 파일을 읽으려고 합니다.In the following example, a task attempts to read from a non-existent file. 그런 다음 연속 작업이 선행 작업의 예외 정보를 표시합니다.The continuation then displays information about the exception in the antecedent task.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

public class ExceptionTwoExample
{
    public static async Task Main()
    {
        var task = Task.Run(
            () =>
            {
                string fileText = File.ReadAllText(@"C:\NonexistentFile.txt");
                return fileText;
            });

        Task continuation = task.ContinueWith(
            antecedent =>
            {
                var fileNotFound =
                    antecedent.Exception
                        ?.InnerExceptions
                        ?.FirstOrDefault(e => e is FileNotFoundException) as FileNotFoundException;

                if (fileNotFound != null)
                {
                    Console.WriteLine(fileNotFound.Message);
                }
            }, TaskContinuationOptions.OnlyOnFaulted);

        await continuation;

        Console.ReadLine();
    }
}
// The example displays the following output:
//        Could not find file 'C:\NonexistentFile.txt'.
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim s As String = File.ReadAllText("C:\NonexistentFile.txt")
                             Return s
                         End Function)

        Dim c = t.ContinueWith(Sub(antecedent)
                                   ' Get the antecedent's exception information.
                                   For Each ex In antecedent.Exception.InnerExceptions
                                       If TypeOf ex Is FileNotFoundException
                                           Console.WriteLine(ex.Message)
                                       End If
                                   Next
                               End Sub, TaskContinuationOptions.OnlyOnFaulted)

        c.Wait()
    End Sub
End Module
' The example displays the following output:
'       Could not find file 'C:\NonexistentFile.txt'.

TaskContinuationOptions.OnlyOnFaulted 옵션으로 실행되었기 때문에 연속 작업은 선행 작업에서 예외가 발생한 경우에만 실행되므로 선행 작업의 Exception 속성이 null이 아니라고 가정할 수 있습니다.Because it was run with the TaskContinuationOptions.OnlyOnFaulted option, the continuation executes only if an exception occurs in the antecedent, and therefore it can assume that the antecedent's Exception property is not null. 선행 작업에 예외가 발생했는지 여부에 관계없이 연속 작업이 실행되는 경우 다음 코드 조각과 같이 예외를 처리하기 전에 선행 작업의 Exception 속성이 null 이 아닌지 여부를 확인해야 합니다.If the continuation executes whether or not an exception is thrown in the antecedent, it would have to check whether the antecedent's Exception property is not null before attempting to handle the exception, as the following code fragment shows.

var fileNotFound =
    antecedent.Exception
        ?.InnerExceptions
        ?.FirstOrDefault(e => e is FileNotFoundException) as FileNotFoundException;

if (fileNotFound != null)
{
    Console.WriteLine(fileNotFound.Message);
}
' Determine whether an exception occurred.
If antecedent.Exception IsNot Nothing Then
    ' Get the antecedent's exception information.
    For Each ex In antecedent.Exception.InnerExceptions
        If TypeOf ex Is FileNotFoundException
            Console.WriteLine(ex.Message)
        End If
    Next
End If

자세한 내용은 예외 처리를 참조하세요.For more information, see Exception Handling.

참조See also