为什么一直向下使用 Async/await

Why use Async/await all the way down

我想澄清一下使用 Await 和 Async 的额外好处是什么。

如果我的应用程序正在调用 await Func1()(因此不会阻塞此处的 UI)。 Func1 正在调用 await Func2(),但是 Func2() 的结果对于 Func1 完成它的工作很重要,那么为什么我需要让 Func2() 可等待. Func1() 执行将花费同样长的时间,因为它正在等待 Func2 完成。 await 在这里所做的就是增加 StateMachine 开销。

我是不是漏掉了什么?

主要好处是等待异步方法 returns 池中的工作线程可用于其他调用(例如,对 .NET MVC 网络应用程序的网络请求)。异步工作在 IO 完成线程上完成。当等待的方法完成时,另一个工作线程将收集结果并恢复执行。这可以防止工作线程池耗尽,并允许您的应用程序处理更多负载(CPU、内存和网络吞吐量,具体取决于)。

至于 "await all the way down",这对我来说似乎是个问题。通常 await 与您的应用程序必须等待的外部资源(数据库调用、HTTP 请求等)相关联。如果您 await 没有外部 IO 依赖项的代码,则会产生不需要的开销。在 async 方法链中可以有多个 awaits,但是等待一些本身调用 await 但没有其他外部 IO 依赖的代码并不好,只会添加 callback/compiler 开销。

在基于 UI 的应用程序中,async await 提供了一种非常简洁的方法来处理导致 UI 更新的异步调用。如果来自 UI 的 'top level' 处理程序正在等待结果,那么除非这样做有意义,否则在整个链中做同样的事情可能没有真正的好处。 async await 的一个设计目标是让异步编程看起来更加同步和连续,并且没有回调分散等 - 使异步编码更易于评估。这不是你需要随意到处使用的东西。

更好的口号async一路向上。因为您从一个异步操作开始并使其调用者异步,然后是下一个调用者等

当您有固有的异步操作(通常是 I/O 但不一定)并且您不想浪费线程等待操作完成时,您应该使用 async-await。选择 async 操作而不是同步操作不会加快操作速度。这将花费相同的时间(甚至更多)。它只是使该线程能够继续执行其他一些 CPU 绑定工作,而不是浪费资源。

但是为了能够 await 该操作,该方法需要是一个 async 并且调用者需要 await 它等等。

所以 async 一路向上使您能够实际进行异步调用并释放任何线程。如果它不是一直 async 那么一些线程被阻塞了。

所以,这个:

async Task FooAsync()
{
    await Func1();
    // do other stuff
}

async Task Func1()
{
    await Func2();
    // do other stuff
}

async Task Func2()
{
    await tcpClient.SendAsync();
    // do other stuff
}

比这个更好:

void Foo()
{
    Func1();
    // do other stuff
}

void Func1()
{
    Func2().Wait();  // Synchronously blocking a thread.
    // do other stuff
}

async Task Func2()
{
    await tcpClient.SendAsync();
    // do other stuff
}

"This async method lacks 'await' operators and will run synchronously" 如果您不等待异步方法,则会发生这种情况。要利用异步方法的好处,您必须等待它,将调用者转换为可以等待的异步方法等。

从流程图中您可以看到异步方法 returns 一个任务(对未来完成工作的承诺)并将控制权交给它的调用者。然后调用者可以继续工作而不依赖于这个结果。显然,这需要在调用堆栈中冒泡,以找到所有这些可以在没有结果的情况下完成的有收益的工作(在 UI 应用程序中,这项工作将包括解锁 UI,这就是为什么它是异步的下例中的事件处理程序)。


来自我最初的误读。所以你已经找到了一些你需要调用的异步代码,是否值得将异步等待模式扩展到你的代码中:

我听说 async-await 的主要问题是它的语法对于它的实际作用来说太简单了。 Program flow 变得复杂。我真的很喜欢 async-await,但不幸的是,在我见过的大多数异步代码中,它是不值得的,只是不必要地破坏了我的调用堆栈。

要记住的一件好事是 50ms rule

"This is the rule that Microsoft followed with the WinRT APIs; anything taking less than 50ms is considered “fast” and close enough to “immediate” that they do not require an asynchronous API."

这是在鼓励异步等待的情况下使用的。但是,我认为它同样应该用于告诉开发人员使用 async-await 来实现本质上即时的功能以将其删除。


通常,async/await 背后的推理是相反的:

无论出于何种原因,您决定使用 await 编写 Func2 会更容易。因此,您通过添加所需的 await 来简化方法,这意味着您还必须更改方法签名(它现在将包括 async 关键字和 return 类型的 TaskTask<T>).

因为return的类型变了,不能再像以前那样调用Func2(var result = Func2();),所以现在需要改变调用方式, Func1。适应 Func1 的最简单方法通常是也使其成为 async,并且 await Func2().

然后,出于同样的原因(更改签名),您需要将所有调用更改为 Func1,依此类推,直到到达某种入口点(UI事件处理程序,或您的 Main 方法,或其他方法)。

因此,您不会开始创建 "outermost" 方法 async 并继续执行 "inner"(调用的)方法;你通常在相反的方向上做事情 async(从 "innermost" 方法回到调用方法)。 称这个想法为 "async all the way up".