在異步程式設計中,一個異步操作通常會在完成時調用第二個作業。 接續允許子系作業取用第一個作業的結果。 傳統上,接續的實現是通過使用回調函數來完成的。 在任務並行程式庫(TPL)中,延續任務提供相同的功能。 接續工作(亦稱為接續)是由稱為前項的另一個工作在完成後所叫用的異步工作。
接續相對容易使用,但功能強大且有彈性。 例如,您可以:
- 將數據從前項傳遞至接續。
- 指定要叫用或未叫用接續的精確條件。
- 在接續開始或合作執行之前取消接續。
- 提供有關如何安排後續的提示。
- 從同一個先前元素調用多個延續。
- 當所有或任一個前項完成時,叫用一個接續。
- 鏈結可以一個接一個地延續到任意長度。
- 使用延續來處理前置作業丟出的例外情況。
關於接續
延續任務是在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.
建立多個前項的接續
您也可以建立一個後續事項,以在一組任務中的任何或所有任務完成時執行。 若要在所有前項工作完成時執行接續,您可以呼叫靜態 (Shared
在 Visual Basic 中) Task.WhenAll 方法或實例 TaskFactory.ContinueWhenAll 方法。 若要在任何前項工作完成時執行接續,您可以呼叫 static (Shared
在 Visual Basic 中) Task.WhenAny 方法或實例 TaskFactory.ContinueWhenAny 方法。
對Task.WhenAll和Task.WhenAny重載的呼叫不會封鎖呼叫執行緒。 不過,您通常會呼叫除 Task.WhenAll(IEnumerable<Task>) 和 Task.WhenAll(Task[]) 方法以外的所有方法,以擷取傳回的 Task<TResult>.Result 屬性,這會阻塞呼叫線程。
下列範例會呼叫 Task.WhenAll(IEnumerable<Task>) 方法來建立接續工作,以反映其10個前項工作的結果。 每個前項工作會平方一個從一到 10 的索引值。 如果前項成功完成 (其 Task.Status 屬性為 TaskStatus.RanToCompletion),則 Task<TResult>.Result 接續的 屬性是每個前項所傳回值的陣列 Task<TResult>.Result 。 此範例會新增它們,以計算介於一到 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 列舉值的多載來指定接續開始的條件。 例如,您可以指定只有在前項成功完成時,或只有在完成錯誤狀態時,才能執行接續。 如果在前提準備好叫用時條件不成立,接續會直接轉換至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 物件會用來在五秒的逾時間隔之後執行 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 中,您可以在 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 類型的接續。 這些工作稱為巢狀工作。 當父工作呼叫 Task<TResult>.ContinueWith 並提供一個傳回工作的 continuationFunction
時,您可以呼叫 Unwrap 來建立代表 <Task<Task<T>>>
或 Task(Of Task(Of T))
異步操作的 Proxy 工作(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 的詳細資訊,請參閱 解開巢狀 Task 的步驟。
處理從後續拋出的例外狀況
前項接續關聯性不是父子式關聯性。 接續擲回的例外狀況不會傳播至前項。 因此,處理由連續操作擲出的例外狀況時,依照您在其他任務中處理例外狀況的方式進行,如下所示:
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 選項所建立的附加子工作,其例外狀況會由父系傳播回呼叫線程,如同任何其他附加子系的情況一樣。 如需詳細資訊,請參閱 附加和分離的子工作。