ChatGPT解决这个技术问题 Extra ChatGPT

如果 async-await 不创建任何额外的线程,那么它如何使应用程序响应?

一次又一次,我看到它说使用 async-await 不会创建任何额外的线程。这没有任何意义,因为计算机似乎一次可以做不止一件事情的唯一方法是

实际上一次做不止一件事(并行执行,利用多个处理器)

通过调度任务并在它们之间切换来模拟它(做一点点A,一点点B,一点点A等)

因此,如果 async-await 两者都没有,那么它如何使应用程序响应?如果只有 1 个线程,则调用任何方法意味着等待该方法完成后再执行任何其他操作,并且该方法中的方法必须等待结果才能继续,等等。

IO 任务不受 CPU 限制,因此不需要线程。异步的要点是在 IO 绑定任务期间不阻塞线程。
@jdweng:不,一点也不。即使它创建了新线程,这与创建新进程也有很大不同。
如果您了解基于回调的异步编程,那么您就会了解 await/async 如何在不创建任何线程的情况下工作。
它并不能完全使应用程序响应更快,但它确实阻止您阻塞线程,这是导致应用程序无响应的常见原因。
@RubberDuck:是的,它可以使用线程池中的线程来继续。但它并没有以 OP 在这里想象的方式启动一个线程——它不像它说的“采用这种普通方法,现在在一个单独的线程中运行它——那里是异步的。”它比那微妙得多。

F
Formalist

实际上,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 秒,在此期间,不会处理任何消息。

大多数处理窗口的事情都是使用消息完成的,这意味着如果消息循环停止发送消息,即使只是一秒钟,用户也会很快注意到它。例如,如果您将记事本或任何其他程序移到您自己的程序之上,然后再次离开,则会向您的程序发送一系列绘图消息,指示窗口的哪个区域现在突然再次变得可见。如果处理这些消息的消息循环正在等待某事,被阻塞,则不进行绘制。

那么,如果在第一个示例中,async/await 没有创建新线程,它是如何做到的呢?

好吧,发生的事情是您的方法分为两部分。这是那些广泛的主题类型之一,所以我不会详细介绍,但足以说明该方法分为以下两部分:

导致 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 ------+

基本上该方法执行如下:

它执行一切等待等待它调用 GetSomethingAsync 方法,该方法执行它的操作,并返回将在未来 2 秒内完成的东西到目前为止,我们仍然在对 button1_Click 的原始调用中,发生在主线程上,从消息循环。如果导致等待的代码花费大量时间,UI 仍将冻结。在我们的示例中,await 关键字和一些聪明的编译器魔法所做的并不是什么,它基本上类似于“好吧,你知道吗,我将在这里简单地从按钮单击事件处理程序返回。当你(例如,我们正在等待的事情)快来完成,让我知道,因为我还有一些代码要执行”。实际上,它会让 SynchronizationContext 类知道它已经完成,这取决于当前正在播放的实际同步上下文,它将排队等待执行。 Windows 窗体程序中使用的上下文类将使用消息循环正在抽取的队列对其进行排队。所以它返回到消息循环,现在可以自由地继续发送消息,例如移动窗口、调整窗口大小或单击其他按钮。对于用户而言,UI 现在再次响应,处理其他按钮单击、调整大小以及最重要的是重绘,因此它看起来不会冻结。 2 秒后,我们等待的事情完成了,现在发生的事情是它(嗯,同步上下文)将一条消息放入消息循环正在查看的队列中,说“嘿,我有更多代码你去执行”,这段代码就是await之后的所有代码。当消息循环到达该消息时,它将基本上“重新进入”它停止的方法,就在 await 之后并继续执行该方法的其余部分。请注意,此代码再次从消息循环中调用,因此如果此代码碰巧在没有正确使用 async/await 的情况下做了一些冗长的事情,它将再次阻塞消息循环

这里有很多活动部件,所以这里有一些指向更多信息的链接,我想说“如果你需要它”,但这个主题相当广泛,了解其中一些活动部件是相当重要的。你总是会明白 async/await 仍然是一个有漏洞的概念。一些潜在的限制和问题仍然会泄漏到周围的代码中,如果没有,您通常最终不得不调试一个看似没有充分理由随机中断的应用程序。

使用 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 关于我们的按钮点击:

Try Roslyn

我不会在这里链接完整生成的类,但它是非常血腥的东西。


所以它基本上就是 OP 所描述的“通过调度任务并在它们之间切换来模拟并行执行”,不是吗?
@Bergi 不完全是。执行是真正并行的 - 异步 I/O 任务正在进行中,并且不需要线程来继续(这是在 Windows 出现之前很久就使用的东西 - MS DOS 也使用异步 I/O,即使它没有有多线程!)。当然,await 也可以按照您描述的方式使用,但通常不是这样。只有回调被调度(在线程池上)——在回调和请求之间,不需要线程。
这就是为什么我想明确避免过多地谈论该方法的作用,因为问题是关于 async/await 的具体问题,它不会创建自己的线程。显然,它们可以用来等待线程完成。
@LasseV.Karlsen——我正在吸收你的好答案,但我仍然对一个细节感兴趣。我知道事件处理程序存在,如步骤 4 所示,它允许消息泵继续泵送,但是如果不在单独的线程上,“需要两秒钟的事情”何时何地继续执行?如果它要在 UI 线程上执行,那么它会在执行时阻止消息泵,因为它必须在同一个线程上执行一些时间..[继续]...
我喜欢你对消息泵的解释。当控制台应用程序或 Web 服务器中没有消息泵时,您的解释有何不同?如何实现方法的重入?
S
Stephen Cleary

我在我的博文 There Is No Thread 中对其进行了全面解释。

总之,现代 I/O 系统大量使用 DMA(直接内存访问)。网卡、视频卡、硬盘控制器、串行/并行端口等上都有特殊的专用处理器。这些处理器可以直接访问内存总线,并完全独立于 CPU 处理读/写。 CPU只需要通知设备内存中包含数据的位置,然后可以做自己的事情,直到设备引发中断通知CPU读/写完成。

一旦操作运行,CPU 就没有工作要做,因此没有线程。


我读完了你的文章,但仍然有一些我不明白的基本内容,因为我对操作系统的低级实现并不熟悉。我得到了你写到的内容:“写操作现在‘正在进行’。有多少线程正在处理它?没有。” .那么如果没有线程,那么如果不在线程上,操作本身是如何完成的呢?
这是成千上万的解释中缺少的一块!!!实际上有人在后台进行 I/O 操作。它不是一个线程,而是另一个专门的硬件组件来完成它的工作!
@PrabuWeerasinghe:编译器创建一个包含状态和局部变量的结构。如果一个 await 需要让出(即返回给它的调用者),该结构被装箱并存在于堆上。
@KevinBui:异步工作取决于线程池线程(工作线程和 I/O 线程)的存在。特别是,I/O 完成端口需要专用的 I/O 线程来处理来自操作系统的完成请求。所有异步 I/O 都需要这个,但异步的好处是每个请求不需要一个线程。
@noelicus:最初的问题是 async/await 是否启动新线程,而他们没有。如果您在同步方法上有 async 修饰符(没有 await),那么编译器会警告您它将同步运行(直接在调用线程上)。对于 CPU 密集型工作,通常使用 await Task.Run,在这种情况下,Task.Run 是使其在线程池线程上运行的原因。
B
BoltClock

一台计算机看起来一次做多于一件事的唯一方法是(1)实际上一次做多于一件事,(2)通过调度任务并在它们之间切换来模拟它。因此,如果 async-await 两者都没有

并不是那些都没有等待。请记住,await 的目的不是让同步代码神奇地异步。这是为了在调用异步代码时使用我们用于编写同步代码的相同技术。 Await 是关于使使用高延迟操作的代码看起来像使用低延迟操作的代码。那些高延迟操作可能在线程上,它们可能在专用硬件上,它们可能将它们的工作分解成小块并将其放入消息队列中,以便稍后由 UI 线程处理。他们正在做一些事情来实现异步,但他们是在做这件事的人。 Await 只是让您利用这种异步性。

另外,我认为您缺少第三种选择。我们这些老人——今天带着说唱音乐的孩子们应该离开我的草坪,等等——记得 1990 年代初期的 Windows 世界。没有多 CPU 机器,也没有线程调度程序。你想同时运行两个 Windows 应用程序,你必须让步。多任务处理是合作的。操作系统告诉一个进程它可以运行,如果它行为不端,它将使所有其他进程无法获得服务。它一直运行直到它屈服,并且不知何故,它必须知道如何在下次操作系统将控制权交还给它时从中断的地方恢复。单线程异步代码很像这样,用“await”而不是“yield”。等待的意思是“我要记住我在这里停下来的地方,让别人跑一会儿;等我等待的任务完成后给我回电话,我会从停下来的地方继续。”我认为您可以看到这如何使应用程序响应更快,就像它在 Windows 3 天中所做的那样。

调用任何方法意味着等待方法完成

有你缺少的钥匙。方法可以在其工作完成之前返回。这就是异步的本质。一个方法返回,它返回一个任务,意思是“这项工作正在进行中;告诉我完成后要做什么”。该方法的工作没有完成,即使它已经返回。

在 await 运算符之前,您必须编写看起来像意大利面条穿过瑞士奶酪的代码,以处理完成后我们还有工作要做的事实,但返回和完成是不同步的。 Await 允许您编写看起来像返回和完成是同步的代码,但实际上它们并不同步。


其他现代高级语言也支持类似的显式协作行为(即函数做一些事情,产生[可能向调用者发送一些值/对象],当控制权交还时继续它停止的地方[可能提供额外的输入] )。一方面,生成器在 Python 中非常大。
@JAB:当然。生成器在 C# 中称为“迭代器块”并使用 yield 关键字。 C# 中的 async 方法和迭代器都是 coroutine 的一种形式,它是一个函数的总称,它知道如何暂停其当前操作以便稍后恢复。如今,许多语言都有协程或类似协程的控制流。
与产量的类比是一个很好的类比——它是一个进程内的协作多任务。 (从而避免了全系统协同多任务的系统稳定性问题)
我认为用于 IO 的“cpu 中断”的概念并不了解很多调制解调器“程序员”,因此他们认为线程需要等待每一位 IO。
@user469104:我的回答最后几段的重点是对比工作流的 completion,这是关于工作流状态的事实,而 return 是事实关于控制流。正如您所注意到的,通常不要求工作流在返回之前完成;在 C# 2 中,yield return 为我们提供了在完成之前返回的工作流。 async 工作流程相同;他们在完成之前返回。
C
Community

我真的很高兴有人问了这个问题,因为在很长一段时间内我也相信线程对于并发是必要的。当我第一次看到事件循环时,我认为它们是谎言。我心想“如果这段代码在单线程中运行,就不可能并发”。请记住,这是在我已经经历了理解并发和并行性之间差异的斗争之后。

经过我自己的研究,我终于找到了缺失的部分:select()。具体来说,IO 多路复用,由不同名称的各种内核实现:select()poll()epoll()kqueue()。这些是 system calls,虽然实现细节不同,但允许您传入一组 file descriptors 来观看。然后,您可以进行另一个调用,该调用会阻塞,直到其中一个被监视的文件描述符发生更改。

因此,可以等待一组 IO 事件(主事件循环),处理第一个完成的事件,然后将控制权交还给事件循环。冲洗并重复。

这是如何运作的?嗯,简短的回答是它是内核和硬件级别的魔法。计算机中除了CPU之外还有很多组件,这些组件可以并行工作。内核可以控制这些设备并直接与它们通信以接收某些信号。

这些 IO 多路复用系统调用是单线程事件循环(如 node.js 或 Tornado)的基本构建块。当您await 一个函数时,您正在监视某个事件(该函数的完成),然后将控制权交还给主事件循环。当您正在观看的事件完成时,该功能(最终)从它停止的地方开始。允许您像这样暂停和恢复计算的函数称为 coroutines


M
Margaret Bloom

awaitasync 使用 任务 而不是线程。

框架有一个线程池准备以Task对象的形式执行一些工作;将任务提交到池意味着选择一个空闲的、已经存在的线程来调用任务操作方法。创建一个任务就是创建一个新对象,比创建一个新线程快得多。

给定一个 Task 可以附加一个 Continuation 到它,它是一个新的 Task 对象,一旦线程结束就会被执行。

由于 async/await 使用 Task,它们不会创建新的 线程。

虽然在每个现代操作系统中都广泛使用中断编程技术,但我认为它们在这里并不相关。
您可以在单个 CPU 中并行(实际上是交错)执行两个 CPU 绑定任务使用 aysnc/await
这不能简单地用操作系统支持排队 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();

实际上一个池可以有它的任务创建策略。


在遇到等待时,控件将返回给调用者。我明白这一点。但是调用异步函数的线程会释放到线程池中吗?例如在 Windows 应用程序中。
@variable 我必须重新了解它在.NET 上的工作原理,但是是的。刚刚调用的 async 函数返回,这意味着编译器创建了一个等待器并向其附加了一个延续(当真正异步的等待事件完成时,任务的等待器将调用该延续)。所以线程无事可做,可以返回池中,这意味着它可以接其他工作。
我想知道由于 syncronziarion 上下文,UI 是否总是被分配相同的线程,你知道吗?在这种情况下,线程将不会返回到池中,并将由 UI 线程用于运行异步方法调用之后的代码。我是这个领域的新手。
@变量 It seems you have to manually call the app dispatcher to make sure the code runs in the UI thread。虽然那个代码对我来说很糟糕。 This 是一个更好的例子。显然,这件事有点复杂,涉及到 GUI 线程的 SynchronizationContext。 ...
...如果 async 函数的调用者有一个 SynchronizationContext(就像 GUI 线程一样),则延续被包装在一个调用中,该调用将在原始上下文中调度它。请参阅this。您的处理程序必须是 async 才能工作。
S
Simon Mourier

以下是我对这一切的看法,它在技术上可能不是超级准确,但至少对我有帮助:)。

机器上基本上有两种类型的处理(计算):

CPU 上发生的处理

发生在其他处理器(GPU、网卡等)上的处理,我们称它们为 IO。

所以,当我们写一段源码的时候,编译之后,根据我们使用的对象(这点很重要),处理会是CPU bound,或者IO bound,其实可以绑定一个组合两个都。

一些例子:

如果我使用 FileStream 对象(它是一个 Stream)的 Write 方法,处理将是 1% CPU 限制和 99% IO 限制。

如果我使用 NetworkStream 对象(它是一个 Stream)的 Write 方法,处理将是 1% CPU 限制和 99% IO 限制。

如果我使用 Memorystream 对象(它是一个 Stream)的 Write 方法,处理将是 100% CPU 绑定。

因此,正如您所看到的,从面向对象的程序员的角度来看,虽然我总是访问 Stream 对象,但下面发生的事情可能在很大程度上取决于对象的最终类型。

现在,为了优化事情,如果可能和/或必要的话,能够并行运行代码有时很有用(注意我不使用异步这个词)。

一些例子:

在桌面应用程序中,我想打印一个文档,但我不想等待它。

我的网络服务器同时为许多客户端提供服务器,每个客户端都并行获取他的页面(未序列化)。

在 async / await 之前,我们基本上有两种解决方案:

线程。它相对容易使用,带有 Thread 和 ThreadPool 类。线程仅受 CPU 限制。

“旧”的 Begin/End/AsyncCallback 异步编程模型。它只是一个模型,它不会告诉您是否会受 CPU 或 IO 限制。如果你看一下 Socket 或 FileStream 类,它是 IO 绑定的,这很酷,但我们很少使用它。

async / await 只是一种常见的编程模型,基于 Task 概念。对于 CPU 绑定任务,它比线程或线程池更易于使用,并且比旧的 Begin/End 模型更易于使用。然而,卧底,它“只是”一个超级复杂的功能完整的包装器。

因此,真正的胜利主要是在 IO Bound 任务上,即不使用 CPU 的任务,但 async/await 仍然只是一种编程模型,它不能帮助您确定最终将如何/在何处进行处理。

这意味着不是因为一个类有一个方法“DoSomethingAsync”返回一个 Task 对象,你可以假定它是 CPU 绑定的(这意味着它可能非常无用,特别是如果它没有取消令牌参数),或者 IO 绑定(这意味着它可能是必须的),或者两者兼而有之(由于该模型非常具有病毒性,因此最终的结合和潜在好处可能是超级混合的,而且不那么明显)。

因此,回到我的示例,在 MemoryStream 上使用 async/await 执行写入操作将保持 CPU 受限(我可能不会从中受益),尽管我肯定会从文件和网络流中受益。


这是一个很好的答案,使用 theadpool 进行 cpu 绑定工作很差,因为 TP 线程应该用于卸载 IO 操作。受 CPU 限制的工作 imo 应该在警告的情况下阻塞,并且没有什么可以阻止使用多线程。
A
Andrew Savinykh

我不打算与 Eric Lippert 或 Lasse V. Karlsen 以及其他人竞争,我只是想提请注意这个问题的另一个方面,我认为没有明确提及。

单独使用 await 不会使您的应用程序具有神奇的响应能力。如果您在等待 UI 线程阻塞的方法中执行任何操作,它仍会像不可等待版本一样阻塞您的 UI

您必须专门编写您的可等待方法,以便它生成一个新线程或使用类似完成端口的东西(它将在当前线程中返回执行,并在完成端口收到信号时调用其他东西以继续)。但这部分在其他答案中得到了很好的解释。


这首先不是一场比赛。这是一次合作!
C
Calmarius

我试图自下而上地解释它。也许有人觉得它有帮助。当我在 DOS 中用 Pascal 制作简单的游戏时,我在那里,完成了,重新发明了它(美好的旧时光......)

所以...每个事件驱动的应用程序内部都有一个事件循环,如下所示:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

框架通常会向您隐藏这些细节,但它就在那里。 getMessage 函数从事件队列中读取下一个事件或等待事件发生:鼠标移动、keydown、keyup、单击等。然后 dispatchMessage 将事件分派给适当的事件处理程序。然后等待下一个事件,依此类推,直到出现退出循环并完成应用程序的退出事件。

事件处理程序应该快速运行,以便事件循环可以轮询更多事件并且 UI 保持响应。如果按钮单击触发了这样的昂贵操作会发生什么?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

好吧,在 10 秒的操作完成之前,UI 变得无响应,因为控件保持在函数内。要解决这个问题,您需要将任务分解成可以快速执行的小部分。这意味着您无法在一个事件中处理整个事情。您必须完成一小部分工作,然后将另一个事件发布到事件队列以请求继续。

因此,您可以将其更改为:

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 事件。

只要这个长时间运行的任务在运行,它的延续事件就一直在事件队列中。所以你基本上发明了你自己的任务调度器。队列中的延续事件是正在运行的“进程”。实际上这是操作系统所做的,除了发送继续事件并返回调度程序循环是通过操作系统注册上下文切换代码的 CPU 的定时器中断完成的,所以你不需要关心它。但是在这里您正在编写自己的调度程序,因此您确实需要关心它 - 到目前为止。

因此,我们可以通过将它们分成小块并发送延续事件,在与 GUI 并行的单个线程中运行长时间运行的任务。这是 Task 类的一般概念。它代表一个工作,当您在其上调用 .ContinueWith 时,您定义在当前工作完成时要调用的函数作为下一个工作(并将其返回值传递给延续)。但是手动将所有这些链接分解成小块是一项繁琐的工作,并且完全打乱了逻辑的布局,因为整个后台任务代码基本上是 .ContinueWith 混乱。所以这就是编译器可以帮助你的地方。它在引擎盖下为您完成所有这些链接和延续。当您说 await 时,您告诉编译器“停在这里,将函数的其余部分添加为延续任务”。编译器负责其余的工作,因此您不必这样做。

虽然这个任务块链接不涉及创建线程,并且当块很小时,它们可以在主线程的事件循环中调度,但实际上有一个运行任务的工作线程池。这可以更好地利用 CPU 内核,还允许开发人员运行手动编写的长任务(这将阻塞工作线程而不是主线程)。


多么完美的例证我很佩服你的解释+1。鉴于作为 Z 代的人,我不知道过去发生了什么,也不知道过去是如何发生的,所以所有老家伙都应该以您在此处已经完成的方式解释类似的概念。
我终于明白了。每个人都说“没有线程”,但没有人以某种方式说有一个,即线程池中的一个(至少一个)。这些也是线程还是我做错了什么?
@deralbert线程池在那里,因为任务不仅用于实现异步等待。您可以手动创建一个 Task 对象,该对象执行昂贵的操作而无需分块。当你运行它时,它会阻止池中的工作线程而不是主线程。但是小块的 async-await 任务块仍然执行得很快,它们不会阻塞,因此它们甚至可以在没有额外线程的情况下在主线程上运行。 (更新了答案以减少误导。)
v
vaibhav kumar

总结其他答案:

Async/await 通常是为 IO 绑定任务创建的,因为通过使用它们,调用线程不需要被阻塞。这在 UI 线程的情况下特别有用,因为我们可以确保它们在执行后台操作时保持响应(例如从远程服务器获取要显示的数据)

异步不会创建它自己的线程。调用方法的线程用于执行异步方法,直到找到可等待对象。然后,同一个线程继续执行异步方法调用之外的其余调用方法。请注意,在被调用的异步方法中,在从等待返回后,可以使用线程池中的线程执行该方法的提醒——这是唯一出现单独线程的地方。


很好的总结,但我认为它应该再回答 2 个问题才能给出完整的画面: 1. 等待的代码在哪个线程上执行? 2. 谁控制/配置上述线程池 - 开发人员或运行时环境?
1. 在这种情况下,大部分等待的代码是一个不使用 CPU 线程的 IO 绑定操作。如果希望将 await 用于 CPU 绑定操作,则可以生成一个单独的任务。 2. 线程池中的线程由 TPL 框架中的任务调度器管理。
W
Welcor

这不是直接回答问题,但我认为这是一个有趣的附加信息:

Async 和 await 本身不会创建新线程。但是根据您使用异步等待的位置,等待之前的同步部分可能在与等待之后的同步部分不同的线程上运行(例如,ASP.NET 和 ASP.NET 核心的行为不同)。

在基于 UI 线程的应用程序(WinForms、WPF)中,您将在之前和之后处于同一个线程上。但是当你在线程池线程上使用 async away 时,等待之前和之后的线程可能不一样。

A great video on this topic


S
Steve Fan

实际上,async await 链是 CLR 编译器生成的状态机。

async await 但是确实使用 TPL 正在使用线程池执行任务的线程。

应用程序没有被阻塞的原因是状态机可以决定执行哪个协程,重复,检查,再决定。

进一步阅读:

What does async & await generate?

Async Await and the Generated StateMachine

Asynchronous C# and F# (III.): How does it work? - Tomas Petricek

编辑:

好的。看来我的阐述是不正确的。但是我必须指出,状态机是 async await 的重要资产。即使您采用异步 I/O,您仍然需要一个助手来检查操作是否完成,因此我们仍然需要一个状态机并确定哪个例程可以一起异步执行。