C# 继续使用(而不是滥用)

C# 继续使用(而不是滥用),c#,asynchronous,task-parallel-library,C#,Asynchronous,Task Parallel Library,假设我们有两个辅助函数: void Step1(); // Maybe long. void Step2(); // Might be short clean up of step 1. 我经常看到: Task.Run(() => Step1()).ContinueWith(t => Step2()); 它创建了两个在系列中运行的任务。 当: 创建一个单个任务,运行系列中的两个函数,这似乎是一个更简单的选择 是否有常识指南可用于确定在较简单的方法中何时实际需要延续? 上述示例没有

假设我们有两个辅助函数:

void Step1(); // Maybe long.
void Step2(); // Might be short clean up of step 1.
我经常看到:

Task.Run(() => Step1()).ContinueWith(t => Step2());
它创建了两个在系列中运行的任务。 当:

创建一个单个任务,运行系列中的两个函数,这似乎是一个更简单的选择

是否有常识指南可用于确定在较简单的方法中何时实际需要延续?

上述示例没有异常处理-异常处理对这些指导原则的影响程度如何?

ContinueWith
将运行,即使第一个任务产生异常(检查
t.exception


try..catch
语句中,可以将带有
ContinueWith
视为异步
finally
。这正是它有用的原因。当调用
ContinueWith
时,您还可以细化纹理。

有两个主要原因:

  • 构图
  • ContinueWith
    方法允许您轻松组合许多不同的任务,并使用助手方法构建“continuence树”。将此更改为命令式调用限制了这一点-这仍然是可能的,但任务比命令式代码更易于组合

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

    是否有可用于确定的常识指导原则 在较简单的方法中,实际需要延续时

    ContinueWith
    使您能够仅在某些情况下通过调用
    Step2
    ,例如
    onlyOnCancelled
    OnlyOnFaulted
    OnlyOnRanToCompletion
    ,等等。这样,您就可以编写适合每种情况的工作流

    您也可以通过一个
    任务来完成此任务。运行
    并尝试catch
    ,但这可能需要您进行更多维护


    就我个人而言,我试图避免使用
    ContinueWith
    ,因为我发现
    async wait
    的冗长程度更低,更像是同步的。我宁愿
    等待
    尝试捕获

    通常,使用最少的持续时间。它们会使代码变得混乱,并降低性能成本

    这样做的一个原因是异常行为。即使第一个任务失败,继续操作也将运行。在这里,据我所知没有错误行为。在这段特定的代码中,这似乎不是一个问题。您可能需要从
    t
    处理异常

    通常,人们会认为“我有一条管道!”并将管道分解为多个步骤。这是很自然的想法。但是管道步骤不一定需要以延续的形式表现出来。它们只能是按顺序排列的方法调用

    是否有可用于确定的常识指导原则 在较简单的方法中,实际需要延续时

    当您:

    • 需要使用TPL(例如,您使用第三方库中返回任务的方法)
    • 可能需要使用回调链
    • 可能需要仅对那些完成执行并产生特定结果的任务(例如错误任务)运行某些回调方法
    在以下情况下,您可能会使用更简单的方法:

    • 您使用的方法不会返回任务
    • 您只需要在一个线程上同步调用两个或多个方法

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

    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); 
        }); 
    
    使用
    继续使用

     double[] nums = { 3,5,2,6,5,4,3 };
     await Task.Run( () => await GetSum(nums))
         .ContinueWith( t => await SubtractValue(nums, t.Result));
    
    在这种特殊情况下,没有语法增益,但是如果没有ContinueWith的帮助,可能会有更复杂的示例无法编写:

    还值得注意的是,编译器足够聪明,可以在两种不同的情况下优化相同的代码,因此您必须选择(除非您需要特定的行为)哪种方式对您来说更清楚


    我希望我没有犯错误,我没有检查代码是否编译,很抱歉,但是在这里太晚了,我明天会更正答案,以防万一。

    性能可能不是问题-任何使用CPU任务的地方都会出现额外延续带来的开销可以忽略不计的情况。当然,除非您使用的是GUI调度程序rse…当您有两个以上的步骤时,维护
    try-catch
    es特别困难,其中一些步骤在出现故障时应该运行,而另一些步骤不应该运行。这种情况会发生。当然,您可以使用类似错误单子的东西来代替
    任务
    等待
    对于异步I/O的继续是一个很好的选择,但是有时,你仍然需要使用
    继续操作,例如,在等待操作树时,而不是一个“堆栈”。@Luaan这很难而且不可读。保持这样的多个连续操作会很痛苦。你不必完全手动操作,的一大好处是
    
     double[] nums = { 3,5,2,6,5,4,3 };
     await Task.Run( () => await GetSum(nums))
         .ContinueWith( t => await SubtractValue(nums, t.Result));