史蒂芬的答案已经很好了,所以我不会重复他说的话;我在Stack Overflow(和其他地方)已经重复了很多次相同的论点。
相反,让我专注于异步代码的一个重要抽象概念:它不是绝对的限定词。没有必要说一段代码是异步的 - 它总是异步的,与其他东西有关。这非常重要。
await
的目的是在异步操作和一些连接同步代码之上构建同步工作流程。您的代码对于代码本身来说
1看起来完全同步。
var a = await A();
await B(a);
事件的排序是由await调用指定的。B使用A的返回值,这意味着A必须在B之前运行。包含此代码的方法具有同步工作流,并且A和B两种方法在彼此之间是同步的。
这非常有用,因为同步工作流通常更容易思考,并且更重要的是,许多工作流程仅仅是同步的。如果B需要运行A的结果,则必须在A之后运行。如果您需要发出HTTP请求以获取另一个HTTP请求的URL,则必须等待第一个请求完成;这与线程/任务调度无关。也许我们可以将其称为“固有同步性”,除了“偶然同步性”之外,在其中强制对不需要有序的东西进行排序。
你说:
“在我看来,由于我主要从事UI开发,异步代码是不在UI线程上而在其他线程上运行的代码。”
您正在描述相对于UI异步运行的代码。这确实是一种非常有用的情况(人们不喜欢停止响应的UI)。但是,它只是更通用原则的特定案例 - 允许事件相互无序发生。再次说明,这并不是绝对的 - 您希望某些事件发生无序(例如,当用户拖动窗口或进度条更改时,窗口仍然应该重新绘制),而其他事件必须不按顺序发生(在加载操作完成之前不能单击“处理”按钮)。在这种用法中,await与原则上使用Application.DoEvents并没有太大区别 - 它引入了许多相同的问题和好处。
这也是原始引用变得有趣的部分。 UI需要线程进行更新。该线程调用事件处理程序,该处理程序可能正在使用await。这是否意味着使用await的行将允许UI根据用户输入进行更新?不。
首先,您需要了解await使用其参数,就像它是一个方法调用一样。在我的示例中,在由await生成的代码可以执行任何操作之前,必须已经调用了A,包括“释放控件返回到UI循环”。 A的返回值是Task而不仅仅是T,表示“可能的未来值”- await生成的代码检查是否已经存在该值(在这种情况下,它只是在同一个线程上继续进行)或者没有(这意味着我们要释放线程回到UI循环)。但是,在任何情况下,Task值本身都必须从A返回。
考虑这个实现:
public async Task<int> A()
{
Thread.Sleep(1000);
return 42;
}
调用者需要
A
返回一个值(一个 int 类型的任务);由于方法中没有
await
,那么就意味着它会返回
return 42;
。但是在睡眠完成之前,这是不可能发生的,因为这两个操作对于线程来说是同步的。无论调用者线程是否使用
await
,它都会被阻塞一秒钟——阻塞发生在
A()
本身而不是
await theTaskResultOfA
。
相比之下,请考虑以下内容:
public async Task<int> A()
{
await Task.Delay(1000);
return 42;
}
一旦执行到
await
,它会发现被等待的任务尚未完成,于是将控制权返回给其调用者;而调用者中的
await
也会因此将控制权返回给
它自己的调用者。我们已经成功地将一些代码与UI异步化了。UI线程和A之间的同步性是偶然的,我们已经将其删除。
这里的重要部分是:没有办法从外部区分两个实现,除非检查代码。只有返回类型是方法签名的一部分 - 它并不表示该方法将异步执行,只表示它
可能。这可能是出于许多良好的原因,所以没有必要反对它 - 例如,当结果已经可用时,打断执行线程是没有意义的。
var responseTask = GetAsync("http://www.google.com");
ComputeAllTheFuzz();
response = await responseTask;
我们需要做一些工作。有些事件可以与其他事件异步运行(在这种情况下,
ComputeAllTheFuzz
独立于 HTTP 请求并且是异步的)。但在某些时候,我们需要回到同步工作流程(例如,需要
ComputeAllTheFuzz
的结果和 HTTP 请求)。这就是
await
点,它再次同步执行(如果您有多个异步工作流程,可以使用类似
Task.WhenAll
的东西)。然而,如果 HTTP 请求在计算之前已经完成,则在
await
点释放控制没有意义 - 我们只需在同一个线程上继续执行。没有浪费 CPU - 没有阻塞线程; 它执行有用的 CPU 工作。但我们没有给 UI 更新的机会。
当然,这就是为什么在更一般的异步方法中通常避免使用此模式的原因。它对于某些异步代码的使用很有用(避免浪费线程和 CPU 时间),但对于另外一些(保持 UI 响应)则无效。如果您期望这样的方法使 UI 保持响应,则不会得到满意的结果。但是,如果将其用作 Web 服务的一部分,则非常适合 - 其重点在于避免浪费线程,而不是保持 UI 响应(通过异步调用服务端点已经提供了该功能 - 在服务端重复此操作没有任何好处)。
简而言之,
await
允许您编写相对于其调用者异步的代码。它不会调用异步魔力,它并非针对所有内容都是异步的,它不会阻止您使用 CPU 或阻塞线程。它只是为您提供了将异步操作转换成同步工作流程的工具,并将整个工作流程的一部分与其调用者异步呈现。
让我们考虑一个 UI 事件处理程序。如果个别异步操作恰好不需要线程来执行(例如异步 I/O),则异步方法的某些部分可能允许其他代码在原始线程上执行(在这些部分中,UI 保持响应)。当操作再次需要 CPU/线程时,它可能需要
原始 线程继续工作。如果需要,则 UI 将再次被阻塞,直到 CPU 工作完成;如果不需要(
awaiter 使用
ConfigureAwait(false)
指定此条件),则 UI 代码将并行运行。当然,假设有足够的资源来同时处理两者。如果您需要 UI 始终保持响应,则不能将 UI 线程用于任何需要注意的执行时间 - 即使这意味着您必须在
Task.Run
中包装一个不可靠的“通常是异步的,但有时会阻塞几秒钟”的异步方法。两种方法都有成本和收益 - 这是一个权衡,就像所有的工程一样 :)
- 当然,在抽象的范畴内看是完美的 - 但每个抽象都会有漏洞,而在await和其他异步执行方法中存在很多泄漏。
- 一个足够聪明的优化器可能会允许B的某个部分运行,直到实际需要A的返回值为止;这就是你的CPU对于正常的“同步”代码所做的事情(乱序执行)。尽管如此,这样的优化必须保持同步的外观 - 如果CPU误判了操作的顺序,它必须丢弃结果并呈现正确的顺序。