使用(而不是滥用)ContinueWith

7
假设我们有两个工作函数:
void Step1(); // Maybe long.
void Step2(); // Might be short clean up of step 1.

我经常看到:

Task.Run(() => Step1()).ContinueWith(t => Step2());

这将创建2个任务,并按顺序运行。

当:
Task.Run(() => { Step1(); Step2(); });

创建一个在序列中运行2个函数的单一任务,这似乎是一个更加简单的选择。是否存在可以应用于确定何时需要继续操作而不是采用更简单方法的常识准则?
上述示例没有异常处理-异常处理对这些指南有多大影响?

如果您有两个需要按顺序调用的同步方法,则创建一个任务来完成此工作是有意义的。Task.ContinueWith 是一种特殊机制,可以将异步方法组合成同步作业。 - Sinatr
6个回答

6
有没有通用准则可用于确定何时需要使用连续性而不是更简单的方法?
“ContinueWith”通过 TaskContinuationOptions为您提供了仅在某些条件下调用Step2的能力,例如OnlyOnCanceled, OnlyOnFaulted, OnlyOnRanToCompletion等。这样,您可以为每种情况组合适合的工作流程。
您也可以使用单个Task.Run和一个try-catch来实现此目的,但这可能更难维护。
就我个人而言,我尽量避免使用ContinueWith,因为我发现async-await不那么冗长,更像同步。我宁愿在try-catch中使用await

当你有超过两个步骤时,有些步骤应该在出错时运行,而有些步骤则不应该。这时候维护try-catch就会变得特别困难。当然,你可以使用错误单子(error monad)代替Taskawait是异步I/O的延续的好选择,但有时你仍然需要使用ContinueWith,例如在等待操作树而不是“堆栈”时。 - Luaan
1
@Luaan 这很难读懂。如果要维护多个延续,那将是一种痛苦。 - Yuval Itzchakov
你不必完全手动完成 - Task(例如错误单子)的一个巨大好处是它们很容易组合。例如,整个错误处理可以在帮助方法中处理(例如HandleErrors(anotherTask)),这使得阅读和编写变得非常容易。试着用try-catch做同样的事情 :D - Luaan

3

我认为有两个主要原因:

  1. 组合性

ContinueWith方法允许您轻松地组合许多不同的任务,并使用辅助方法构建“继续树”。将其更改为命令式调用会限制它 - 当然仍然是可能的,但是任务比命令式代码更具可组合性。

  1. 错误处理

ContinueWith情况下,即使Step1抛出异常,Step2也始终运行。当然,这可以通过使用try子句来模拟,但这有点棘手。最重要的是,它不具备组合性,并且不易扩展 - 如果您发现必须运行多个步骤,每个步骤都有自己的错误处理,那么您将会在使用try-catch时遇到很大困难。当然,Task不是解决此问题的唯一方法,也不一定是最好的方法 - 错误单子将使您能够轻松地组合相互依赖的操作。


3
通常情况下,尽量使用最少的续体。它们会使代码混乱并影响性能。其中一个原因是异常处理行为。即使第一个任务失败,续体也会运行。在这里,据我所知没有错误行为。这似乎不是这段代码中的问题。你需要以某种方式处理t的异常。通常人们会想:“我有一个流水线!”并将管道分解成步骤。这是很自然的想法。但管道步骤不一定需要表现为续体的形式。它们可以只是顺序方法调用。

性能可能不是问题 - 任何你会使用 CPU 任务的地方,额外 continuation 带来的开销都是可以忽略不计的。当然,除非你在使用 GUI 调度器... - Luaan

1
ContinueWith会运行,即使第一个任务出现异常(请检查t.Exception)。 ContinueWith可以被视为在try..catch语句中异步执行finally的方式。这正是它有用的地方。此外,您可以精细控制ContinueWith的调用时机。

0
有没有通用的准则可以应用来确定何时需要使用 continuation,而不是简单的方法?
当你需要以下情况时,最可能会使用 ContinueWith
  • 需要使用 TPL(例如,你使用来自第三方库的返回 Task 的方法)
  • 可能需要使用回调链
  • 可能需要仅对以某种结果(例如故障任务)完成执行的任务运行某些回调方法
当你使用不返回 Task 的方法时,或者只想在一个线程上同步调用两个或多个方法时,很可能会使用更简单的方法。

0

首先,我假设您需要执行异步作业:

public async void Step1(){ /* bla*/ }

public async void Step2(){ /* bla*/ }

然后调用应该是:

public async void taskRunner(){
    await Task.Run(() => { await Step1(); await Step2(); }); 
} 

为确保它像继续执行一样工作,您必须添加异常处理(第二个任务始终会执行,即使第一个任务以异常结束)。

public async void taskRunner(){
    await Task.Run(() => { 
        try{
            await Step1(); 
        }catch(Exception e){

        }
        await Step2(); 
    }); 
} 

另一个要点是任务可以是lambda表达式,因此它们可以仅使用前一个任务的结果并将其解包到下一个任务中,如果没有使用ContinueWith则需要额外的变量:
 double[] nums = { 3,5,2,6,5,4,3 };
 await Task.Run(() => { //still missing exception handling here ;)
        double result = await GetSum( nums); 
        await SubtractValue(nums, result); 
    }); 

使用ContinueWith

 double[] nums = { 3,5,2,6,5,4,3 };
 await Task.Run( () => await GetSum(nums))
     .ContinueWith( t => await SubtractValue(nums, t.Result));

在这种特定情况下,没有语法上的收益,但可能存在更复杂的例子,如果没有ContinueWith的帮助,就无法编写:
还值得注意的是,编译器足够聪明,可以将2种不同情况下的代码优化为相同的代码,因此您必须选择(除非您需要特定的行为)哪种方式更清晰。
我希望我没有犯错误,我没有检查代码是否编译,对此我很抱歉,因为现在已经很晚了,我会在明天纠正答案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接