【问题标题】:Is a continuation created even if there is nothing after an await?即使在等待之后没有任何内容,是否也会创建延续?
【发布时间】:2021-10-06 02:18:13
【问题描述】:

我正在阅读这个article 并找到了这个例子:

public static class DeadlockDemo
{
  private static async Task DelayAsync()
  {
    await Task.Delay(1000);
  }
  // This method causes a deadlock when called in a GUI or ASP.NET context.
  public static void Test()
  {
    // Start the delay.
    var delayTask = DelayAsync();
    // Wait for the delay to complete.
    delayTask.Wait();
  }
}

这种死锁的根本原因是等待处理上下文的方式。默认情况下,当等待一个不完整的任务时,当前的“上下文”被捕获并用于在任务完成时恢复该方法。这个“上下文”是当前的 SynchronizationContext,除非它是 null,在这种情况下它是当前的 TaskScheduler。 GUI 和 ASP.NET 应用程序有一个 SynchronizationContext,它一次只允许运行一段代码。当 await 完成时,它会尝试在捕获的上下文中执行 async 方法的其余部分。但是该上下文中已经有一个线程,它(同步地)等待异步方法完成。他们都在等待对方,造成了僵局。

我知道如果在await Task.Delay(1000); 之后有代码,它将成为延续的一部分,并且延续将在与Test() 相同的上下文中运行,这就是死锁的发生方式。

但是没有延续,那么死锁到底是怎么发生的呢?

或者,是否创建了一个空的延续?这样做有什么意义?

这是让我困惑的部分:

当等待完成时,它会尝试执行剩余的 捕获的上下文中的异步方法。

什么是“异步方法的剩余部分”?

【问题讨论】:

  • 挑剔:如果等待 Task.Delay(1000) 之后有代码;该代码将在 DelayAsync() 的上下文中运行,这恰好与 Task() 相同,因为该方法是同步调用的。
  • 大概,“异步方法的剩余部分”可能是一个什么都不做的空方法,但仍然需要调用。但我不知道是否记录了这种特定行为。
  • @DavidKlempfner await 说你想回到原来的同步上下文。它的行为不受其后是否有任何代码的影响。如果您想避免这种情况,请使用ConfigureAwait(false) 或直接返回任务
  • @DavidKlempfner 好问题!
  • 如果你等待的东西抛出了异常怎么办? DelayAsync 有两个步骤 - 第一步它执行Task.Delay,第二步在Task.Delay 完成时执行,例如检查是否有异常。这第二步也是“继续”。

标签: c# asynchronous async-await task synchronizationcontext


【解决方案1】:

如果我们是 async Taskasync Task<T> 方法,那么在 await 之后总是有工作要做 - 我们需要确保 awaited 任务产生的任何异常都正确传播到我们自己的Task,或者我们通过适当的正常返回值。

如果我们是任何类型的 async 方法,使用诸如 using 之类的结构,在我们方法的结尾处​​插入编译器生成的代码,否则我们将在末尾插入代码方法,即使它没有出现在源代码中。

如果我们是任意使用awaits 的普通async 方法,那么我们已经构建并运行了一个状态机来执行我们的方法,那么优化“最后一次等待后无代码”是没有意义的"可能性。

的情况下,我们是一个 async void1 方法,它在末尾包含一个 await,除了一些关于哪里的细节之外可能会报告来自任务的未处理异常,我们已经有机会通过根本不使用 async 方法而忽略等待的方法来避免过多的代码生成。

所以没有理由尝试优化这种情况。


1反正那时我们已经处于犯罪状态。

【讨论】:

  • "...优化“最后一次等待后没有代码”的可能性是没有意义的。”
  • @TheodorZoulias - 人们经常表现得好像应该抓住每一个优化机会,而忽略了这样一个事实,即(如这里)它只适用于狭窄的环境并且有机会实施 this 优化会浪费在其他地方有用的资源。正如我的回答已经表明的那样,您,代码作者,已经可以通过不写 await 来避免继续。
  • Damien 我不认为 “我们没有优化 '最后一次等待后无代码',因为您可以选择不使用 async/await”一个合乎逻辑的论点。 OP 给出了一个具有单个 await 的异步方法的示例,但他们的问题可以概括为具有多个 awaits 的异步方法。真正的问题是“为什么异步状态机在最后一个await 之后生成一个延续,即使在那之后没有代码?”。
  • @TheodorZoulias 在生成的异步状态机中,一个步骤开始执行任务,并且仅在下一步(“空继续”)中探索该任务的结果。所以它实际上不能为空,状态机不能只是触发并忘记该任务,即使未使用结果也需要探索,如果有异常应该传播。
  • @Evk 所以你说为了让异步状态机跳过最后一个空继续,它必须在当前线程上调用TaskAwaiter.GetResult(),如果出现异常,它应该在捕获的上下文中再次调用它?这是有道理的。如果await 经常失败,这将使工作加倍。
猜你喜欢
  • 1970-01-01
  • 2013-12-18
  • 2020-01-21
  • 1970-01-01
  • 2019-09-12
  • 1970-01-01
  • 2020-11-17
  • 1970-01-01
  • 2019-04-16
相关资源
最近更新 更多