对任务完成的反应:`.ContinueWith()`vs`GetAwaiter()。OnCompleted()`

sml*_*mls 5 c# continuations task task-parallel-library

假设我有一个Task生成的int,以及一个接受的回调int

Task<int> task = ...;
Action<int> f = ...;
Run Code Online (Sandbox Code Playgroud)

现在,我想对其进行设置,以便一旦任务完成并返回其整数结果,f将使用该整数来调用callfack ,而无需主线程等待任务完成:

  1. 据我了解,典型的解决方案是Task.ContinueWith方法:

    task.ContinueWith(t => f(t.Result));
    
    Run Code Online (Sandbox Code Playgroud)
  2. 但是,也可以TaskAwaiter为任务获取一个,并使用其类似事件的界面:

    TaskAwaiter<int> awaiter = task.GetAwaiter();
    awaiter.OnCompleted(() => f(awaiter.GetResult()));
    
    Run Code Online (Sandbox Code Playgroud)

现在,我意识到这TaskAwaiter并不是应用程序代码中的常用对象,主要存在于内部供await关键字使用。
但是,为了加深我对TPL的理解,我想知道:

解决方案(1)和(2)之间有什么实际区别?

例如,

  • ...关于在哪个线程上调用回调?
  • ...关于在任务或回调中引发异常时会发生什么?
  • ...还有其他副作用吗?

Evk*_*Evk 5

一个区别是

task.ContinueWith(t => f(t.Result));
Run Code Online (Sandbox Code Playgroud)

不会捕获当前同步上下文,并且在 UI 应用程序中 - 回调将在线程池线程上执行。尽管

TaskAwaiter<int> awaiter = task.GetAwaiter();
awaiter.OnCompleted(() => f(awaiter.GetResult()));
Run Code Online (Sandbox Code Playgroud)

将捕获同步上下文,并且回调将在 UI 线程上执行。

当然你也可以这样做ContinueWith

task.ContinueWith(r => f(r.Result), TaskScheduler.FromCurrentSynchronizationContext());
Run Code Online (Sandbox Code Playgroud)

但这不是您所使用的问题。因此,您的问题中提供的方式至少在这方面是不同的。

异常表示也存在差异,Task.Result如果任务发生故障,则访问将抛出AggregateException(一个或多个异常作为内部异常),而访问awaiter.GetResult()不会将抛出的异常包装起来AggregateException,并将按原样重新抛出(如果有多个异常,例如from Task.WhenAll- 除一个之外的所有内容都将被忽略,并且只有一个将被抛出)。