如果asynchronous等待不会创build任何其他线程,那么它如何使应用程序响应?
一次又一次,我看到它说,使用async
– await
不会创build任何额外的线程。 这是没有道理的,因为电脑似乎一次只能做多件事情的唯一方法是
- 实际上一次做多个事情(并行执行,使用多个处理器)
- 通过调度任务并在它们之间切换来进行模拟(做一些A,一点B,一点A等等)
所以如果async
– await
都没有,那么它如何使应用程序响应? 如果只有一个线程,那么调用任何方法意味着在执行其他任何操作之前等待该方法完成,并且该方法中的方法在继续之前必须等待结果,等等。
实际上,asynchronous/等待不是那么神奇。 完整的主题是相当广泛的,但对于你的问题,我想我们可以pipe理一个快速而完整的答案。
让我们来解决一个Windows窗体应用程序中的一个简单的button点击事件:
public async void button1_Click(object sender, EventArgs e) { Console.WriteLine("before awaiting"); await GetSomethingAsync(); Console.WriteLine("after awaiting"); }
我要明确地 不谈论GetSomethingAsync
现在正在返回什么。 我们只是说这是在2秒后完成的事情。
在传统的,非asynchronous的世界中,你的button点击事件处理程序看起来像这样:
public void button1_Click(object sender, EventArgs e) { Console.WriteLine("before waiting"); DoSomethingThatTakes2Seconds(); Console.WriteLine("after waiting"); }
当您单击表单中的button时,应用程序将显示冻结大约2秒钟,而我们等待此方法完成。 发生什么事情是“消息泵”,基本上是一个循环,被阻止。
这个循环不断地问窗口:“有没有人做过一些事情,比如移动鼠标,点击了什么?我需要重新绘制一些东西吗?如果是,请告诉我! 然后处理这个“东西”。 这个循环得到一个消息,说用户点击了“button1”(或者Windows的同类消息),最后调用了上面的button1_Click
方法。 在这个方法返回之前,这个循环现在停滞不前。 这需要2秒,在此期间,没有消息正在处理。
大多数处理窗口的事情都是使用消息完成的,这意味着如果消息循环停止泵送消息,即使只是一秒钟,用户也很快就会注意到。 例如,如果您将记事本或任何其他程序移动到自己的程序之上,然后再移开,则会向您的程序发送一连串的绘图消息,指示窗口的哪个区域现在突然再次变为可见。 如果处理这些消息的消息循环正在等待某个东西被阻塞,那么就不会完成绘画。
那么,如果在第一个例子中, async/await
不会创build新的线程,那么它是如何实现的呢?
那么,发生什么事是你的方法分成两个。 这是一个广泛的主题types的东西之一,所以我不会过多的细节,但足以说这个方法分为两个东西:
- 所有引导
await
的代码,包括对GetSomethingAsync
的调用 - 所有的代码在
await
插图:
code... code... code... await X(); ... code... code... code...
重新排列:
code... code... code... var x = X(); await X; code... code... code... ^ ^ ^ ^ +---- portion 1 -------------------+ +---- portion 2 ------+
基本上这个方法是这样执行的:
- 它执行一切
await
-
它调用
GetSomethingAsync
方法,它会完成它的工作,并返回将来会完成2秒的事情到目前为止,我们还在原来的button1_Click调用中,发生在主线程上,从消息循环调用。 如果导致
await
的代码需要很长时间,UI仍然会冻结。 在我们的例子中,不是那么多 -
什么
await
关键字,以及一些聪明的编译器魔术,做的是,它基本上像“好吧,你知道吗,我要从这里button单击事件处理程序简单地返回。当你(如在,我们等待)完成,让我知道,因为我还有一些代码要执行“。实际上,它将让SynchronizationContext类知道它已经完成,根据实际正在使用的同步上下文,它将排队等待执行。 Windows窗体程序中使用的上下文类将使用消息循环正在抽取的队列对其进行排队。
-
所以它返回到消息循环,现在可以自由地继续泵送消息,如移动窗口,调整其大小或单击其他button。
对于用户来说,用户界面现在再次响应,处理其他button点击,resize,最重要的是, 重绘 ,所以它不会冻结。
- 2秒钟后,我们正在等待的事情完成了,现在发生的事情是它(同步上下文)将消息放入消息循环正在查看的队列中,说:“嘿,我有更多的代码你执行“,而这个代码就是等待之后的所有代码。
- 当消息循环到达那个消息时,基本上会在它停止的地方“重新进入”方法,
await
并继续执行方法的其余部分。 请注意,这个代码是从消息循环中再次调用的,所以如果这个代码碰巧做了一些冗长的事情,而没有正确地使用async/await
,它将再次阻塞消息循环
这里有很多可移动的部分,所以这里有一些更多信息的链接,我会说“你应该需要它”,但是这个主题是相当广泛的,知道一些移动部分是相当重要的。 总是你会明白,asynchronous/等待仍然是一个漏洞的概念。 一些基本的限制和问题仍然会泄漏到周围的代码中,如果不这样做,通常最终不得不debugging一个应用程序,这个应用程序似乎没有任何理由随机中断。
- 使用Async和Await进行asynchronous编程(C#和Visual Basic)
- SynchronizationContext类
- 斯蒂芬Cleary – 没有线 值得一读!
- 第9频道 – Mads Torgersen:C#asynchronous 非常值得一看!
好的,那么如果GetSomethingAsync
在2秒钟内完成一个线程的话呢? 是的,那么显然有一个新的线索在发挥。 然而,这个线程并不是因为这个方法的asynchronous性,而是因为这个方法的程序员select了一个线程来实现asynchronous代码。 几乎所有的asynchronousI / O 都不使用线程,它们使用不同的东西。 async/await
自己不会启动新线程,但显然“我们等待的东西”可以使用线程来实现。
在.NET中有很多东西不一定会自己启动一个线程,但仍然是asynchronous的:
- Web请求(以及许多其他与networking有关的事情需要花费时间)
- asynchronous文件读写
- 还有更多,如果有问题的类/接口有一个名为
SomethingSomethingAsync
或BeginSomething
和EndSomething
方法,并且有一个IAsyncResult
涉及,
通常这些东西不使用引擎盖下的线程。
好的,所以你想要一些“广泛的主题”?
那么,让我们问Try Roslyn关于我们的button点击:
试试Roslyn
我不打算链接完整的生成的类,但它是非常可爱的东西。
计算机看起来每次只能做一件事的唯一方法是(1)实际上一次做多于一件事,(2)通过调度任务并在它们之间切换来模拟它。 所以如果asynchronous等待这两者都没有
这不是等待这两个 。 请记住, await
的目的不是让同步代码神奇asynchronous 。 在调用asynchronous代码时 ,可以使用我们用于编写同步代码的相同技术 。 等待是使使用高延迟操作的代码看起来像使用低延迟操作的代码 。 那些高延迟的操作可能在线程上,它们可能在特殊用途的硬件上,它们可能将它们的工作分解成小块,并且将其放入消息队列中以供稍后的UI线程处理。 他们正在做一些不同步的事情 ,但是他们正在做这些事情。 等待只是让你利用这个asynchronous。
另外,我想你错过了第三个select。 我们的老人 – 今天带着说唱音乐的孩子们应该离开我的草坪等等 – 记得20世纪90年代初Windows的世界。 没有多CPU机器和线程调度器。 你想同时运行两个Windows应用程序,你必须屈服 。 多任务是合作的 。 操作系统告诉一个进程运行,如果运行不正常,则会导致所有其他进程不能运行。 它一直运行,直到它产生,并且不知何故,它必须知道如何在操作系统下次操作控制权回到它时停止的地方 。 单线程asynchronous代码很像这样,用“await”代替“yield”。 等待的意思是“我要记住我在这里离开的地方,让其他人跑一会儿,当我等待的任务完成后再打给我,我会从停止的地方接走。” 我认为你可以看到它是如何使应用程序更加快速响应的,就像在Windows 3中一样。
调用任何方法意味着等待方法完成
有你失踪的关键。 一个方法可以在其工作完成之前返回 。 这就是那里asynchronous的本质。 一个方法返回,它返回一个任务,意思是“这项工作正在进行中;告诉我什么时候完成”。 该方法的工作没有完成, 即使它已经返回 。
在等待操作员之前,你必须编写看起来像通过瑞士奶酪穿过的意大利面条的代码,以处理完成后我们有工作要做的事情,但是返回和完成不同步 。 等待使您可以编写看起来像返回的代码,并且完成同步,而不会实际同步。
我在我的博客文章“没有线程”中全面解释。
总之,现代I / O系统大量使用DMA(直接存储器访问)。 网卡,显卡,硬盘控制器,串口/并口等都有特殊的专用处理器,这些处理器可以直接访问内存总线,并且完全独立于CPU处理读写操作。 CPU只需要通知设备在内存中包含数据的位置,然后就可以做自己的事情,直到设备引发中断,通知CPU读/写完成。
一旦操作在飞行中,CPU就没有工作了,因此没有线程。
我真的很高兴有人问这个问题,因为最长的时间我也认为线程是并发的必要条件。 当我第一次看到事件循环时 ,我以为他们是个谎言。 我以为自己“这个代码如果在一个线程中运行,就不可能是并发的”。 请记住,这是我已经经历了理解并发性和并行性之间的区别的斗争之后。
经过我自己的研究,我终于find了缺失的一块: select()
。 具体而言,IO多路复用,由不同的内核以不同的名称执行: select()
, poll()
, epoll()
, kqueue()
。 这些是系统调用 ,虽然实现细节不同,但允许您传入一组文件描述符来观察。 然后,您可以进行另一个阻塞,直到观察到的文件描述符中的一个发生更改。
因此,可以等待一组IO事件(主事件循环),处理完成的第一个事件,然后将控制权交还给事件循环。 冲洗并重复。
这个怎么用? 那么简单的答案就是它是内核和硬件级别的魔法。 除CPU以外,计算机中还有许多组件,这些组件可以并行工作。 内核可以控制这些设备并直接与它们通信以接收某些信号。
这些IO复用系统调用是单线程事件循环(如node.js或Tornado)的基本构build块。 当你await
一个函数的时候,你正在监视一个特定的事件(函数的完成),然后让控制回到主事件循环。 当你正在观看的事件完成时,function(最终)从停止的地方开始。 允许你暂停和恢复计算的函数被称为协程 。
await
和async
使用任务不是线程。
该框架有一个线程池准备好以Task对象的forms执行一些工作; 向池提交任务意味着select一个空闲的, 已经存在的 1线程来调用任务操作方法。
创build一个任务是创build一个新的对象的问题,远远快于创build一个新的线程。
给定一个Task可以附加一个Continuation ,它是一个新的Task对象,一旦线程结束就会被执行。
由于async/await
使用任务,他们不创build一个新的线程。
虽然中断编程技术在每个现代操作系统中都被广泛使用,但我认为它们在这里并不相关。
您可以使用aysnc/await
将两个CPU绑定任务并行执行(实际交错)在单个CPU中。
这不能简单地解释为操作系统支持排队IORP 。
上次我将编译器转换的async
方法检查到DFA中 ,工作被分成几个步骤,每个步骤都以await
指令结束。
await
开始它的任务并附加它继续执行下一步。
作为一个概念的例子,这里是一个伪代码的例子。
为了清楚起见,事情正在被简化,因为我完全不记得所有的细节。
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其实一个游泳池可以有其创build任务的政策。
我不打算与Eric Lippert或Lasse V. Karlsen等人竞争,我只想提请注意这个问题的另一个方面,我想这个问题没有明确提及。
使用它自己的await
不会让你的应用程序神奇的响应。 如果您在UI线程块中等待的方法中执行了任何操作, 它仍然会以与非等待版本相同的方式阻止您的UI 。
你必须专门编写你的awaitable方法,以便产生一个新的线程,或者使用一个完成端口(在当前线程中返回执行,并在完成端口被发信号时调用其他的继续)。 但是这个部分在其他答案中已经很好的解释了
这是我如何看待这一切,它可能不是超技术上的准确,但它可以帮助我,至less:)。
基本上有两种types的处理(计算)发生在一台机器上:
- 处理发生在CPU上
- 在其他处理器(GPU,网卡等)上发生的处理,我们称之为IO。
所以,当我们编写一段源代码的时候,在编译之后,根据我们所使用的对象(这非常重要),处理将被CPU绑定 ,或者IO绑定 ,事实上,它可以绑定到一个组合都。
一些例子:
- 如果我使用
FileStream
对象(它是一个Stream)的Write方法,处理将会说,1%的CPU绑定和99%的IO绑定。 - 如果我使用
NetworkStream
对象(它是一个Stream)的Write方法,处理将会说,1%的CPU绑定和99%的IO绑定。 - 如果我使用
Memorystream
对象(它是一个Stream)的Write方法,处理将是100%的CPU绑定。
所以,正如你所看到的,从面向对象的程序员的angular度来看,虽然我总是访问一个Stream
对象,但是下面发生的事情可能很大程度上取决于对象的最终types。
现在,为了优化事情,如果可能和/或必要的话,能够并行运行代码(注意我不使用asynchronous字)是有用的。
一些例子:
- 在桌面应用程序中,我想打印一个文档,但我不想等待它。
- 我的Web服务器同时为许多客户端提供服务,每个客户端并行访问他的页面(而不是序列化)。
在asynchronous/等待之前,我们基本上有两个解决scheme:
- 线程 。 使用Thread和ThreadPool类相对容易。 线程只是CPU绑定的 。
- “旧” Begin / End / AsyncCallbackasynchronous编程模型。 这只是一个模型,它不会告诉你,如果你是CPU或IO绑定。 如果你看一下Socket或者FileStream类,它是IO绑定,这很酷,但我们很less使用它。
asynchronous/等待只是基于任务概念的常见编程模型 。 线程或线程池比使用CPU绑定的任务要容易一些,比起老的Begin / End模型要容易得多。 然而,这是一个超级复杂的function,它们都是完整的封装。
所以, 真正的胜利主要是IO Bound任务 ,不使用CPU的任务,但async / await仍然只是一个编程模型,它并不能帮助您确定最终如何处理。
这意味着它不是因为一个类有一个方法“DoSomethingAsync”返回一个Task对象,你可以假定它将被CPU绑定(这意味着它可能是无用的 ,特别是如果它没有取消令牌参数)或IO绑定(这意味着它可能是必须的 ),或两者的结合(因为模型是相当病毒的,结合和潜在的好处可以是,最后,超级混合和不那么明显)。
所以,回到我的例子中,在MemoryStream上使用async / await执行Write操作将保持CPU绑定(我可能不会从中受益),尽pipe我一定会从文件和networkingstream中受益。
总结其他答案:
asynchronous/等待主要是为使用它们的IO绑定任务创build的,可以避免阻塞调用线程。
在IO绑定任务的情况下,这样做的主要好处是避免阻塞UI线程。 对于非UI线程,可以有性能优势。
asynchronous不创build它自己的线程。 调用方法的线程用于执行asynchronous方法直到find一个等待方法。 然后,同一个线程继续执行async方法调用之外的其余调用方法。 在被调用的asynchronous方法中,从可等待状态返回后,可以在线程池中的一个线程上执行延续 – 唯一一个单独的线程进入图片。
实际上, async await
链是由CLR编译器生成的状态机。
async await
但是使用TPL正在使用线程池执行任务的线程。
应用程序不被阻塞的原因是状态机可以决定执行哪个协程,重复,检查和再次决定。
进一步阅读:
asynchronous&等待生成什么?
asynchronous等待和生成的状态机
asynchronousC#和F#(III):它是如何工作的? – 托马斯·佩特里切克
编辑 :
好的。 看来我的阐述是不正确的。 不过,我必须指出,状态机是async await
的重要资产。 即使你采用asynchronousI / O,你仍然需要一个帮助器来检查操作是否完成,因此我们仍然需要一个状态机,并确定哪个例程可以同时执行。