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

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

问题描述:

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

Time and time again, I see it said that using async-await doesn't create any additional threads. That doesn't make sense because the only ways that a computer can appear to be doing more than 1 thing at a time is

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

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

So if async-await does neither of those, then how can it make an application responsive? If there is only 1 thread, then calling any method means waiting for the method to complete before doing anything else, and the methods inside that method have to wait for the result before proceeding, and so forth.

实际上,async/await 并没有那么神奇.整个主题相当广泛,但对于您的问题的快速而完整的回答,我认为我们可以做到.

Actually, async/await is not that magical. The full topic is quite broad but for a quick yet complete enough answer to your question I think we can manage.

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

Let's tackle a simple button click event in a Windows Forms application:

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

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

I'm going to explicitly not talk about whatever it is GetSomethingAsync is returning for now. Let's just say this is something that will complete after, say, 2 seconds.

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

In a traditional, non-asynchronous, world, your button click event handler would look something like this:

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

当您单击表单中的按钮时,应用程序将出现大约 2 秒钟的冻结,同时我们等待此方法完成.发生的情况是消息泵",基本上是一个循环,被阻塞了.

When you click the button in the form, the application will appear to freeze for around 2 seconds, while we wait for this method to complete. What happens is that the "message pump", basically a loop, is blocked.

这个循环不断地询问windows有没有人做过什么,比如移动鼠标,点击了什么?我需要重新绘制什么吗?如果是这样,告诉我!"然后处理那个东西".这个循环收到一条消息,表明用户单击了button1"(或来自 Windows 的等效消息类型),并最终调用了上面的 button1_Click 方法.在此方法返回之前,此循环现在一直在等待.这需要 2 秒,在此期间,没有处理任何消息.

This loop continuously asks windows "Has anyone done something, like moved the mouse, clicked on something? Do I need to repaint something? If so, tell me!" and then processes that "something". This loop got a message that the user clicked on "button1" (or the equivalent type of message from Windows), and ended up calling our button1_Click method above. Until this method returns, this loop is now stuck waiting. This takes 2 seconds and during this, no messages are being processed.

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

Most things that deal with windows are done using messages, which means that if the message loop stops pumping messages, even for just a second, it is quickly noticeable by the user. For instance, if you move notepad or any other program on top of your own program, and then away again, a flurry of paint messages are sent to your program indicating which region of the window that now suddenly became visible again. If the message loop that processes these messages is waiting for something, blocked, then no painting is done.

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

So, if in the first example, async/await doesn't create new threads, how does it do it?

好吧,结果是你的方法被分成了两部分.这是一种广泛的主题类型,所以我不会详细介绍,但可以说该方法分为这两件事:

Well, what happens is that your method is split into two. This is one of those broad topic type of things so I won't go into too much detail but suffice to say the method is split into these two things:

  1. 导致 await 的所有代码,包括对 GetSomethingAsync 的调用
  2. await之后的所有代码
  1. All the code leading up to await, including the call to GetSomethingAsync
  2. All the code following await

插图:

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

重新排列:

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

基本上这个方法是这样执行的:

Basically the method executes like this:

  1. 它执行直到 await
  2. 它调用 GetSomethingAsync 方法,该方法完成它的工作,并返回将在未来 2 秒内完成的东西

  1. It executes everything up to await
  2. It calls the GetSomethingAsync method, which does its thing, and returns something that will complete 2 seconds in the future

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

So far we're still inside the original call to button1_Click, happening on the main thread, called from the message loop. If the code leading up to await takes a lot of time, the UI will still freeze. In our example, not so much

await 关键字以及一些巧妙的编译器魔法所做的就是它基本上类似于好吧,你知道吗,我将简单地从按钮单击返回事件处理程序在这里.当您(例如,我们正在等待的事情)完成时,请告诉我,因为我还有一些代码要执行".

What the await keyword, together with some clever compiler magic, does is that it basically something like "Ok, you know what, I'm going to simply return from the button click event handler here. When you (as in, the thing we're waiting for) get around to completing, let me know because I still have some code left to execute".

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

Actually it will let the SynchronizationContext class know that it is done, which, depending on the actual synchronization context that is in play right now, will queue up for execution. The context class used in a Windows Forms program will queue it using the queue that the message loop is pumping.

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

So it returns back to the message loop, which is now free to continue pumping messages, like moving the window, resizing it, or clicking other buttons.

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

For the user, the UI is now responsive again, processing other button clicks, resizing and most importantly, redrawing, so it doesn't appear to freeze.

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

There are many moving parts under the hood here so here are some links to more information, I was going to say "should you need it", but this topic is quite broad and it is fairly important to know some of those moving parts. Invariably you're going to understand that async/await is still a leaky concept. Some of the underlying limitations and problems still leak up into the surrounding code, and if they don't, you usually end up having to debug an application that breaks randomly for seemingly no good reason.

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

OK, so what if GetSomethingAsync spins up a thread that will complete in 2 seconds? Yes, then obviously there is a new thread in play. This thread, however, is not because of the async-ness of this method, it is because the programmer of this method chose a thread to implement asynchronous code. Almost all asynchronous I/O don't use a thread, they use different things. async/await by themselves do not spin up new threads but obviously the "things we wait for" may be implemented using threads.

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

There are many things in .NET that do not necessarily spin up a thread on their own but are still asynchronous:

  • 网络请求(以及许多其他需要时间的网络相关事情)
  • 异步文件读写
  • 还有更多,一个好兆头是如果有问题的类/接口有名为 SomethingSomethingAsyncBeginSomethingEndSomething 的方法,并且有一个IAsyncResult 涉及.
  • Web requests (and many other network related things that takes time)
  • Asynchronous file reading and writing
  • and many more, a good sign is if the class/interface in question has methods named SomethingSomethingAsync or BeginSomething and EndSomething and there's an IAsyncResult involved.

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

Usually these things do not use a thread under the hood.

好的,所以你想要一些广泛主题的东西"?

OK, so you want some of that "broad topic stuff"?

好吧,让我们询问 尝试 Roslyn 关于我们的按钮点击:

Well, let's ask Try Roslyn about our button click:

尝试罗斯林

我不会在这里链接完整生成的类,但它非常血腥.

I'm not going to link in the full generated class here but it's pretty gory stuff.