延续任务
更新:2010 年 6 月
在异步编程中,一个异步操作在完成时调用另一个操作并将数据传递到其中的情况非常常见。 传统上,此过程是通过使用回调方法完成的。 在任务并行库中,“延续任务”提供了同样的功能。 延续任务(也简称为“延续”)是一个异步任务,由另一个任务(称为“前面的任务”)在完成时调用。
尽管延续相对容易使用,但也十分强大和灵活。 例如,您可以:
将数据从前面的任务传递到延续
指定将调用或不调用延续所依据的精确条件
在延续启动之前取消延续,或在延续正在运行时以协作方式取消延续
提供有关应如何安排延续的提示
从同一前面的任务中调用多个延续
在多个前面的任务中的全部或任意任务完成时调用一个延续
将延续依次相连,形成任意长度
使用延续来处理前面的任务所引发的异常
通过使用 Task.ContinueWith 方法创建延续。 下面的示例演示基本模式(为清楚起见,省略了异常处理)。
' The antecedent task. Can also be created with Task.Factory.StartNew.
Dim taskA As Task(Of DayOfWeek) = New Task(Of DayOfWeek)(Function()
Return DateTime.Today.DayOfWeek
End Function)
' The continuation. Its delegate takes the antecedent task
' as an argument and can return a different type.
Dim continuation As Task(Of String) = taskA.ContinueWith(Function(antecedent)
Return String.Format("Today is {0}", antecedent.Result)
End Function)
' Start the antecedent.
taskA.Start()
' Use the contuation's result.
Console.WriteLine(continuation.Result)
// The antecedent task. Can also be created with Task.Factory.StartNew.
Task<DayOfWeek> taskA = new Task<DayOfWeek>(() => DateTime.Today.DayOfWeek);
// The continuation. Its delegate takes the antecedent task
// as an argument and can return a different type.
Task<string> continuation = taskA.ContinueWith((antecedent) =>
{
return String.Format("Today is {0}.",
antecedent.Result);
});
// Start the antecedent.
taskA.Start();
// Use the contuation's result.
Console.WriteLine(continuation.Result);
您还可以创建一个多任务延续,该延续将在一系列任务中的任意或全部任务完成时运行,如下面的示例所示。
Dim task1 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 34
End Function)
Dim task2 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 8
End Function)
Dim tasks() As Task(Of Integer) = {task1, task2}
Dim continuation = Task.Factory.ContinueWhenAll(tasks, Sub(antecedents)
Dim answer As Integer = tasks(0).Result + tasks(1).Result
Console.WriteLine("The answer is {0}", answer)
End Sub)
task1.Start()
task2.Start()
continuation.Wait()
Task<int>[] tasks = new Task<int>[2];
tasks[0] = new Task<int>(() =>
{
// Do some work...
return 34;
});
tasks[1] = new Task<int>(() =>
{
// Do some work...
return 8;
});
var continuation = Task.Factory.ContinueWhenAll(
tasks,
(antecedents) =>
{
int answer = tasks[0].Result + tasks[1].Result;
Console.WriteLine("The answer is {0}", answer);
});
tasks[0].Start();
tasks[1].Start();
continuation.Wait();
延续创建时的状态为 WaitingForActivation,因此延续只能由其前面的任务来启动。 若在用户代码中对延续调用 Task.Start,将引发 System.InvalidOperationException。
延续本身是 Task,并不阻止它在其上启动的线程。 使用等待方法进行阻止,直到延续任务完成。
延续选项
在创建单任务延续时,您可以使用 ContinueWith 重载,该重载采用 System.Threading.Tasks.TaskContinuationOptions 枚举来指定前面的任务要启动延续所依据的条件。 例如,您可以将延续指定为仅在前面的任务已完成运行时运行,或仅在前面的任务完成时处于错误状态时运行,诸如此类。 如果该条件在前面的任务准备调用延续时未得到满足,则延续将直接转换为 Canceled 状态,之后将无法启动。 如果为多任务延续指定 NotOn 或 OnlyOn 选项中的任意一个,则在运行时将引发异常。
System.Threading.Tasks.TaskContinuationOptions 枚举还包括与 System.Threading.Tasks.TaskCreationOptions 枚举相同的选项。 AttachedToParent、LongRunning 和 PreferFairness 在这两种枚举类型中具有相同的意义和值。 可将这些选项用于多任务延续。
下表列出了 TaskContinuationOptions 中的所有值。
元素 |
说明 |
---|---|
指定在未指定 TaskContinuationOptions 时的默认行为。 将在前面的任务完成时安排延续,而不管前面的任务的最终状态如何。 如果任务为子任务,则会将其创建为分离的嵌套任务。 |
|
指定将对延续进行安排,以使越早安排的任务将更有可能越早运行,而越迟安排的任务将更有可能越迟运行。 |
|
指定延续将是长期运行的、粗粒度的操作。 它会向 System.Threading.Tasks.TaskScheduler 提示,过度订阅可能是合理的。 |
|
如果延续为子任务,则指定将延续附加到任务层次结构中的父级。 只有当延续前面的任务也是子任务时,延续才是子任务。 |
|
指定不应在延续前面的任务已完成运行的情况下安排延续。 |
|
指定不应在延续前面的任务引发了未处理异常的情况下安排延续。 |
|
指定不应在延续前面的任务已取消的情况下安排延续。 |
|
指定只有在前面的任务已完成运行的情况下才应安排延续。 |
|
指定只有在延续前面的任务引发了未处理异常的情况下才应安排延续。 如果使用 OnlyOnFaulted 选项,则可保证前面的任务中的 Exception 属性不为 null。 您可以使用该属性来捕获异常,并确定导致任务出错的异常。 如果您不访问 Exception 属性,则不会处理异常。 此外,如果尝试访问已取消或出错的任务的 Result 属性,则会引发一个新异常。 |
|
指定只有在延续前面的任务完成时的状态为 Canceled 的情况下才应安排延续。 |
|
适用于运行时间非常短的延续。 指定延续在理想情况应在导致前面的任务转换为其最终状态的相同线程上运行。 如果在创建延续任务时已经完成前面的任务,则系统将尝试在创建延续任务的线程上运行延续任务。 如果前面的任务的 CancellationTokenSource 已在一个 finally(在 Visual Basic 中为 Finally)块中释放,则使用此选项的延续任务将在该 finally 块中运行。 |
将数据传递到延续
对前面的任务的引用将以参数形式传递到延续的用户委托。 如果前面的任务是 System.Threading.Tasks.Task<TResult>,并且任务已完成运行,则延续可以访问任务的 Task<TResult>.Result 属性。 对于多任务延续和 Task.WaitAll 方法,参数为由前面的任务组成的数组。 当您使用 Task.WaitAny 时,参数为已完成的第一个前面的任务。
在任务完成之前,Task<TResult>.Result 将阻塞。 但是,如果任务已取消或出错,则 Result 将在代码尝试访问它时引发异常。 可通过使用 OnlyOnRanToCompletion 选项避免此问题,如下面的示例所示。
Dim aTask = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim bTask = aTask.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {0}", antecedent.Result)
End Sub,
TaskContinuationOptions.OnlyOnRanToCompletion)
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
},
TaskContinuationOptions.OnlyOnRanToCompletion);
如果希望延续即使在前面的任务未完成运行时也运行,则必须防止出现异常。 一种可行方法是测试前面的任务的状态,并且只有在状态不是 Faulted 或 Canceled 时才访问 Result。 也可以检查前面的任务的 Exception 属性。 有关更多信息,请参见异常处理(任务并行库)。
取消延续
在以下情况中,延续将转换为 Canceled 状态:
当延续引发 OperationCanceledException 以响应取消请求时。 就像任何任务一样,如果异常包含已传递到延续的相同标记,则会将其视为确认协作取消。
将 System.Threading.CancellationToken 作为参数传递到延续并且标记的 IsCancellationRequested 属性在延续运行之前为 true (True 时。 在这种情况下,延续不会启动,并且将直接转换为 Canceled 状态。
当延续由于其 TaskContinuationOptions 中设置的条件未得到满足而从不运行时。 例如,如果某个任务转换为 Faulted 状态,则该任务由 NotOnFaulted 选项创建的延续将转换为 Canceled 状态,并且将不会运行。
若要防止延续在其前面的任务已取消的情况下执行,请在创建该延续时指定 NotOnCanceled 选项。
如果任务及其延续表示同一逻辑操作的两个部分,则您可以将相同的取消标记传递到这两个任务,如下面的示例所示。
Dim someCondition As Boolean = True
Dim cts As New CancellationTokenSource
Dim task1 = New Task(Sub()
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here...
' ...
End While
End Sub,
cts.Token
)
Dim task2 = task1.ContinueWith(Sub(antecedent)
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here
' ...
End While
End Sub,
cts.Token)
task1.Start()
' ...
' Antecedent and/or continuation will
' respond to this request, depending on when it is made.
cts.Cancel()
Task task = new Task(() =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token
);
Task task2 = task.ContinueWith((antecedent) =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token);
task.Start();
//...
// Antecedent and/or continuation will
// respond to this request, depending on when it is made.
cts.Cancel();
如果未取消前面的任务,则仍然可以使用标记来取消延续。 如果已取消前面的任务,则延续将不会启动。
在延续转换为 Canceled 状态后,它可能会影响后面的后续,具体情况取决于为这些延续指定的 TaskContinuationOptions。
已释放的延续将不会启动。
延续和子任务
在前面的任务及其所有附加的子任务完成之前,延续将不会运行。 延续不会等待分离的子任务完成。 前面的任务的最终状态依赖于任何附加的子任务的最终状态。 分离的子任务的状态不影响父级。 有关更多信息,请参见嵌套任务和子任务。
处理从延续中引发的异常
前面的任务与延续之间的关系不是父/子关系。 由延续引发的异常不会传播到前面的任务。 因此,请按在任何其他任务中处理异常的方式来处理由延续引发的异常,如下所示。
Dim task1 = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim continuation = task1.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {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
Console.WriteLine("Exception handled. Let's move on.")
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
throw new InvalidOperationException();
});
try
{
t.Wait();
c.Wait();
}
catch (AggregateException ae)
{
foreach(var e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
Console.WriteLine("Exception handled. Let's move on.");
使用第二个延续来观察第一个延续的 Exception 属性。 有关更多信息,请参见异常处理(任务并行库)和如何:处理由任务引发的异常。
如果延续为子任务并且是通过使用 AttachedToParent 选项创建的,则父级会将该延续的异常传播回调用线程,就像任何其他附加子级的情况一样。 有关更多信息,请参见嵌套任务和子任务。
请参见
概念
修订记录
日期 |
修订记录 |
原因 |
---|---|---|
2010 年 6 月 |
添加了有关延续的异步行为的注释。 |
客户反馈 |