연속 작업을 사용하여 작업 연결

비동기 프로그래밍에서는 한 비동기 작업이 완료 시 두 번째 작업을 호출하는 것이 일반적입니다. 연속 작업을 통해 하위 작업에서 첫 번째 작업의 결과를 사용할 수 있습니다. 일반적으로 연속 작업은 콜백 메서드를 통해 수행되었습니다. TPL(작업 병렬 라이브러리)에서는 연속 작업이 동일한 기능을 제공합니다. 연속 작업(연속이라고도 함)은 선행 작업이라고도 하는 다른 작업이 완료 시 호출하는 비동기 작업입니다.

연속은 비교적 사용이 용이하지만 강력하고 유연합니다. 이렇게 시작할 수 있는 작업의 예는 다음과 같습니다.

  • 선행 작업의 데이터를 연속 작업에 전달합니다.
  • 연속 작업이 호출되거나 호출되지 않는 정확한 조건을 지정합니다.
  • 시작되기 전이나 실행 중일 때 함께 연속 작업을 취소합니다.
  • 연속 작업을 예약하는 방법에 대한 힌트를 제공합니다.
  • 동일한 선행 작업에서 여러 개의 연속 작업을 호출합니다.
  • 여러 선행 작업 중 하나 또는 모두가 완료되면 하나의 연속 작업을 호출합니다.
  • 연속 작업을 임의 길이까지 차례로 연결합니다.
  • 연속 작업을 사용하여 선행 작업에서 발생한 예외를 처리합니다.

연속 작업 정보

연속은 WaitingForActivation 상태에서 만들어진 작업입니다. 선행 작업이 완료되면 자동으로 활성화됩니다. 사용자 코드에서 연속 작업에 대해 Task.Start 를 호출하면 System.InvalidOperationException 예외가 발생합니다.

연속은 그 자체로 Task이며 시작된 스레드를 차단하지 않습니다. 연속 작업이 완료될 때까지 차단하려면 Task.Wait 메서드를 호출합니다.

단일 선행 작업에 대한 연속 작업 만들기

Task.ContinueWith 메서드를 호출하여 선행 작업이 완료되었을 때 실행되는 연속 작업을 만듭니다. 다음 예제에서는 기본 패턴을 보여줍니다(이해하기 쉽도록 예외 처리는 생략됨). 현재 요일의 이름을 나타내는 taskA개체를 반환하는 선행 작업 DayOfWeek 를 실행합니다. taskA가 완료되면 antecedentContinueWith 연속 메서드에서 결과를 나타냅니다. 선행 작업의 결과가 콘솔에 기록됩니다.

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.

여러 선행 작업에 대한 연속 작업 만들기

작업 그룹 중 하나 또는 모두가 완료되었을 때 실행되는 연속 작업을 만들 수도 있습니다. 모든 선행 작업이 완료되었을 때 연속 작업을 실행하려면 static(Visual Basic에서는Shared ) Task.WhenAll 메서드 또는 인스턴스 TaskFactory.ContinueWhenAll 메서드를 호출할 수 있습니다. 선행 작업 중 하나가 완료되었을 때 연속 작업을 실행하려면 static(Visual Basic에서는Shared ) Task.WhenAny 메서드 또는 인스턴스 TaskFactory.ContinueWhenAny 메서드를 호출할 수 있습니다.

Task.WhenAllTask.WhenAny 오버로드 호출은 호출 스레드를 차단하지 않습니다. 그러나 일반적으로는 Task.WhenAll(IEnumerable<Task>)Task.WhenAll(Task[]) 메서드를 제외하고 모두 호출하여 반환된 Task<TResult>.Result 속성을 검색하므로, 호출 스레드가 차단됩니다.

다음 예제에서는 Task.WhenAll(IEnumerable<Task>) 메서드를 호출하여 10개 선행 작업의 결과를 반영하는 연속 작업을 만듭니다. 각 선행 작업은 1에서 10까지의 인덱스 값을 제곱합니다. 선행 작업이 성공적으로 완료될 경우( Task.Status 속성이 TaskStatus.RanToCompletion임) 연속 작업의 Task<TResult>.Result 속성은 각 선행 작업에서 반환된 Task<TResult>.Result 값의 배열입니다. 예제에서는 값을 더하여 1과 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

연속 옵션

단일 작업 연속을 만드는 경우 ContinueWith 열거형 값을 받는 System.Threading.Tasks.TaskContinuationOptions 오버로드를 사용하여 연속 작업이 시작되는 조건을 지정할 수 있습니다. 예를 들어 선행 작업이 성공적으로 완료되거나 오류 상태로 완료되는 경우에만 연속 작업이 실행되도록 지정할 수 있습니다. 선행 항목이 연속을 호출할 준비가 되었을 때 조건이 true가 아닌 경우 연속은 TaskStatus.Canceled 상태로 직접 전환되며 나중에 시작할 수 없습니다.

TaskFactory.ContinueWhenAll 메서드의 오버로드와 같은 여러 다중 작업 연속 메서드에는 System.Threading.Tasks.TaskContinuationOptions 매개 변수도 포함되어 있습니다. 그러나 모든 System.Threading.Tasks.TaskContinuationOptions 열거형 멤버의 하위 집합만 유효합니다. System.Threading.Tasks.TaskContinuationOptions 열거형에 해당 항목이 있는 System.Threading.Tasks.TaskCreationOptions 값(예: TaskContinuationOptions.AttachedToParent, TaskContinuationOptions.LongRunningTaskContinuationOptions.PreferFairness)을 지정할 수 있습니다. 다중 작업 연속에 NotOn 또는 OnlyOn 옵션을 지정하는 경우 런타임에 ArgumentOutOfRangeException 예외가 발생합니다.

작업 계속 옵션에 대한 자세한 내용은 TaskContinuationOptions 문서를 참조하세요.

연속 작업에 데이터 전달

Task.ContinueWith 메서드는 연속 작업의 사용자 대리자에게 선행 작업에 대한 참조를 인수로 전달합니다. 선행 작업이 System.Threading.Tasks.Task<TResult> 개체이고 작업이 완료될 때까지 실행된 경우 연속 작업이 해당 작업의 Task<TResult>.Result 속성에 액세스할 수 있습니다.

Task<TResult>.Result 속성은 작업이 완료될 때까지 차단됩니다. 그러나 작업이 취소되거나 오류가 발생한 경우 Result 속성에 액세스하려고 하면 AggregateException 예외가 발생합니다. 다음 예제와 같이 OnlyOnRanToCompletion 옵션을 사용하여 이 문제를 방지할 수 있습니다.

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?

선행 작업이 성공적으로 완료될 때까지 실행되지 않은 경우에도 연속 작업을 실행하려면 예외로부터 보호해야 합니다. 한 가지 방법은 선행 작업의 Task.Status 속성을 테스트하고 상태가 Result 또는 Faulted 가 아닌 경우에만 Canceled속성에 액세스하는 것입니다. 선행 작업의 Exception 속성을 검사할 수도 있습니다. 자세한 내용은 예외 처리를 참조하세요. 다음 예에서는 상태가 TaskStatus.RanToCompletion인 경우에만 선행 항목의 Task<TResult>.Result 속성에 액세스하도록 이전 예를 수정합니다.

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?

연속 작업 취소

다음과 같은 경우 연속 작업의 Task.Status 속성이 TaskStatus.Canceled 로 설정됩니다.

작업과 해당 연속 작업이 동일한 논리 작업의 두 부분을 나타내는 경우 다음 예제와 같이 두 작업에 모두 동일한 취소 토큰을 전달할 수 있습니다. 취소 토큰은 33으로 나눌 수 있는 정수 목록을 생성하는 선행 작업으로 구성되며 연속 작업에 전달됩니다. 그런 다음 연속 작업이 목록을 표시합니다. 선행 작업과 연속 작업은 모두 임의 간격 동안 정기적으로 일시 중지됩니다. 또한 System.Threading.Timer 개체는 5초 시간 제한 간격 후에 Elapsed 메서드를 실행하는 데 사용됩니다. 이 예제에서는 현재 실행 중인 작업이 CancellationToken.ThrowIfCancellationRequested 메서드를 호출하게 하는 CancellationTokenSource.Cancel 메서드를 호출합니다. 선행 작업이나 해당 연속 작업이 실행 중일 때 CancellationTokenSource.Cancel 메서드를 호출할지 여부는 임의로 생성된 일시 중지 기간에 따라 달라집니다. 선행 작업이 취소되면 연속 작업이 시작되지 않습니다. 선행 작업이 취소되지 않은 경우에도 토큰을 사용하여 계속 작업을 취소할 수 있습니다.

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 옵션을 지정하여 토큰을 제공합니다.

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 에 따라 이후의 연속 작업에 영향을 줄 수 있습니다.

삭제된 연속은 시작되지 않습니다.

연속 작업 및 자식 작업

연속 작업은 선행 작업 및 연결된 모든 자식 작업이 완료될 때까지 실행되지 않습니다. 연속은 분리된 자식 작업이 완료될 때까지 기다리지 않습니다. 다음 두 예제에서는 연속 작업을 만드는 선행 작업에 연결된 자식 작업과 분리된 자식 작업을 보여 줍니다. 다음 예에서 연속은 모든 자식 작업이 완료된 후에만 실행되며 예를 여러 번 실행하면 매번 동일한 출력이 생성됩니다. 기본적으로 Task.Run 메서드는 기본 작업 생성 옵션이 TaskCreationOptions.DenyChildAttach인 부모 작업을 만들기 때문에 예제에서는 TaskFactory.StartNew 메서드를 호출하여 선행 작업을 시작합니다.

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

그러나 자식 작업이 선행 작업에서 분리된 경우 자식 작업의 상태에 관계없이 선행 작업이 종료된 즉시 연속 작업이 실행됩니다. 따라서 다음 예제를 여러 번 실행하면 작업 스케줄러가 각 자식 작업을 처리한 방식에 따라 다른 출력이 생성될 수 있습니다.

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

선행 작업의 최종 상태는 연결된 자식 작업의 최종 상태에 따라 달라집니다. 분리된 자식 작업의 상태는 부모 작업에 영향을 주지 않습니다. 자세한 내용은 연결된 자식 작업과 분리된 자식 작업을 참조하세요.

연속 작업에 상태 연결

연속 작업에 임의 상태를 연결할 수 있습니다. ContinueWith 메서드는 각각 연속 상태를 나타내는 Object 값을 받는 오버로드된 버전을 제공합니다. 나중에 Task.AsyncState 속성을 사용하여 이 상태 개체에 액세스할 수 있습니다. 값을 제공하지 않으면 이 상태 개체는 null입니다.

연속 상태는 APM(비동기 프로그래밍 모델) 을 사용하는 기존 코드를 TPL을 사용하도록 변환하는 경우에 유용합니다. APM에서는 BeginMethod 메서드에 개체 상태를 제공할 수 있으며 나중에 IAsyncResult.AsyncState 속성을 사용하여 해당 상태에 액세스할 수 있습니다. APM을 사용하는 코드를 TPL을 사용하도록 변환할 때 이 상태를 유지하려면 ContinueWith 메서드를 사용합니다.

Visual Studio 디버거에서 Task 개체로 작업하는 경우에도 연속 상태가 유용할 수 있습니다. 예를 들어 병렬 작업 창의 작업 열에는 각 작업에 대한 상태 개체의 문자열 표현이 표시됩니다. 병렬 작업 창에 대한 자세한 내용은 작업 창 사용을 참조하세요.

다음 예제에서는 연속 상태를 사용하는 방법을 보여 줍니다. 연속 작업 체인을 만듭니다. 각 작업은 DateTime 메서드의 state 매개 변수에 대해 현재 시간인 ContinueWith 개체를 제공합니다. 각 DateTime 개체는 연속 작업이 만들어진 시간을 나타냅니다. 각 작업은 작업 완료 시간을 나타내는 두 번째 DateTime 개체를 결과로 생성합니다. 이 예제에서는 모든 작업이 완료된 후 만든 시간과 각 연속 작업의 완료 시간이 표시됩니다.

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
    ' Simulates 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.

작업 유형을 반환하는 연속 작업

때로는 Task 형식을 반환하는 연속을 연결해야 할 수도 있습니다. 이러한 작업을 중첩된 작업이라고 합니다. 부모 작업이 Task<TResult>.ContinueWith를 호출하고 작업을 반환하는 continuationFunction을 제공하는 경우 Unwrap을 호출하여 <Task<Task<T>>> 또는 Task(Of Task(Of T))(Visual Basic)의 비동기 작업을 나타내는 프록시 작업을 만들 수 있습니다.

다음 예제에서는 추가 작업 반환 함수를 래핑하는 연속 작업을 사용하는 방법을 보여 줍니다. 각 연속 작업을 래핑 해제하여 래핑된 내부 작업을 노출할 수 있습니다.

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 사용에 대한 자세한 내용은 방법: 중첩된 작업 래핑 해제를 참조하세요.

연속 작업에서 throw된 예외 처리

선행-계속 관계는 부모-자식 관계가 아닙니다. 연속에서 throw된 예외는 선행 항목으로 전파되지 않습니다. 따라서 다른 작업에서 처리하는 것처럼 연속 작업에서 발생한 예외를 다음과 같이 처리합니다.

  • Wait, WaitAll또는 WaitAny 메서드나 해당하는 제네릭 항목을 사용하여 연속 작업을 기다릴 수 있습니다. 다음 예제와 같이 동일한 try 문에서 선행 작업과 해당 연속 작업을 기다릴 수 있습니다.
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 속성을 관찰할 수 있습니다. 다음 예제에서는 작업이 존재하지 않는 파일을 읽으려고 합니다. 그런 다음 연속 작업이 선행 작업의 예외 정보를 표시합니다.
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이 아니라고 가정할 수 있습니다. 선행 항목에서 예외가 throw되었는지 여부에 관계없이 연속 작업이 실행되는 경우 다음 코드 조각에 표시된 것처럼 예외 처리를 시도하기 전에 선행 항목의 Exception 속성이 null이 아닌지 확인해야 합니다.

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

자세한 내용은 예외 처리를 참조하세요.

참고 항목