【问题标题】:What is the difference, if any, between Task.Delay.ContinueWith and async await with Task.Delay?Task.Delay.ContinueWith 和 async await with Task.Delay 之间有什么区别(如果有的话)?
【发布时间】:2018-05-22 08:32:25
【问题描述】:

这是一个 linqpad 示例,展示了在短暂延迟后异步执行方法的两种方式。这两个例子似乎都做了同样的事情。我通常会实现第一个版本(使用 Task.Delay.ContinueWith),但我也看到了使用的第二个实现(异步等待)。这两种实现之间有什么区别吗?此场景的工作 Linqpad 示例:

void Main()
{
   // Using Task.Delay.ContinueWith...
   Task.Delay(1000).ContinueWith(t => DoSomething());       

   // ... vs async await. Note that I'm not awaiting the task here
   DoSomethingAsync();
}

public void DoSomething()
{
    "Doing Something...".Dump();
}

public async Task DoSomethingAsync()
{
    await Task.Delay(1000);

    "Doing Something...".Dump();
}

阅读这篇博文https://blogs.msdn.microsoft.com/pfxteam/2012/03/24/should-i-expose-asynchronous-wrappers-for-synchronous-methods/ 后,我认为第一个实现是“正确”的,因为“DoSomethingAsync()”实际上只是将方法卸载到线程池,并且博文指出:

“不应仅仅为了卸载目的而公开异步方法:同步方法的使用者可以通过使用专门针对异步处理同步方法的功能(例如 Task.Run)轻松实现这些好处。”

但是,StackOverflow 上的这个答案提出了第二种解决方案:

Delay then execute Task

这两种实现之间有什么实际区别吗?如果“异步等待”实现也有效(或更正确),返回的任务应该怎么做?我真的不想等待它,这是一个即发即弃的操作,但我也想处理任何可能引发的异常。

在第一个实现中,我知道我可以使用 ContinueWith, OnlyOnFaulted 来处理异常。

【问题讨论】:

  • 如果不在线程池上,您认为您的延续在选项 1 下运行在哪里?
  • ContinueWith 委托将在与原始任务相同的线程上运行,因为它只是它的延续。如果您使用 await 它将使用另一个线程池线程作为其不同的任务。您可以通过输出 Thread.CurrentThread.ManagedThreadId 来检查这一点。
  • DoSomethingAsync 返回一个任务,ContinueWith 也是如此。
  • @Damien_The_Unbeliever - 我知道 ContinueWith 将在 ThreadPool 线程上运行。这就是我真正想要的。

标签: c# asynchronous async-await task-parallel-library


【解决方案1】:

它们相似,但不完全相同。例如,在SynchronizationContext.Current 之前,async 方法的延续将被调度到此同步上下文,但ContinueWith 不会,并将在线程池线程上运行。虽然使用 ContinueWith 的另一个重载,但你可以让它做同样的事情:

.ContinueWith(t => DoSomething(), TaskScheduler.FromCurrentSynchronizationContext());

您可以使用await yourTask.ConfigureAwait(false) 阻止在异步版本中调度到同步上下文。

那么,异常处理就不同了。在async 版本中,会直接抛出异常,如果有多个异常(例如来自await Task.WhenAll) - 只会抛出第一个,其余的将被吞噬。很难错过这个例外。

ContinueWith 版本中,抛出的异常用t.Exception 表示,它总是AggregateException,所以你必须解包它。另一方面 - 所有异常都存在(如果有多个)并且没有一个被吞下。但是,很容易忘记处理该异常。例如,在您问题中的代码中 - 您不处理 ContinueWith 中的异常,因此无论是否有异常,DoSomething() 继续都会被执行。在async 版本中,出现异常时不执行延续。

这两种实现都是“有效的”。您不应该忘记在这两种情况下处理异常。使用ContinueWith - 要么始终检查t.Exception,要么使用OnlyOnFaulted 安排单独的继续(并仅在此处检查)。如果是 async 版本 - 将正文包裹在 try-catch 块中。出于“一劳永逸”的性质 - 您无法在调用站点处理异常,但您不应完全放弃它们。

在您在短暂延迟后执行方法的特定情况下,我想说这只是一个偏好问题(捕获同步上下文的差异除外)。我个人更喜欢ContinueWith,它表达的意图更清晰,不需要语义不明确的单独方法。

【讨论】:

  • 两种建议的实现是否都会导致进程崩溃,如blogs.msdn.microsoft.com/pfxteam/2009/06/01/… then 中所述?
  • @JMc 异步版本不能导致这种情况。 ContinueWith 版本可以,如果您没有按照建议观察异常(通过访问 Exception 属性),并且如果您在链接中描述的行为在 .NET 版本上运行。请注意,从 .NET 4.5 开始 - 未观察到的任务异常不会使进程崩溃。
【解决方案2】:

ContinueWith() 允许您以“即发即弃”的方式链接任务。否则,您必须等待任务完成,然后执行下一个可等待操作(并且“等待”意味着您还必须使用“异步”修改调用方方法签名,这可能不太适合,比如说,事件处理程序)。

【讨论】:

  • 实际上,ContinueWith() 确实会等待目标任务完成然后继续。请检查-msdn.microsoft.com/en-us/library/dd270696(v=vs.110).aspx
  • 从技术上讲,您不会使用异步修改方法签名。 Async 只是允许使用 await 关键字的标记。对于带有和不带有 async 关键字的方法,编译后的代码是相同的。
  • @SouvikGhosh,绝对是,它将任务链接起来,然后一个接一个地执行它们,等待每个任务完成后再调用下一个。
  • @ckuri,嗯,是的,不是的。仅添加“异步”本身就是不变的。同时,在返回 void 的方法(如大多数事件处理程序)上这样做是危险的,因为在发生异常的情况下您会丢失堆栈跟踪,然后它没有被正确捕获。
猜你喜欢
  • 1970-01-01
  • 2021-08-25
  • 2012-03-20
  • 1970-01-01
  • 1970-01-01
  • 2017-12-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多