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;
}