小编典典

如果 async-await 不创建任何额外的线程,那么它如何使应用程序响应?

all

一次又一次,我看到它说使用async-await不会创建任何额外的线程。这没有任何意义,因为计算机似乎一次可以做不止一件事情的唯一方法是

  • 实际上一次做不止一件事(并行执行,利用多个处理器)
  • 通过调度任务并在它们之间切换来模拟它(做一点点A,一点点B,一点点A等)

因此,如果async-await两者都没有,那么它如何使应用程序响应?如果只有 1
个线程,那么调用任何方法意味着在执行其他任何操作之前等待该方法完成,并且该方法中的方法必须等待结果才能继续,等等。


阅读 71

收藏
2022-04-12

共1个答案

小编典典

实际上,async/await 并没有那么神奇。完整的主题非常广泛,但我认为我们可以管理您的问题的快速而完整的答案。

让我们在 Windows 窗体应用程序中处理一个简单的按钮单击事件:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

我将 明确地 谈论它现在GetSomethingAsync正在返回的任何内容。假设这将在 2 秒后完成。

在传统的非异步世界中,您的按钮单击事件处理程序将如下所示:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

当您单击表单中的按钮时,应用程序将显示冻结大约 2 秒钟,而我们等待此方法完成。发生的事情是“消息泵”,基本上是一个循环,被阻塞了。

这个循环不断地询问窗口“有没有人做过什么,比如移动鼠标,点击了什么?我需要重绘什么吗?如果有,告诉我!”
然后处理那个“东西”。这个循环得到一条消息,用户点击了“button1”(或来自 Windows
的等效消息类型),并最终调用了我们button1_Click上面的方法。在此方法返回之前,此循环现在一直处于等待状态。这需要 2
秒,在此期间,不会处理任何消息。

大多数处理窗口的事情都是使用消息完成的,这意味着如果消息循环停止发送消息,即使只是一秒钟,用户也会很快注意到它。例如,如果您将记事本或任何其他程序移到您自己的程序之上,然后再次离开,则会向您的程序发送一系列绘图消息,指示窗口的哪个区域现在突然再次变得可见。如果处理这些消息的消息循环正在等待某事,被阻塞,则不进行绘制。

那么,如果在第一个示例中,async/await不创建新线程,它是如何做到的呢?

好吧,发生的事情是您的方法分为两部分。这是那些广泛的主题类型之一,所以我不会详细介绍,但足以说明该方法分为以下两部分:

  1. 之前的所有代码await,包括调用GetSomethingAsync
  2. 以下所有代码await

插图:

code... code... code... await X(); ... code... code... code...

重新排列:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

基本上该方法执行如下:

  1. 它执行所有内容await
  2. 它调用该GetSomethingAsync方法,该方法执行其操作,并返回 将在未来 2 秒内完成的内容

到目前为止,我们仍然在对 button1_Click 的原始调用中,发生在主线程上,从消息循环中调用。 如果导致的代码await花费大量时间,UI
仍将冻结。在我们的例子中,没有那么多

  1. 关键字的await作用,连同一些聪明的编译器魔法,基本上类似于“好吧,你知道吗,我将在这里简单地从按钮单击事件处理程序返回。当你(如,我​​们的东西)重新等待)完成,让我知道,因为我还有一些代码要执行”。

实际上,它会让 SynchronizationContext 类知道它已经完成,这取决于当前正在播放的实际同步上下文,它将排队等待执行。Windows
窗体程序中使用的上下文类将使用消息循环正在抽取的队列对其进行排队。

  1. 所以它返回到消息循环,现在可以自由地继续发送消息,例如移动窗口、调整窗口大小或单击其他按钮。

对于用户而言,UI 现在再次响应,处理其他按钮单击、调整大小以及最重要的是 重绘 ,因此它看起来不会冻结。

  1. 2 秒后,我们等待的事情完成了,现在发生的事情是它(嗯,同步上下文)将一条消息放入消息循环正在查看的队列中,说“嘿,我有更多代码你去执行”,这段代码就是await 之后的所有代码。
  2. 当消息循环到达该消息时,它将基本上“重新进入”它停止的方法,await然后继续执行该方法的其余部分。请注意,此代码再次从消息循环中调用,因此如果此代码碰巧在没有async/await正确使用的情况下做了一些冗长的事情,它将再次阻塞消息循环

这里有很多活动部件,所以这里有一些指向更多信息的链接,我想说“如果你需要它”,但这个主题 相当广泛,了解 其中一些活动部件
是相当重要的。你总是会明白 async/await
仍然是一个有漏洞的概念。一些潜在的限制和问题仍然会泄漏到周围的代码中,如果没有,您通常最终不得不调试一个看似没有充分理由随机中断的应用程序。


好的,那么如果GetSomethingAsync启动一个将在 2 秒内完成的线程呢?是的,那么显然有一个新线程在起作用。然而,这个线程并不是 因为
这个方法的异步性,而是因为这个方法的程序员选择了一个线程来实现异步代码。几乎所有异步 I/O 都不
使用线程,它们使用不同的东西。async/await 本身 不会启动新线程,但显然“我们等待的东西”可以使用线程来实现。

.NET 中有很多东西不一定会自行启动线程,但仍然是异步的:

  • Web 请求(以及许多其他需要时间的网络相关事情)
  • 异步文件读写
  • 还有更多,一个好兆头是所讨论的类/接口是否具有名为SomethingSomethingAsyncor的方法BeginSomethingEndSomething并且IAsyncResult涉及到。

通常这些东西不使用引擎盖下的线程。


2022-04-12