C# 异步任务、取消和异常
我目前正在学习如何使用C# 异步任务、取消和异常,c#,task-parallel-library,task,taskcompletionsource,C#,Task Parallel Library,Task,Taskcompletionsource,我目前正在学习如何使用Tasks正确地公开库API的异步部分,以便客户能够更轻松、更好地使用它们。我决定在它周围包装一个任务,它不会在线程池中被调度(在这里的实例中,无论如何都不需要,因为它基本上只是一个计时器)。这很好,但是取消现在有点让人头疼 该示例显示了在令牌上注册委托的基本用法,但比我的情况稍微复杂一些,更重要的是,我不确定如何处理TaskCanceledException。表示只返回并将任务状态切换到RanToCompletion,或抛出OperationCanceledExcepti
Task
s正确地公开库API的异步部分,以便客户能够更轻松、更好地使用它们。我决定在它周围包装一个任务
,它不会在线程池中被调度(在这里的实例中,无论如何都不需要,因为它基本上只是一个计时器)。这很好,但是取消现在有点让人头疼
该示例显示了在令牌上注册委托的基本用法,但比我的情况稍微复杂一些,更重要的是,我不确定如何处理TaskCanceledException
。表示只返回并将任务状态切换到RanToCompletion
,或抛出OperationCanceledException
(这会导致任务的结果被取消
)都可以。但是,这些示例似乎只涉及或至少提到通过传递给TaskFactory.StartNew
的委托启动的任务
我目前的代码(大致)如下:
public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
// Cancellation
token.Register(() => {
tcs.TrySetCanceled();
CancelAndCleanupFoo(foo);
});
RunFoo(foo, callback);
return tcs.Task;
}
公共任务运行(IFoo-foo,CancellationToken=default(CancellationToken)){
var tcs=new TaskCompletionSource();
//常规精加工处理器
EventHandler回调=(发送方,参数)=>tcs.TrySetResult(null);
//取消
令牌.寄存器(()=>{
tcs.trysetconceled();
取消和清理foo(foo);
});
RunFoo(foo,回调);
返回tcs.Task;
}
(执行过程中没有结果,也没有可能的异常;我选择从这里开始,而不是从库中更复杂的地方开始的原因之一。)
在当前表单中,当我在TaskCompletionSource
上调用TrySetCanceled
时,如果我等待返回的任务,我总是会得到一个TaskCanceledException
。我的猜测是,这是正常的行为(我希望是),当我想使用取消功能时,我应该在通话中使用try
/catch
如果我不使用TrySetCanceled
,那么我最终将在finish回调中运行,任务看起来正常完成。但是我想如果用户想要区分正常完成的任务和被取消的任务,那么TaskCanceledException
几乎就是确保这一点的副作用,对吗
还有一点我不太明白:任何异常,即使是与取消相关的异常,都被TPL包装在一个aggregateeException
中。然而,在我的测试中,我总是直接获得TaskCanceledException
,而不使用任何包装器。我是否遗漏了一些东西,或者只是记录得很差
TL;医生:
- 对于要转换到
状态的任务,始终需要相应的异常,用户必须围绕异步调用包装cancelled
/try
,才能检测到该异常,对吗catch
- 被抛出的
被解除包装也是正常的,我在这里没有做错什么TaskCanceledException
动画,执行它(显然是异步的),然后向您发出信号,表示它已完成
这不是一个实际的任务,因为它不是一个必须在线程上运行的工作。这是一个异步操作,在.NET中使用任务对象公开
此外,实际上并不是取消某些内容,而是停止动画。这是一个完全正常的操作,所以它不应该抛出异常。如果您的方法返回一个解释动画是否完成的值,则更好,例如:
public Task<bool> Run(IAnimation animation, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<bool>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(true);
// Cancellation
token.Register(() => {
CleanupFoo(animation);
tcs.TrySetResult(false);
});
RunFoo(animation, callback);
return tcs.Task;
}
更新
这可以通过一些C#7技巧进一步改进
例如,您可以使用本地函数,而不是使用回调和lambda。除了使代码更干净之外,它们不会在每次调用时分配一个委托。更改不需要客户方的C#7支持:
Task<bool> Run(IAnimation animation, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<bool>();
// Regular finish handler
void OnFinish (object sender, EventArgs args) => tcs.TrySetResult(true);
void OnStop(){
CleanupFoo(animation);
tcs.TrySetResult(false);
}
// Null-safe cancellation
token.Register(OnStop);
RunFoo(animation, OnFinish);
return tcs.Task;
}
模式匹配实际上也比类型检查快。这要求客户端支持C#7 您正在做的很好-任务表示将来会有结果的一些操作,不需要在另一个线程或类似的线程上运行任何操作。使用标准的对消方法,而不是返回布尔值,这是非常正常的
回答您的问题:当您执行tcs.trysetcancelled()
时,它会将任务移动到已取消状态(task.IsCancelled
将为true),此时不会引发异常。但是,当您等待此任务时,它会注意到任务已取消,此时将抛出taskcancelledeexception
。这里没有任何内容包装到聚合异常中,因为实际上没有任何内容要包装-TaskCancelledException
作为wait
逻辑的一部分抛出。现在,如果您将执行类似于task.Wait()
的操作,那么它将按照您的预期将taskcancelledeexception
包装到aggregateeexception
中
请注意,await
unwrapps AggregateExceptions,因此您可能永远不会期望await task
抛出AggregateException-在出现多个异常的情况下,只会抛出第一个异常-其余异常将被吞没
现在,如果您在常规任务中使用取消令牌,情况会有所不同。当您执行类似于token.ThrowIfCancellationRequested
的操作时,它实际上会抛出OperationCanceledException
(请注意,它不是TaskCanceledException
,而是TaskCanceledException
的子类)。然后,如果CancellationToken
用于引发此异常,则
Task<bool> Run(IAnimation animation, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<bool>();
// Regular finish handler
void OnFinish (object sender, EventArgs args) => tcs.TrySetResult(true);
void OnStop(){
CleanupFoo(animation);
tcs.TrySetResult(false);
}
// Null-safe cancellation
token.Register(OnStop);
RunFoo(animation, OnFinish);
return tcs.Task;
}
interface IResult{}
public class Success:IResult{}
public class Stopped {
public int Frame{get;}
Stopped(int frame) { Frame=frame; }
}
....
var result=await Run(...);
switch (result)
{
case Success _ :
Console.WriteLine("Finished");
break;
case Stopped s :
Console.WriteLine($"Stopped at {s.Frame}");
break;
}
public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<AsyncCompletedEventArgs> callback = (sender, args) =>
{
if (args.Cancelled)
{
tcs.TrySetCanceled(token);
CleanupFoo(foo);
}
else
tcs.TrySetResult(null);
};
RunFoo(foo, token, callback);
return tcs.Task;
}
public Task Run(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
private Task DoRun(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
public async Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
using (token.Register(() =>
{
tcs.TrySetCanceled(token);
CleanupFoo();
});
{
var task = DoRun(foo);
try
{
await task;
tcs.TrySetResult(null);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}
await tcs.Task;
}