一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有1个线程,那么调用任何方法都意味着在执行其他操作之前等待该方法完成,并且该方法中的方法必须在继续执行之前等待结果,以此类推。
一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有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秒,在此期间,没有消息被处理。
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
我不打算在这里链接完整的生成类,但这是相当血腥的东西。
其他回答
我在我的博客文章《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就没有工作要做,因此没有线程。
我不打算和Eric Lippert或者Lasse V. Karlsen等人竞争,我只是想让大家注意这个问题的另一个方面,我想这个问题没有明确提到。
单独使用await并不能让你的应用神奇地做出响应。如果不管你在方法中做什么,你正在等待的UI线程阻塞,它仍然会阻塞你的UI,就像不可等待的版本一样。
你必须编写你的awaitable方法,以便它产生一个新线程或使用一个完成端口之类的东西(它将在当前线程中返回执行,并在完成端口收到信号时调用其他东西来继续)。但这部分在其他答案中有很好的解释。
await和异步使用任务而不是线程。
框架有一个线程池,准备以Task对象的形式执行一些工作; 向池提交任务意味着选择一个已经存在的空闲线程来调用任务 操作方法。 创建一个任务就是创建一个新对象,远远快于创建一个新线程。
如果Task可以附加一个Continuation,那么它就是一个要执行的新Task对象 一旦线程结束。
因为async/await使用任务,它们不会创建一个新的线程。
虽然中断编程技术在每个现代操作系统中都被广泛使用,但我不认为它们是 有关。 你可以让两个CPU绑定任务在一个CPU上并行执行(实际上是交错执行) aysnc /等待。 这不能简单地用操作系统支持排队IORP的事实来解释。
上次我检查了编译器将异步方法转换为DFA,工作分为几个步骤, 每一个都以等待指令结束。 await启动它的Task,并为它附加一个continuation以执行下一个任务 的一步。
作为一个概念示例,下面是一个伪代码示例。 为了清晰起见,事情被简化了,因为我不记得所有的细节。
method:
instr1
instr2
await task1
instr3
instr4
await task2
instr5
return value
它会变成这样
int state = 0;
Task nextStep()
{
switch (state)
{
case 0:
instr1;
instr2;
state = 1;
task1.addContinuation(nextStep());
task1.start();
return task1;
case 1:
instr3;
instr4;
state = 2;
task2.addContinuation(nextStep());
task2.start();
return task2;
case 2:
instr5;
state = 0;
task3 = new Task();
task3.setResult(value);
task3.setCompleted();
return task3;
}
}
method:
nextStep();
1实际上,一个池可以有自己的任务创建策略。
我试着从下往上解释。也许有人会觉得有用。 我在那里,做了,重新发明了它,在DOS和Pascal中制作简单的游戏(好旧时光…)
所以…每个事件驱动的应用程序内部都有一个类似这样的事件循环:
while (getMessage(out message)) // pseudo-code
{
dispatchMessage(message); // pseudo-code
}
框架通常对您隐藏这个细节,但它确实存在。 getMessage函数从事件队列中读取下一个事件或等待事件发生:鼠标移动、按下键、按上键、单击等等。然后dispatchMessage将事件分派给适当的事件处理程序。 然后等待下一个事件,依此类推,直到退出事件出现,退出循环并完成应用程序。
事件处理程序应该快速运行,以便事件循环可以轮询更多事件,并且UI保持响应性。 如果单击按钮触发了这样昂贵的操作,会发生什么?
void expensiveOperation()
{
for (int i = 0; i < 1000; i++)
{
Thread.Sleep(10);
}
}
UI变得无响应,直到10秒操作结束,因为控件停留在函数中。 要解决这个问题,您需要将任务分解为可以快速执行的小部分。 这意味着您不能在一个事件中处理整个事件。 您必须完成一小部分工作,然后将另一个事件发布到事件队列以请求继续。
所以你可以把它改成:
void expensiveOperation()
{
doIteration(0);
}
void doIteration(int i)
{
if (i >= 1000) return;
Thread.Sleep(10); // Do a piece of work.
postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code.
}
在这种情况下,只运行第一次迭代,然后它将消息发送到事件队列以运行下一次迭代并返回。 我们的示例postFunctionCallMessage伪函数在队列中放置了一个“调用此函数”事件,因此事件调度程序将在到达队列时调用它。 这允许在连续运行长时间运行的工作的片段的同时处理所有其他GUI事件。
As long as this long running task is running, its continuation event is always in the event queue. So you basically invented your own task scheduler. Where the continuation events in the queue are "processes" that are running. Actually this what operating systems do, except that the sending of the continuation events and returning to the scheduler loop is done via the CPU's timer interrupt where the OS registered the context switching code, so you don't need to care about it. But here you are writing your own scheduler so you do need to care about it - so far.
So we can run long running tasks in a single thread parallel with the GUI by breaking up them into small chunks and sending continuation events. This is the general idea of the Task class. It represents a piece of work and when you call .ContinueWith on it, you define what function to call as the next piece when the current piece finishes (and its return value is passed to the continuation). But doing all this chaining splitting up work into small pieces manually is a cumbersome work and totally messes up the layout of the logic, because the entire background task code basically a .ContinueWith mess. So this is where the compiler helps you. It does all this chaining and continuation for you under the hood. When you say await you tell the compiler that "stop here, add the rest of the function as a continuation task". The compiler takes care of the rest, so you don't have to.
虽然这个任务块链接不涉及创建线程,并且当任务块很小时,它们可以在主线程的事件循环上调度,但实际上有一个工作线程池来运行任务。这允许更好地利用CPU内核,还允许开发人员运行手动编写的长任务(这会阻塞工作线程而不是主线程)。
实际上,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
我不打算在这里链接完整的生成类,但这是相当血腥的东西。