비동기 프로그래밍에서는 하나의 비동기 작업이 완료 시 두 번째 작업을 호출하는 것이 일반적입니다. 연속 작업을 사용하면 하위 작업이 첫 번째 작업의 결과를 사용할 수 있습니다. 일반적으로 콜백 메서드를 사용하여 연속 작업을 수행했습니다. TPL(작업 병렬 라이브러리)에서는 연속 작업동일한 기능을 제공합니다. 연속 작업(연속 작업이라고도 함)은 선행 작업이 완료될 때 선행알려진 다른 태스크에서 호출하는 비동기 작업입니다.
연속 작업은 비교적 사용하기 쉽지만 강력하고 유연합니다. 예를 들어, 당신은 다음을 할 수 있습니다:
- 선행 단계에서 후속 단계로 데이터를 전달합니다.
- 연속이 호출되거나 호출되지 않을 정확한 조건을 지정합니다.
- 작업이 시작되기 전에 취소하거나 실행 중일 때 협력하여 취소합니다.
- 연속 작업을 예약하는 방법에 대한 힌트를 제공합니다.
- 동일한 선행 조건에서 여러 가지 연속 실행을 호출합니다.
- 여러 선행 작업 중 하나 또는 전부가 완료되면 하나의 연속을 호출합니다.
- 체인을 어떤 임의의 길이로 계속 연결합니다.
- 연속 작업을 사용하여 선행 작업에서 throw된 예외를 처리합니다.
연속성에 대하여
계속 작업은 WaitingForActivation 상태에서 생성된 작업입니다. 선행 작업 또는 작업이 완료되면 자동으로 활성화됩니다. 사용자 코드의 계속에서 Task.Start을 호출하는 경우 System.InvalidOperationException 예외가 발생합니다.
지속은 그 자체가 Task이며, 시작된 스레드를 차단하지 않습니다. 연속 작업이 완료될 때까지 차단하려면 Task.Wait 메서드를 호출합니다.
단일 선행 작업에 대한 연속 작업 만들기
Task.ContinueWith 메서드를 호출하여 선행 작업이 완료될 때 실행되는 연속 작업을 만듭니다. 다음 예제에서는 기본 패턴을 보여 줍니다(명확성을 위해 예외 처리는 생략됨). 현재 요일의 이름을 나타내는 taskA
개체를 반환하는 선행 작업 DayOfWeek 실행합니다.
taskA
완료되면 antecedent
ContinueWith
연속 메서드의 결과를 나타냅니다. 선행 작업의 결과는 콘솔에 기록됩니다.
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.
여러 선행 작업에 대한 연속 작업 만들기
태스크 그룹이 모두 완료될 때 실행되는 연속 작업을 만들 수도 있습니다. 모든 선행 작업이 완료되면 연속 작업을 실행하려면 정적(Visual Basic의Shared
) Task.WhenAll 메서드 또는 인스턴스 TaskFactory.ContinueWhenAll 메서드를 호출할 수 있습니다. 선행 작업이 완료되면 연속 작업을 실행하려면 정적(Visual Basic의Shared
) Task.WhenAny 메서드 또는 인스턴스 TaskFactory.ContinueWhenAny 메서드를 호출할 수 있습니다.
Task.WhenAll 및 Task.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.LongRunning 열거형에 해당 값이 있는 TaskContinuationOptions.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 속성을 검사할 수도 있습니다. 자세한 내용은 예외 처리참조하세요. 다음 예제에서는 이전 예제를 수정하여 해당 상태가 Task<TResult>.Result경우에만 선행 작업의 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?
이어지기 취소
연속 작업의 Task.Status 속성은 다음 상황들에서는 TaskStatus.Canceled으로 설정됩니다.
취소 요청에 대한 응답으로 OperationCanceledException 예외가 발생합니다. 다른 작업과 마찬가지로 예외에 연속으로 전달된 동일한 토큰이 포함된 경우 협조적 취소 승인으로 처리됩니다.
연속체는 System.Threading.CancellationToken로 전달되며, 그 IsCancellationRequested 속성이
true
입니다. 이 경우 연속이 시작되지 않고 TaskStatus.Canceled 상태로 전환됩니다.TaskContinuationOptions 인수로 설정된 조건이 충족되지 않아 연속이 실행되지 않습니다. 예를 들어, 선행 작업이 TaskStatus.Faulted 상태로 전환되면 TaskContinuationOptions.NotOnFaulted 옵션을 받은 연속 작업은 실행되지 않지만 Canceled 상태로 전환됩니다.
작업과 해당 연속 작업이 동일한 논리 작업의 두 부분을 나타내는 경우 다음 예제와 같이 두 작업 모두에 동일한 취소 토큰을 전달할 수 있습니다. 33으로 나누어 떨어지는 정수 목록을 생성하는 선행 요소로 구성되며, 이는 연속 요소로 전달됩니다. 다음 단계에서 목록을 표시합니다. 선행 작업과 연속 작업 모두 임의 간격으로 정기적으로 일시 중지됩니다. 또한 System.Threading.Timer 개체는 5초 제한 시간 간격 후에 Elapsed
메서드를 실행하는 데 사용됩니다. 이 예제에서는 현재 실행 중인 작업이 CancellationTokenSource.Cancel 메서드를 호출하게 하는 CancellationToken.ThrowIfCancellationRequested 메서드를 호출합니다. 선행 작업 또는 연속 작업이 실행될 때 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: {task.Status}");
Console.WriteLine($"Continuation Status: {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 따라 다음 연속 작업에 영향을 줄 수 있습니다.
삭제된 연속 작업은 시작되지 않습니다.
연속 작업 및 자식 작업
선행 작업과 연결된 모든 자식 작업이 완료될 때까지 연속 작업이 실행되지 않습니다. 연속 작업은 분리된 자식 작업이 완료되기를 기다리지 않습니다. 다음 두 예제에서는 연속성을 만드는 이전 작업에 연결되었다가 이전 작업에서 분리되는 자식 작업을 보여줍니다. 다음 예제에서는 모든 자식 작업이 완료된 후에만 연속 작업이 실행되고 예제의 여러 실행에서 매번 동일한 출력을 생성합니다. 이 예제에서는 기본적으로 TaskFactory.StartNew 메서드가 기본 작업 만들기 옵션이 Task.Run부모 작업을 만들기 때문에 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
그러나 자식 작업이 선행 작업에서 분리된 경우 자식 작업의 상태에 관계없이 선행 작업이 종료되는 즉시 연속이 실행됩니다. 따라서 다음 예제의 여러 실행은 작업 스케줄러가 각 자식 작업을 처리하는 방법에 따라 달라지는 변수 출력을 생성할 수 있습니다.
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에서 Begin메서드 메서드에서 개체 상태를 제공할 수 있으며 나중에 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된 예외는 선행 작업으로 전파되지 않습니다. 따라서 다른 작업에서와 마찬가지로 연속 작업에서 발생한 예외를 다음과 같이 처리합니다.
-
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
않는다고 가정할 수 있습니다. 선행 작업에서 예외가 발생하든 발생하지 않든 연속 작업이 실행될 경우, 예외를 처리하기 전에 다음 코드 조각과 같이 선행 작업의 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
자세한 내용은 예외 처리참조하세요.
- 연속 작업이 TaskContinuationOptions.AttachedToParent 옵션을 사용하여 만든 연결된 자식 작업인 경우 다른 연결된 자식의 경우와 마찬가지로 부모에 의해 해당 예외가 호출 스레드로 다시 전파됩니다. 자세한 내용은 연결된 자식 작업 및 분리된 자식 작업을 참조하세요.
참고하십시오
.NET