我已阅读关于async await的各种文章,现在正在尝试深入理解await async。我的问题是我发现等待异步方法并不会创建新线程,而只是使UI响应。这是正确的。非常重要的一点是要意识到await的含义是异步等待。它并不意味着“使此操作异步化”。而是指:
- 此操作已经是异步的。
- 如果操作完成,请获取其结果。
- 如果操作未完成,请将其余部分作为不完整操作的继续项返回给调用方。
- 当不完整操作变为完成状态时,它将安排继续执行。
这是错误的。您没有正确考虑时间优势。想象这种情况:假设一个没有ATM的世界,我成长于那个世界,那是一个奇怪的时代。银行通常有一排人在等待存取款。假设该银行只接受和提供单美元纸币。现在,假设排队有三个人,他们每个人都想要十美元,而你只想要一个美元。下面是两个算法......
另一个优势是:银行柜员很昂贵;这个系统只雇用一个柜员就能完成小任务并获得良好的吞吐量。而在同步系统中,要想获得良好的吞吐量,就需要雇佣更多的柜员,这非常昂贵。
Task.WhenAll() 或 Task.WhenAny() 是否也适用此原理?
它们不会创建线程,只是接受一堆任务,在所有/任意任务完成时完成。
创建 getStringTask 任务时,另一个线程会复制当前上下文并开始执行 GetStringAsync 方法。
绝对不是这样的。该任务已经是异步的,并且由于它是 IO 任务,所以不需要线程。IO 硬件已经是异步的。因此不需要新工人。
等待 getStringTask 时,我们会看看其他线程是否已完成其任务。
不,没有其他线程。我们查看 IO 硬件是否已完成其任务。没有线程。
当你把面包放进烤面包机,然后去查看电子邮件时,烤面包机里没有人运行烤面包机。你可以启动一个异步作业,然后去做其他事情,因为你有特殊目的的硬件,其本质是异步的。网络硬件和烤面包机一样都是如此。没有线程。没有小人在你的烤面包机里运行它。它自己运行。
如果没有完成任务,控制权将返回 AccessTheWebAsync() 方法的调用方,直到另一个线程完成任务以恢复控制。
再次强调,没有其他线程。
但控制流正确。 如果任务已完成,则获取任务的值。如果未完成,则将控制返回给调用者,并将当前工作流的剩余部分分配为任务的后续过程。 任务完成后,后续过程将被安排运行。
我真的不明白等待任务时如何不会创建其他线程。
再次思考一下,当你在生活中因为受阻而停止做某个任务,然后做其他事情一段时间,最后在解除阻塞后再次开始执行第一个任务的时候,是否需要雇佣一个工人?当然不需要。但是你还是可以在烤面包机里烤面包的同时煮鸡蛋。基于任务的异步只是把这种现实生活的工作流程放到了软件中。
我总是惊叹于你们这些年轻人,你们听着奇怪的音乐却表现得好像线程一直存在,而做多任务没有其他方法。我学编程时的操作系统没有线程。如果你想让两个事情看起来同时发生,你必须自己构建异步;它不是内置在语言或操作系统中的。但我们成功了。
合作式单线程异步是回归到引入线程作为控制流结构之前的世界,这是一个更优雅、更简单的世界。在协作式多任务系统中,await 是一个挂起点。在没有引入线程的 Windows 中,你需要调用 Yield()
来实现,而我们也没有语言支持来创建 Continuations 和 Closures;如果你想让状态在 yield 之间保持不变,你需要编写代码来实现。你们现在可真幸福啊!
正如你所说的那样,只是没有使用线程。检查任务是否完成,如果完成了,你就完成了。如果没有完成,将剩余的工作流程安排为任务的后续操作,并返回。这就是 await
的全部内容。
我们在设计该功能时担心人们会像你现在可能仍然相信一样,认为 "await" 对其后面的 调用 做了某些事情。它没有。Await 对的是返回值。同样,当你看到:
int foo = await FooAsync();
你应该在脑海中想象:
Task<int> task = FooAsync();
if (task is not already completed)
set continuation of task to go to "resume" on completion
return;
resume: // If we get here, task is completed
int foo = task.Result;
使用await调用方法并不是一种特殊的调用方式。“await”并没有启动任何线程等操作,它只是对返回值进行操作的运算符。
所以等待任务并不会启动一个线程。等待任务(1)检查任务是否完成,(2)如果没有完成,则将剩余的方法作为任务的继续项分配,并返回。这就是全部内容。await并不会创建线程。现在,也许被调用的方法会启动一个线程,但那是它自己的事情。这与await无关,因为await发生在调用返回之后。被调用函数不知道其返回值正在被等待。
假设我们等待一个需要大量计算的CPU绑定任务。目前我所知道的是,如果是I/O绑定代码,它将在低级别CPU组件上执行(比线程低得多),并且仅使用一个线程简短地通知上下文有关已完成任务的状态。
关于上面对FooAsync的调用,我们知道它是异步的并且返回一个任务。我们不知道它是如何异步的。这是FooAsync作者的事情!但是,FooAsync作者可以使用三种主要技术来实现异步。正如您所指出的那样,主要技术有两种:
如果任务因需要在当前机器上另一个CPU上进行长时间计算而具有高延迟性,则获得工作线程并开始在另一个CPU上执行工作是有意义的。当工作完成时,相关联的任务可以将其继续项安排回到UI线程(如果该任务是在UI线程上创建的)或者在适当的其他工作线程上运行。
如果任务因需要与慢速硬件(例如磁盘或网络)通信而具有高延迟性,则像您指出的那样,没有线程。专用硬件以异步方式完成任务,最终由操作系统提供的中断处理负责在正确的线程上调度任务完成。
第三个异步原因不是因为您正在管理高延迟操作,而是因为您正在将算法分解为小部分并将它们放在工作队列中。也许您正在制作自己的自定义调度程序、实施Actor模型系统或尝试进行无栈编程等。没有线程,没有I / O,但存在异步性。
所以再次强调,awaiting并不会使某些东西在工作线程上运行。调用启动工作线程的方法才会使某些东西在工作线程上运行。让您调用的方法决定是否创建工作线程。 异步方法已经是异步的。您不需要对它们做任何事情来使它们异步化。await并不会使任何内容异步化。
Await存在的唯一目的是为了使开发人员更容易检查异步操作是否已完成,并在未完成时将当前方法的剩余部分作为继续部分进行注册。这就是await的用途。再次强调,await不会创建异步性。await有助于构建异步工作流程。等待是工作流程中必须完成异步任务才能继续的点。
那是正确的。如果您拥有同步方法,并且知道它是CPU绑定的,并且希望将其异步化,并且知道该方法在另一个线程上运行是安全的,则Task.Run将找到一个工作线程,安排委托在工作线程上执行,并给您代表异步操作的任务。您只应该使用非常耗时的方法(1)超过30毫秒,比如CPU绑定的方法(2),(3)安全地调用另一个线程。否则可能会出现问题。如果雇用一个工人做不到30毫秒的工作,那么想想现实生活。如果您有一些计算要做,买广告、面试候选人、雇用人员、让他们加起三十个数字,然后解雇他们是有意义的吗?雇用一个工作线程是很昂贵的。如果雇佣成本比自己做这项工作更高,那么雇佣线程不会带来任何性能提升;反而会使情况变得更糟。如果您雇用一个工人来执行IO绑定任务,您就相当于雇用一个工人坐在邮箱旁边几年,并在邮件到达时大喊特喊。这并不能使邮件到达更快。这只是浪费了可以用于解决其他问题的工作资源。如果您雇用一个工人来执行非线程安全的任务,那么如果您雇用两个工人,告诉他们同时驾驶同一辆车到两个不同的地点,他们将在高速公路上争夺方向盘时撞车。