关于线程的混淆以及asynchronous方法在C#中是否真正asynchronous

我正在阅读asynchronous/等待,当Task.Yield可能是有用的,并遇到这个职位。 我对这个post的下面有一个问题:

当你使用async / await时,不能保证你在等待FooAsync()时调用的方法实际上是asynchronous运行的。 内部实现可以使用完全同步的path自由返回。

这对我来说有点不清楚,可能是因为我头脑中的asynchronous定义不alignment。

在我看来,因为我主要做UI开发,asynchronous代码是不在UI线程上运行的代码,而是在其他线程上运行。 我想在我引用的文本中,如果某个方法在任何线程上阻塞(即使它是一个线程池线程),也不是真正的asynchronous方法。

题:

如果我有一个长期运行的任务是CPU绑定(假设它正在做很多的硬性math),那么asynchronous运行该任务必须阻止一些线程是正确的? 有些事情必须真正做math。 如果我等待它,那么一些线程被阻塞。

什么是一个真正的asynchronous方法的例子,他们将如何实际工作? 那些仅限于I / O操作,它利用了一些硬件function,所以没有线程被阻塞?

这对我来说有点不清楚,可能是因为我头脑中的asynchronous定义不alignment。

很好,请求澄清。

在我看来,因为我主要做UI开发,asynchronous代码是不在UI线程上运行的代码,而是在其他线程上运行。

这个信念是常见的但是是错误的 没有要求在任何第二个线程上运行asynchronous代码。

想象一下,你正在煮早餐。 你在烤面包机里放一些烤面包片,当你在等待烤面包popup的时候,你从昨天开始通过你的邮件,付一些账单,嘿,烤面包就弹了出来。 你完成支付账单,然后去烤面包。

你在那里雇用第二名工人来看你的烤面包机?

你没有。 线程是工人。 asynchronous工作stream可以在一个线程上发生。 asynchronous工作stream程的要点是避免雇用更多的员工,如果可以避免的话。

如果我有一个长期运行的任务是CPU绑定(假设它正在做很多的硬性math),那么asynchronous运行该任务必须阻止一些线程是正确的? 有些事情必须真正做math。

在这里,我会给你一个难以解决的问题。 这是一列100个数字; 请手动添加。 所以你把第一个添加到第二个,并总计。 然后你将运行总数加到第三个并得到总数。 那么,哦,地狱,数字的第二页是失踪。 记住你在哪里,去烤面包。 噢,烤面包的时候,还剩下一个字母, 当你完成烤面包的时候,继续把这些数字加起来,记得在下一次有空的时候吃烤面包片。

你雇用另外一名工人join这个数字的地方在哪里? 计算上昂贵的工作不需要是同步的,并且不需要阻塞线程 。 使计算工作潜在asynchronous的事情是阻止它的能力,记住你在哪里,去做别的事情,记住该做什么,然后重新开始。

现在肯定有可能聘请第二名工人,他们什么都不做,只是增加了数字,然后被解雇了。 你可以问那个工人“你做完了吗?” 如果答案是否定的,你可以去做一个三明治,直到完成。 这样,你和工人都很忙。 但是不要求不同步涉及多名工人。

如果我等待它,那么一些线程被阻塞。

不不不。 这是你误解中最重要的部分。 await并不意味着“asynchronous开始这项工作”。 await意味着“我有一个asynchronous生成的结果可能无法使用,如果不可用, 请在这个线程上找一些其他的工作,这样我们就不会阻塞这个线程,等待刚才所说的相反

什么是一个真正的asynchronous方法的例子,他们将如何实际工作? 那些仅限于I / O操作,它利用了一些硬件function,所以没有线程被阻塞?

asynchronous工作通常涉及自定义硬件或multithreading,但不是必需的。

不要考虑工人 。 考虑工作stream程 。 asynchronous的本质是把工作stream分解成小部分,这样就可以确定这些部分必须发生的顺序 ,然后依次执行每个部分 ,但是允许不相互依赖的部分交错

在asynchronous工作stream程中,您可以轻松检测出表示零件之间依赖关系的工作stream中的位置 。 这些部件标有await 。 这就是await的含义:接下来的代码取决于这部分工作stream程的完成,所以如果没有完成,就去找其他的任务去做, await任务完成后再回来。 整个问题的关键在于让工人继续工作,即使在一个将来也会产生需要的结果的世界里。

我正在阅读asynchronous/等待

我可以推荐我的async介绍吗?

当Task.Yield可能是有用的

几乎从不。 我发现它在进行unit testing时偶尔有用。

在我看来,因为我主要做UI开发,asynchronous代码是不在UI线程上运行的代码,而是在其他线程上运行。

asynchronous代码可以是无线的 。

我想在我引用的文本中,如果某个方法在任何线程上阻塞(即使它是一个线程池线程),也不是真正的asynchronous方法。

我会说这是正确的。 我使用术语“真正的asynchronous”的操作,不阻止任何线程(和不同步)。 对于出现asynchronous的操作,我也使用“伪asynchronous”这个术语,但是只能以这种方式工作,因为它们运行在或阻塞了一个线程池线程。

如果我有一个长期运行的任务是CPU绑定(假设它正在做很多的硬性math),那么asynchronous运行该任务必须阻止一些线程是正确的? 有些事情必须真正做math。

是; 在这种情况下,你会想要定义一个同步API的工作(因为它是同步工作),然后你可以使用Task.Run从你的UI线程调用它,例如:

 var result = await Task.Run(() => MySynchronousCpuBoundCode()); 

如果我等待它,那么一些线程被阻塞。

没有; 线程池线程将被用于运行代码(实际上并不被阻塞),并且UI线程asynchronous地等待该代码完成(也不被阻塞)。

什么是一个真正的asynchronous方法的例子,他们将如何实际工作?

NetworkStream.WriteAsync (间接)要求网卡写出一些字节。 没有线程负责一次写出一个字节,并等待每个字节被写入。 网卡处理所有这些。 当网卡写完所有字节后,它(最终)完成从WriteAsync返回的任务。

那些仅限于I / O操作,它利用了一些硬件function,所以没有线程被阻塞?

不完全,尽pipeI / O操作是简单的例子。 另一个相当简单的例子是定时器(例如, Task.Delay )。 尽pipe你可以围绕任何types的“事件”构build一个真正的asynchronousAPI。

当你使用async / await时,不能保证你在等待FooAsync()时调用的方法实际上是asynchronous运行的。 内部实现可以使用完全同步的path自由返回。

这对我来说有点不清楚,可能是因为我头脑中的asynchronous定义不alignment。

这只是意味着在调用asynchronous方法时有两种情况。

首先,在将任务返回给您时,操作已经完成 – 这将是一个同步path。 第二个是操作仍在进行中 – 这是asynchronouspath。

考虑这个代码,它应该显示这两个path。 如果密钥在caching中,则会同步返回。 否则,启动一个调出数据库的asynchronous操作:

 Task<T> GetCachedDataAsync(string key) { if(cache.TryGetvalue(key, out T value)) { return Task.FromResult(value); // synchronous: no awaits here. } // start a fully async op. return GetDataImpl(); async Task<T> GetDataImpl() { value = await database.GetValueAsync(key); cache[key] = value; return value; } } 

所以通过理解,你可以推断理论上, database.GetValueAsync()的调用可能有一个类似的代码,它本身可以同步返回:所以即使你的asynchronouspath可能最终同步运行100%。 但是你的代码不需要关心:asynchronous/等待无缝处理这两种情况。

如果我有一个长期运行的任务是CPU绑定(假设它正在做很多的硬性math),那么asynchronous运行该任务必须阻止一些线程是正确的? 有些事情必须真正做math。 如果我等待它,那么一些线程被阻塞。

阻塞是一个明确定义的术语 – 这意味着您的线程在等待某些内容(I / O,互斥锁等)时已经放弃了执行窗口。 所以你的线程做math不被认为是阻塞的:它实际上是在执行工作。

什么是一个真正的asynchronous方法的例子,他们将如何实际工作? 那些仅限于I / O操作,它利用了一些硬件function,所以没有线程被阻塞?

“真正的asynchronous方法”将是一个永远不会阻止的方法。 它通常最终涉及I / O,但也可能意味着当您想要将当前线程用于某些其他内容(如在UI开发中)或者尝试引入并行时, await您沉重的math代码:

 async Task<double> DoSomethingAsync() { double x = await ReadXFromFile(); Task<double> a = LongMathCodeA(x); Task<double> b = LongMathCodeB(x); await Task.WhenAll(a, b); return a.Result + b.Result; } 

这个主题是相当广泛的,可能会出现一些讨论。 但是,在C#中使用asyncawait被认为是asynchronous编程。 但是,asynchronous工作是完全不同的讨论。 直到.NET 4.5没有asynchronous和等待关键字,开发人员必须直接针对任务并行图书馆 (TPL)开发。 在那里开发人员完全控制何时以及如何创build新的任务,甚至是线程。 然而,由于不是这方面的专家,所以这个方法有一个不好的地方,那就是应用程序可能因为线程之间的竞争条件等原因而导致性能问题严重和bug。

从.NET 4.5开始,引入了asynchronous和等待关键字,并采用了asynchronous编程的新方法。 asynchronous和等待关键字不会导致创build额外的线程。 asynchronous方法不需要multithreading,因为asynchronous方法不能在自己的线程上运行。 该方法在当前的同步上下文上运行,并且仅在该方法处于活动状态时才在该线程上使用时间。 您可以使用Task.Run将CPU绑定的工作移至后台线程,但后台线程无助于只等待结果可用的进程。

asynchronous编程的asynchronous方法比几乎所有情况下的现有方法更可取。 特别是,这种方法比BackgroundWorker对于IO绑定操作更好,因为代码更简单,而且不必防范竞争条件。 您可以在这里阅读更多关于这个主题。

我不认为自己是一个C#黑带,一些更有经验的开发人员可能会进一步讨论,但作为一个原则,我希望我能够回答你的问题。

asynchronous并不意味着并行

asynchronous只意味着并发。 实际上,即使使用显式线程也不能保证它们将同时执行(例如,当线程对同一个单核心有亲和力,或者更常见的情况是,机器中只有一个核心开始)。

因此,你不应该期望一个asynchronous操作同时发生在别的东西上。 asynchronous只意味着它会发生,最终在另一个时间一个 (希腊)=没有, syn (希腊语)=在一起, khronos (希腊语)=时间=> asynchronous =不同时发生)。

注意:asynchronous的概念是在代码实际运行时,你不关心调用。 这可以使系统在可能的情况下利用并行性来执行操作。 它甚至可能立即运行。 它甚至可能发生在同一个线程…更多的在后面。

当你awaitasynchronous操作时,你正在创build并发com (latin)= together, currere (latin)= run。=>“Concurrent”= 一起运行 )。 这是因为在继续之前,您正在要求asynchronous操作完成。 我们可以说执行收敛。 这与join线程的概念相似。


asynchronous时不能并行

当你使用async / await时,不能保证你在等待FooAsync()时调用的方法实际上是asynchronous运行的。 内部实现可以使用完全同步的path自由返回。

这可以通过三种方式发生:

  1. 任何返回Task东西都可以使用await 。 当你收到Task它可能已经完成。

    然而,这并不意味着它同步运行。 事实上,它build议它在asynchronous运行之前完成,并在获得Task实例之前完成。

    请记住,您可以await已完成的任务:

     private static async Task CallFooAsync() { await FooAsync(); } private static Task FooAsync() { return Task.CompletedTask; } private static void Main() { CallFooAsync().Wait(); } 

    另外,如果一个async方法没有await ,它将同步运行。

    注意:正如你已经知道的,一个返回一个Task的方法可能正在等待networking或文件系统等…这样做并不意味着在ThreadPool上启动一个新的Thread或排队。

  2. 在由单个线程处理的同步上下文中,结果将是同步执行Task ,并带来一些开销。 这是UI线程的情况,我会更多地讨论下面发生的事情。

  3. 可以编写自定义的TaskScheduler来始终同步运行任务。 在同一个线程上,调用。

    注意:最近我写了一个自定义的SyncrhonizationContext ,它在单个线程上运行任务。 你可以在创build一个(System.Threading.Tasks。)任务调度器中find它。 这将导致这样的TaskScheduler调用FromCurrentSynchronizationContext

    默认的TaskScheduler会将调用排入ThreadPool 。 然而当你等待操作时,如果它没有在ThreadPool上运行,它将尝试从ThreadPool移除它并ThreadPool联方式运行(在等待中的同一个线程上…线程正在等待,所以它不是忙)。

    注意:一个值得注意的例外是标有LongRunningTaskLongRunning Task将在单独的线程上运行 。


你的问题

如果我有一个长期运行的任务是CPU绑定(假设它正在做很多的硬性math),那么asynchronous运行该任务必须阻止一些线程是正确的? 有些事情必须真正做math。 如果我等待它,那么一些线程被阻塞。

如果你正在做计算,他们必须发生在某个线程上,那部分是正确的。

然而, asyncawait的美妙之处在于,等待的线程不必被阻塞(稍后的更多)。 然而,通过让等候中的任务安排在等待的同一个线程上运行,从而导致同步执行(这在UI线程中是一个容易的错误),很容易让自己在脚下自由自在。

asyncawait的关键特征之一是他们从调用者那里获取SynchronizationContext 。 对于大多数使用默认TaskScheduler线程(如前所述,使用ThreasPool )。 但是,对于UI线程来说,这意味着将任务发布到消息队列中,这意味着它们将在UI线程上运行。 这样做的好处是您不必使用InvokeBeginInvoke来访问UI组件。

在我进入如何从UI线程await一个Task而不阻塞它之前,我想要说明的是,可以实现一个TaskScheduler ,如果你在一个Taskawait ,你不会阻塞你的线程或使它闲置,而是让你的线程select另一个正在等待执行的Task当我回溯.NET 2.0的任务时,我尝试了这一点。

什么是一个真正的asynchronous方法的例子,他们将如何实际工作? 那些仅限于I / O操作,它利用了一些硬件function,所以没有线程被阻塞?

您似乎混淆了asynchronous不阻塞线程 。 如果你想要的是一个.NET中的asynchronous操作的例子,不需要阻塞一个线程,一个方法来做到这一点,你可能会发现容易掌握的是使用continuations而不是await 。 对于需要在UI线程上运行的延续,可以使用TaskScheduler.FromCurrentSynchronizationContext

不要执行花式旋转等待 。 我的意思是使用TimerApplication.Idle或类似的东西。

当你使用async你要告诉编译器重写方法的代码,以允许破坏它。 结果类似于continuation,使用更方便的语法。 当线程到达awaitTask将被调度,并且在当前async调用(超出方法)之后,线程可以自由地继续。 当Task完成后,继续(在await )被安排。

对于UI线程来说,这意味着一旦await ,它可以继续处理消息。 一旦等待的Task完成,继续(在await )将被安排。 因此, await并不意味着阻止线程。

然而,盲目地joinasyncawait并不能解决你所有的问题。

我向你提交一个实验。 获取一个新的Windows窗体应用程序,放入一个Button和一个TextBox ,并添加下面的代码:

  private async void button1_Click(object sender, EventArgs e) { await WorkAsync(5000); textBox1.Text = @"DONE"; } private async Task WorkAsync(int milliseconds) { Thread.Sleep(milliseconds); } 

它阻止了用户界面。 会发生什么呢,就像前面提到的那样, await自动使用调用者线程的SynchronizationContext 。 在这种情况下,这是UI线程。 因此, WorkAsync将在UI线程上运行。

这是发生了什么事情:

  • UI线程获取点击消息并调用click事件处理程序
  • 在点击事件处理程序中,UI线程到达await WorkAsync(5000)
  • WorkAsync(5000) (并调度其延续)计划运行在当前的同步上下文,这是UI线程同步上下文…意味着它张贴消息执行它
  • UI线程现在可以自由处理更多的消息
  • UI线程select消息以执行WorkAsync(5000)并计划其延续
  • UI线程继续调用WorkAsync(5000)
  • WorkAsync ,UI线程运行Thread.Sleep 。 用户界面现在无响应5秒钟。
  • 继续计划运行其余的click事件处理程序,这是通过为UI线程发布另一个消息来完成的
  • UI线程现在可以自由处理更多的消息
  • UI线程select消息以在点击事件处理程序中继续
  • UI线程更新文本框

结果是同步执行,带有开销。

是的,你应该使用Task.Delay来代替。 那不是重点; 考虑Sleep一些计算。 关键是,只要使用asyncawait任何地方await都不会给你一个自动并行的应用程序。 select你想要在后台线程上运行的东西(例如在ThreadPool )以及你想在UI线程上运行的东西要好得多。

现在,请尝试下面的代码:

  private async void button1_Click(object sender, EventArgs e) { await Task.Run(() => Work(5000)); textBox1.Text = @"DONE"; } private void Work(int milliseconds) { Thread.Sleep(milliseconds); } 

你会发现,等待不会阻止用户界面。 这是因为在这种情况下, Thread.Sleep现在运行在ThreadPool感谢Task.Run 。 并且,由于button1_Clickasync ,一旦代码到达,UI线程可以继续工作。 Task完成之后,代码将在await之后恢复,这要感谢编译器重写方法,以便能够正确地执行此操作。

这是发生了什么事情:

  • UI线程获取点击消息并调用click事件处理程序
  • 在单击事件处理程序中,UI线程将await Task.Run(() => Work(5000))
  • Task.Run(() => Work(5000)) (并调度其延续)计划在当前的同步上下文上运行,这是UI线程同步上下文…意味着它发布消息来执行它
  • UI线程现在可以自由处理更多的消息
  • UI线程select消息来执行Task.Run(() => Work(5000))并在完成时安排它的继续
  • UI线程继续调用Task.Run(() => Work(5000)) ,这将运行在ThreadPool
  • UI线程现在可以自由处理更多的消息

ThreadPool完成时,继续将安排剩余的click事件处理程序运行,这是通过为UI线程发布另一个消息来完成的。 当UI线程select消息继续点击事件处理程序时,它将更新文本框。

斯蒂芬的回答已经很棒了,所以我不会重复他所说的话。 我已经在堆栈溢出(和其他地方)上多次重复相同的论点。

相反,让我关注asynchronous代码的一个重要的抽象事物:它不是绝对的限定符。 说一段代码是asynchronous的是毫无意义的 – 它总是与其他事物不同步。 这非常重要。

await的目的是在asynchronous操作和一些连接同步代码之上构build同步工作stream。 你的代码与代码本身完全同步1

 var a = await A(); await B(a); 

事件的sorting由await调用指定。 B使用A的返回值,这意味着A必须在B之前运行。包含此代码的方法具有同步工作stream,并且两种方法A和B彼此同步。

这非常有用,因为同步工作stream程通常更容易考虑,更重要的是,许多工作stream程都是同步的。 如果B需要A的运行结果,则必须在A 2之后运行。 如果您需要发出HTTP请求来获取另一个HTTP请求的URL,则必须等待第一个请求完成。 它与线程/任务调度无关。 也许我们可以称之为“固有同步性”,除了“偶然同步”之外,你可以强迫那些不需要命令的东西。

你说:

在我看来,因为我主要做UI开发,asynchronous代码是不在UI线程上运行的代码,而是在其他线程上运行。

您正在描述相对于UIasynchronous运行的代码。 这当然是一个非常有用的情况下asynchronous(人们不喜欢停止响应的UI)。 但是这只是一个更普遍的原则的具体例子 – 允许事情相互之间不按顺序发生。 同样,这不是一个绝对的 – 你想要一些事件发生不按顺序(例如,当用户拖动窗口或进度栏更改,窗口应该仍然重绘),而其他人不能发生不按顺序(处理button在“加载”操作完成之前不得点击)。 在这个用例中await与原则上使用Application.DoEvents没有什么不同 – 它引入了许多相同的问题和好处。

这也是原来的引用变得有趣的部分。 UI需要一个线程来更新。 该线程调用可能正在使用await的事件处理程序。 这是否意味着使用await的行将允许UI根据用户input进行更新? 没有。

首先,你需要明白, await使用它的论点,就像它是一个方法调用一样。 在我的示例中,必须在await生成的代码可以执行任何操作之前调用A ,包括“释放控制回到UI循环”。 A的返回值是Task<T>而不仅仅是T ,代表“将来可能的值” – 并await生成的代码检查,看看该值是否已经存在(在这种情况下,它只是继续在同一个线程)或不(这意味着我们得到释放线程回到UI循环)。 但是在任何情况下, Task<T>值本身都必须A返回。

考虑这个实现:

 public async Task<int> A() { Thread.Sleep(1000); return 42; } 

调用者需要A返回一个值(int的任务); 因为方法中没有await ,这意味着return 42; 。 但是在睡眠结束之前不会发生,因为这两个操作是相对于线程同步的。 调用者线程将被阻塞一秒,而不pipe是否使用await或者不使用 – 阻塞在A()本身中,而不是await theTaskResultOfA

相反,考虑这一点:

 public async Task<int> A() { await Task.Delay(1000); return 42; } 

一旦执行到了await ,它就会看到正在等待的任务尚未完成,并将控制权返回给调用者; 并且呼叫者的await因此将控制返回给呼叫者。 我们已经设法使一些代码在UI方面是asynchronous的。 UI线程和A之间的同步性是偶然的,我们删除它。

这里的重要部分是:没有检查代码,没有办法从外部区分这两个实现。 只有返回types是方法签名的一部分 – 它没有说方法会asynchronous执行,只是它可能会 。 这可能是出于许多原因,所以没有必要与之作斗争 – 例如,当结果已经可用时,没有必要中断执行的线程:

 var responseTask = GetAsync("http://www.google.com"); // Do some CPU intensive task ComputeAllTheFuzz(); response = await responseTask; 

我们需要做一些工作。 一些事件可以相对于其他事件asynchronous运行(在这种情况下, ComputeAllTheFuzz独立于HTTP请求)并且是asynchronous的。 但是在某些时候,我们需要回到一个同步工作stream(例如,需要ComputeAllTheFuzz和HTTP请求的结果)。 这是await点,它再次同步执行(如果你有多个asynchronous工作stream程,你会使用类似Task.WhenAll东西)。 但是,如果HTTP请求在计算之前设法完成,那么在await点释放控制权就毫无意义 – 我们可以简单地继续在同一个线程上。 CPU没有浪费 – 不会阻塞线程; 它确实有用的CPU工作。 但是我们没有给UI提供任何更新的机会。

这当然是为什么通常在更一般的asynchronous方法中避免这种模式的原因。 对于一些asynchronous代码的使用(避免浪费线程和CPU时间)是有用的,但是对其他(保持UI响应)不是很有用。 如果你期望这样的方法来保持UI的响应,你不会对结果感到满意。 但是,如果将它用作Web服务的一部分,那么它将会很好用 – 重点在于避免浪费线程,不保持UI的响应(这已经通过asynchronous调用服务端点提供 – 不会从中受益同样的事情再次在服务方面)。

简而言之, await允许你编写与调用者不同步的代码。 它不会调用asynchronous的魔力,它不是asynchronous的,它不阻止你使用CPU或阻塞线程。 它只是为您提供了轻松使asynchronous操作不会出现同步工作stream的工具,并且将整个工作stream的一部分呈现为与调用者相关的asynchronous。

让我们考虑一个UI事件处理程序。 如果单个asynchronous操作碰巧不需要一个线程来执行(例如asynchronousI / O),则asynchronous方法的一部分可能允许其他代码在原始线程上执行(并且UI在这些部分中保持响应)。 当操作再次需要CPU /线程时,可能需要或不需要原来的线程来继续工作。 If it does, the UI will be blocked again for the duration of the CPU work; if it doesn't (the awaiter specifies this using ConfigureAwait(false) ), the UI code will run in parallel. Assuming there's enough resources to handle both, of course. If you need the UI to stay responsive at all times, you cannot use the UI thread for any execution long enough to be noticeable – even if that means you have to wrap an unreliable "usually asynchronous, but sometimes blocks for a few seconds" async method in a Task.Run . There's costs and benefits to both approaches – it's a trade-off, as with all engineering 🙂


  1. Of course, perfect as far as the abstraction holds – every abstraction leaks, and there's plenty of leaks in await and other approaches to asynchronous execution.
  2. A sufficiently smart optimizer might allow some part of B to run, up to the point where the return value of A is actually needed; this is what your CPU does with normal "synchronous" code ( Out of order execution ). Such optimizations must preserve the appearance of synchronicity, though – if the CPU misjudges the ordering of operations, it must discard the results and present a correct ordering.

Here's asynchronous code which shows how async / await allows code to block and release control to another flow, then resume control but not needing a thread.

 public static async Task<string> Foo() { Console.WriteLine("In Foo"); await Task.Yield(); Console.WriteLine("I'm Back"); return "Foo"; } static void Main(string[] args) { var t = new Task(async () => { Console.WriteLine("Start"); var f = Foo(); Console.WriteLine("After Foo"); var r = await f; Console.WriteLine(r); }); t.RunSynchronously(); Console.ReadLine(); } 

在这里输入图像说明

So it's that releasing of control and resynching when you want results that's key with async/await ( which works well with threading )

NOTE: No Threads were blocked in the making of this code 🙂

I think sometimes the confusion might come from "Tasks" which doesn't mean something running on its own thread. It just means a thing to do, async / await allows tasks to be broken up into stages and coordinate those various stages into a flow.

It's kind of like cooking, you follow the recipe. You need to do all the prep work before assembling the dish for cooking. So you turn on the oven, start cutting things, grating things, etc. Then you await the temp of oven and await the prep work. You could do it by yourself swapping between tasks in a way that seems logical (tasks / async / await), but you can get someone else to help grate cheese while you chop carrots (threads) to get things done faster.