一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有1个线程,那么调用任何方法都意味着在执行其他操作之前等待该方法完成,并且该方法中的方法必须在继续执行之前等待结果,以此类推。
一次又一次,我看到它说使用async-await不会创建任何额外的线程。这是没有道理的,因为计算机一次可以做多件事的唯一方法是
实际上同时做多件事(并行执行,使用多个处理器) 通过调度任务并在它们之间切换来模拟它(做一点a,一点B,一点a,等等)。
因此,如果async-await都不做这些,那么它如何使应用程序具有响应性呢?如果只有1个线程,那么调用任何方法都意味着在执行其他操作之前等待该方法完成,并且该方法中的方法必须在继续执行之前等待结果,以此类推。
当前回答
我不打算和Eric Lippert或者Lasse V. Karlsen等人竞争,我只是想让大家注意这个问题的另一个方面,我想这个问题没有明确提到。
单独使用await并不能让你的应用神奇地做出响应。如果不管你在方法中做什么,你正在等待的UI线程阻塞,它仍然会阻塞你的UI,就像不可等待的版本一样。
你必须编写你的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方法,以便它产生一个新线程或使用一个完成端口之类的东西(它将在当前线程中返回执行,并在完成端口收到信号时调用其他东西来继续)。但这部分在其他答案中有很好的解释。
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实际上,一个池可以有自己的任务创建策略。
我真的很高兴有人问这个问题,因为很长一段时间以来,我也认为线程对于并发性是必要的。当我第一次看到事件循环时,我以为它们是谎言。我对自己说:“如果这段代码在一个线程中运行,它就不可能是并发的”。请记住,这是在我已经经历了理解并发性和并行性之间区别的斗争之后。
经过我自己的研究,我终于找到了缺失的部分:select()。具体来说,IO多路复用,由不同的内核以不同的名称实现:select(), poll(), epoll(), kqueue()。这些是系统调用,尽管实现细节不同,但允许您传入一组文件描述符进行监视。然后,您可以进行另一个调用,该调用将阻塞,直到被监视的文件描述符之一发生变化。
因此,可以等待一组IO事件(主事件循环),处理第一个完成的事件,然后将控制权交还给事件循环。清洗并重复。
这是如何工作的呢?简而言之,这是内核和硬件级的魔力。计算机中除了CPU之外还有许多组件,这些组件可以并行工作。内核可以控制这些设备,并直接与它们通信以接收特定的信号。
这些IO多路复用系统调用是单线程事件循环(如node.js或Tornado)的基本构建块。当您等待一个函数时,您正在观察某个事件(该函数的完成),然后将控制权交还给主事件循环。当您正在观看的事件完成时,函数(最终)从它停止的地方开始。允许像这样暂停和恢复计算的函数称为协程。