一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有1个线程,那么调用任何方法都意味着在执行其他操作之前等待该方法完成,并且该方法中的方法必须在继续执行之前等待结果,以此类推。
一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有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内核,还允许开发人员运行手动编写的长任务(这会阻塞工作线程而不是主线程)。
其他回答
我试着从下往上解释。也许有人会觉得有用。 我在那里,做了,重新发明了它,在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通常是为IO绑定任务创建的,因为通过使用它们,调用线程不需要被阻塞。这在UI线程的情况下特别有用,因为我们可以确保它们在执行后台操作时保持响应(比如从远程服务器获取数据)。
Async不创建自己的线程。调用方法的线程用于执行异步方法,直到它找到一个可等待对象。然后,同一线程继续执行异步方法调用之外的调用方法的其余部分。注意,在被调用的async方法中,从awaitable返回后,该方法的提醒可以使用线程池中的线程执行——这是唯一出现单独线程的地方。
以下是我对这一切的看法,它在技术上可能不是超级准确,但至少对我有帮助:)。
机器上基本上有两种类型的处理(计算):
发生在CPU上的处理 在其他处理器(GPU,网卡等)上发生的处理,让我们称之为IO。
因此,当我们编写一段源代码时,在编译之后,根据我们使用的对象(这是非常重要的),处理将受到CPU或IO的限制,事实上,它可以绑定到两者的组合。
一些例子:
如果我使用FileStream对象(它是一个流)的Write方法,处理将会说,1%的CPU绑定和99%的IO绑定。 如果我使用NetworkStream对象(它是一个流)的写方法,处理将会说,1%的CPU绑定,和99%的IO绑定。 如果我使用Memorystream对象(这是一个流)的写方法,处理将是100% CPU的限制。
因此,如您所见,从面向对象程序员的角度来看,尽管我总是访问Stream对象,但下面发生的事情可能在很大程度上取决于对象的最终类型。
现在,为了优化事情,如果可能和/或必要的话,有时能够并行运行代码(注意我不使用异步这个词)是有用的。
一些例子:
在桌面应用程序中,我想打印一个文档,但我不想等待它。 我的web服务器同时为许多客户端提供服务,每个客户端并行获取他的页面(不是序列化的)。
在async / await之前,我们基本上有两个解决方案:
线程。它相对容易使用,有Thread和ThreadPool类。线程只受CPU限制。 “旧的”Begin/End/AsyncCallback异步编程模型。这只是一个模型,它并没有告诉你你将会受到CPU或IO的限制。如果你看一下Socket或FileStream类,它是IO绑定的,这很酷,但我们很少使用它。
async / await只是一个基于Task概念的通用编程模型。对于CPU绑定的任务,它比线程或线程池更容易使用,而且比旧的Begin/End模型更容易使用。 然而,它“只是”一个超级复杂的功能齐全的包装。
因此,真正的胜利主要是在IO绑定任务上,不使用CPU的任务,但async/await仍然只是一个编程模型,它不能帮助你确定处理最终如何/在哪里发生。
这意味着它不是因为一个类有一个方法“DoSomethingAsync”返回一个任务对象,你可以假设它将是CPU绑定(这意味着它可能非常无用,特别是如果它没有取消令牌参数),或IO绑定(这意味着它可能是必须的),或两者的组合(因为模型是相当病毒式传播的,绑定和潜在的好处可以,最终,超级混合,不那么明显)。
所以,回到我的例子,在MemoryStream上使用async/await进行写操作将保持CPU限制(我可能不会从中受益),尽管我肯定会从文件和网络流中受益。
实际上,异步等待链是由CLR编译器生成的状态机。
async await使用的线程是TPL使用线程池执行任务的线程。
应用程序没有被阻塞的原因是状态机可以决定执行哪个协同例程、重复、检查并再次决定。
进一步阅读:
异步和等待生成什么?
异步等待和生成的状态机
异步c#和f# (III.):它是如何工作的?-托马斯·佩特里塞克
编辑:
好的。看来我的阐述是不正确的。然而,我必须指出,状态机是异步等待的重要资产。即使你采用异步I/O,你仍然需要一个助手来检查操作是否完成,因此我们仍然需要一个状态机,并确定哪些例程可以一起异步执行。
我不打算和Eric Lippert或者Lasse V. Karlsen等人竞争,我只是想让大家注意这个问题的另一个方面,我想这个问题没有明确提到。
单独使用await并不能让你的应用神奇地做出响应。如果不管你在方法中做什么,你正在等待的UI线程阻塞,它仍然会阻塞你的UI,就像不可等待的版本一样。
你必须编写你的awaitable方法,以便它产生一个新线程或使用一个完成端口之类的东西(它将在当前线程中返回执行,并在完成端口收到信号时调用其他东西来继续)。但这部分在其他答案中有很好的解释。