一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有1个线程,那么调用任何方法都意味着在执行其他操作之前等待该方法完成,并且该方法中的方法必须在继续执行之前等待结果,以此类推。
一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有1个线程,那么调用任何方法都意味着在执行其他操作之前等待该方法完成,并且该方法中的方法必须在继续执行之前等待结果,以此类推。
当前回答
实际上,异步等待链是由CLR编译器生成的状态机。
async await使用的线程是TPL使用线程池执行任务的线程。
应用程序没有被阻塞的原因是状态机可以决定执行哪个协同例程、重复、检查并再次决定。
进一步阅读:
异步和等待生成什么?
异步等待和生成的状态机
异步c#和f# (III.):它是如何工作的?-托马斯·佩特里塞克
编辑:
好的。看来我的阐述是不正确的。然而,我必须指出,状态机是异步等待的重要资产。即使你采用异步I/O,你仍然需要一个助手来检查操作是否完成,因此我们仍然需要一个状态机,并确定哪些例程可以一起异步执行。
其他回答
我在我的博客文章《There Is No Thread》中详细解释了这一点。
In summary, modern I/O systems make heavy use of DMA (Direct Memory Access). There are special, dedicated processors on network cards, video cards, HDD controllers, serial/parallel ports, etc. These processors have direct access to the memory bus, and handle reading/writing completely independently of the CPU. The CPU just needs to notify the device of the location in memory containing the data, and then can do its own thing until the device raises an interrupt notifying the CPU that the read/write is complete.
一旦操作在运行中,CPU就没有工作要做,因此没有线程。
我真的很高兴有人问这个问题,因为很长一段时间以来,我也认为线程对于并发性是必要的。当我第一次看到事件循环时,我以为它们是谎言。我对自己说:“如果这段代码在一个线程中运行,它就不可能是并发的”。请记住,这是在我已经经历了理解并发性和并行性之间区别的斗争之后。
经过我自己的研究,我终于找到了缺失的部分:select()。具体来说,IO多路复用,由不同的内核以不同的名称实现:select(), poll(), epoll(), kqueue()。这些是系统调用,尽管实现细节不同,但允许您传入一组文件描述符进行监视。然后,您可以进行另一个调用,该调用将阻塞,直到被监视的文件描述符之一发生变化。
因此,可以等待一组IO事件(主事件循环),处理第一个完成的事件,然后将控制权交还给事件循环。清洗并重复。
这是如何工作的呢?简而言之,这是内核和硬件级的魔力。计算机中除了CPU之外还有许多组件,这些组件可以并行工作。内核可以控制这些设备,并直接与它们通信以接收特定的信号。
这些IO多路复用系统调用是单线程事件循环(如node.js或Tornado)的基本构建块。当您等待一个函数时,您正在观察某个事件(该函数的完成),然后将控制权交还给主事件循环。当您正在观看的事件完成时,函数(最终)从它停止的地方开始。允许像这样暂停和恢复计算的函数称为协程。
总结其他答案:
Async/await通常是为IO绑定任务创建的,因为通过使用它们,调用线程不需要被阻塞。这在UI线程的情况下特别有用,因为我们可以确保它们在执行后台操作时保持响应(比如从远程服务器获取数据)。
Async不创建自己的线程。调用方法的线程用于执行异步方法,直到它找到一个可等待对象。然后,同一线程继续执行异步方法调用之外的调用方法的其余部分。注意,在被调用的async方法中,从awaitable返回后,该方法的提醒可以使用线程池中的线程执行——这是唯一出现单独线程的地方。
实际上,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秒,在此期间,没有消息被处理。
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不创建新线程,它是如何做到的呢?
你的方法被分成了两个。这是一个宽泛的主题类型的东西,所以我不会讲得太详细,但足以说明该方法分为以下两部分:
所有导致await的代码,包括对GetSomethingAsync的调用 接下来的所有代码都在等待
说明:
code... code... code... await X(); ... code... code... code...
重新安排:
code... code... code... var x = X(); await X; code... code... code...
^ ^ ^ ^
+---- portion 1 -------------------+ +---- portion 2 ------+
基本上这个方法是这样执行的:
It executes everything up to await It calls the GetSomethingAsync method, which does its thing, and returns something that will complete 2 seconds in the future 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 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". 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. 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. 2 seconds later, the thing we're waiting for completes and what happens now is that it (well, the synchronization context) places a message into the queue that the message loop is looking at, saying "Hey, I got some more code for you to execute", and this code is all the code after the await. When the message loop gets to that message, it will basically "re-enter" that method where it left off, just after await and continue executing the rest of the method. Note that this code is again called from the message loop so if this code happens to do something lengthy without using async/await properly, it will again block the message loop
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.
使用Async和Await进行异步编程(c#和Visual Basic) SynchronizationContext类 Stephen Cleary -没有一篇文章值得一读! 频道9 - Mads Torgersen: c# Async内幕值得一看!
那么,如果GetSomethingAsync启动一个将在2秒内完成的线程呢?是的,很明显有新的线索。然而,这个线程并不是因为这个方法的异步性,而是因为这个方法的程序员选择了一个线程来实现异步代码。几乎所有的异步I/O都不使用线程,它们使用不同的东西。Async /await本身不会启动新的线程,但显然“我们等待的东西”可以使用线程实现。
.NET中有许多东西不一定会自己启动线程,但仍然是异步的:
Web请求(以及许多其他需要时间的与网络相关的事情) 异步文件读写 还有更多,如果有问题的类/接口有名为SomethingSomethingAsync或BeginSomething和EndSomething的方法,并且有一个IAsyncResult涉及,这是一个好迹象。
通常这些东西在引擎盖下不使用线程。
好吧,所以你想要一些“广泛话题的东西”?
好吧,让我们问问Try Roslyn关于我们的按钮点击:
尝试Roslyn
我不打算在这里链接完整的生成类,但这是相当血腥的东西。
这并没有直接回答问题,但我认为它提供了一些有趣的额外信息:
Async和await本身不会创建新线程。但是根据你在哪里使用async-await,在await之前的同步部分可以运行在不同的线程上,而在await之后的同步部分则可以运行在不同的线程上(例如ASP。NET和ASP。NET核心表现不同)。
在基于ui线程的应用程序(WinForms, WPF)中,您将在之前和之后处于同一个线程上。但是当您在线程池线程上使用async-await时,await之前和await之后的线程可能不相同。
关于这个话题的一个很棒的视频