.net-有关线程以及C#中异步方法是否真正异步的困惑

我正在阅读async / await,当FooAsync()可能有用并且遇到这篇文章时。 我对该帖子的以下内容有疑问:

当您使用async / await时,无法保证您使用的方法   等待时调用FooAsync()实际上将异步运行。   内部实现可以自由使用完整返回   同步路径。

这对我来说还不太清楚,可能是因为我脑海中对异步的定义不统一。

在我看来,由于我主要从事UI开发,所以异步代码是不在UI线程上运行但在其他线程上运行的代码。 我想在我引用的文本中,如果某个方法在任何线程上阻塞(例如,即使是线程池线程),它也不是真正异步的。

题:

如果我有一个长时间运行的任务,该任务受CPU限制(比如说它在做大量的硬数学运算),那么异步运行该任务必须阻塞某些线程,对吗? 实际需要做一些数学运算。 如果我等待它,那么某些线程将被阻塞。

真正的异步方法的一个例子是什么,它们如何实际工作? 是否仅限于利用某些硬件功能的I / O操作,从而不会阻塞线程?

Flack asked 2020-02-13T10:44:18Z
7个解决方案
109 votes

这对我来说还不太清楚,可能是因为我脑海中对异步的定义不统一。

很好,要求您澄清。

在我看来,由于我主要从事UI开发,所以异步代码是不在UI线程上运行但在其他线程上运行的代码。

这种信念是普遍的,但却是错误的。 不需要异步代码在任何其他线程上运行。

想象一下您正在做早餐。 您在烤面包机中放了一些烤面包,而在等待烤面包出炉的同时,您从昨天开始查看邮件,支付一些账单,然后,突然出现了烤面包。 您完成了帐单的支付,然后在面包上涂黄油。

您在哪里雇了第二个工人看烤面包机?

你没有 线程是工人。 异步工作流可以全部发生在一个线程上。 异步工作流的要点是,如果可以避免,则避免雇用更多的工人。

如果我有一个长时间运行的任务,该任务受CPU限制(比如说它在做大量的硬数学运算),那么异步运行该任务必须阻塞某些线程,对吗? 实际需要做一些数学运算。

在这里,我将为您解决一个难题。 这是一列100个数字; 请手动将它们加起来。 因此,您将第一个添加到第二个并进行总计。 然后,将运行总计添加到第三个并获得总计。 然后,哦,地狱,第二页的数字丢失了。 记住你在哪里,去做些烤面包。 哦,在敬酒的同时,还有一封剩余的字母到了。 给烤面包涂黄油后,继续将这些数字相加,并记得下次有空的时候吃烤面包。

您雇用其他工人来添加数字的位置在哪里? 计算上昂贵的工作不需要同步,也不需要阻塞线程。 使计算工作可能异步的原因是能够停止计算,记住您在哪里,去做其他事情,记住在那之后该做什么以及在您离开的地方继续工作。

现在肯定可以雇用第二名工人,他除了加数字外什么也不做,然后被解雇。 你可以问那个工人“你做完了吗?” 如果答案是否定的,您可以做一个三明治直到完成。 这样您和工人都很忙。 但是并不需要异步涉及多个工作人员。

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

不不不。 这是您误会中最重要的部分。 await并不意味着“异步开始执行此作业”。 await的意思是“我这里异步生成的结果可能不可用。如果不可用,请在该线程上找到其他工作,以便我们不会阻塞该线程。等待与您所说的相反。

真正的异步方法的一个例子是什么,它们如何实际工作? 是否仅限于利用某些硬件功能的I / O操作,从而不会阻塞线程?

异步工作通常涉及自定义硬件或多个线程,但不是必需的。

不要考虑工人。 考虑工作流程。 异步的本质是将工作流分解成几个小部分,以便您可以确定这些部分必须发生的顺序,然后依次执行每个部分,但允许相互不依赖的部分进行交错。

在异步工作流程中,您可以轻松地检测工作流程中表示零件之间相关性的位置。 这些部分用await标记。这就是await的含义:后面的代码取决于工作流的这一部分是否完成,因此,如果未完成,请去找其他任务去做,然后在任务完成时再回到这里 完成了。 关键是要保证工人的工作,即使是在将来会产生所需结果的世界中。

Eric Lippert answered 2020-02-13T10:45:43Z
26 votes

我正在异步/等待中阅读

我可以推荐我的Task.Delay简介吗?

当Task.Yield可能有用时

几乎从不。 在进行单元测试时,我偶尔发现它很有用。

在我看来,由于我主要从事UI开发,所以异步代码是不在UI线程上运行但在其他线程上运行的代码。

异步代码可以是无线程的。

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

我会说是正确的。 我将术语“真正异步”用于不阻塞任何线程(并且不同步)的操作。 对于那些看起来异步但只能以这种方式运行的操作,因为它们在线程池线程上运行或阻塞,所以我也使用术语“ fake async”。

如果我有一个长时间运行的任务,该任务受CPU限制(比如说它在做大量的硬数学运算),那么异步运行该任务必须阻塞某些线程,对吗? 实际需要做一些数学运算。

是; 在这种情况下,您需要使用同步API定义该工作(因为它是同步工作),然后可以使用Task.Delay从您的UI线程调用它,例如:

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

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

没有; 线程池线程将用于运行代码(实际上并未被阻止),UI线程异步地等待该代码完成(也未被阻止)。

真正的异步方法的一个例子是什么,它们如何实际工作?

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

是否仅限于利用某些硬件功能的I / O操作,从而不会阻塞线程?

尽管I / O操作是简单的示例,但并不完全。 另一个相当简单的示例是计时器(例如Task.Delay)。 尽管您可以围绕任何类型的“事件”构建真正的异步API。

Stephen Cleary answered 2020-02-13T10:47:12Z
10 votes

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

对我来说这有点不清楚,可能是因为   异步在我的脑海里没有排队。

这仅表示调用异步方法时有两种情况。

首先是,在将任务退还给您时,该操作已经完成-这将是一条同步路径。 第二个是操作仍在进行中-这是异步路径。

考虑下面的代码,它应该显示这两个路径。 如果密钥在缓存中,则将其同步返回。 否则,将启动异步操作并调出数据库:

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;
    }
}

因此,通过了解这一点,您可以推断出,理论上,await的调用可能具有类似的代码,并且其本身可以同步返回:因此,即使您的异步路径也可能最终以100%同步运行。 但是您的代码不需要关心:async / await可以无缝处理这两种情况。

如果我有一个长时间运行的任务,该任务受CPU限制(比如说它在做大量的硬数学运算),那么异步运行该任务必须阻塞某些线程,对吗? 实际需要做一些数学运算。 如果我等待它,那么某些线程将被阻塞。

阻塞是一个定义明确的术语-意味着您的线程在等待某些东西(I / O,互斥锁等)时产生了其执行窗口。 因此,您进行数学运算的线程不被认为是阻塞的:它实际上是在执行工作。

真正的异步方法的一个例子是什么,它们如何实际工作? 是否仅限于利用某些硬件功能的I / O操作,从而不会阻塞线程?

一种“真正异步的方法”将是永远不会阻塞的方法。 它通常最终会涉及到I / O,但是如果您想在当前线程中使用其他功能(例如在UI开发中)或尝试引入并行性时,也可能意味着需要await添加大量的数学代码:

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;
}
Cory Nelson answered 2020-02-13T10:48:16Z
6 votes

这个主题相当广泛,可能会进行一些讨论。 但是,在C#中使用Task.Runawait被认为是异步编程。 但是,异步如何工作是完全不同的讨论。 在.NET 4.5之前,没有async和await关键字,并且开发人员必须直接针对Task Parallel Library(TPL)进行开发。 在那里,开发人员可以完全控制何时以及如何创建新任务甚至线程。 但是,这样做有一个缺点,因为它并不是真正的专家,由于线程之间的竞争状况,应用程序可能会遇到严重的性能问题和错误。

从.NET 4.5开始,引入了async和await关键字,并提供了一种异步编程的新方法。 async和await关键字不会导致创建其他线程。 异步方法不需要多线程,因为异步方法不会在自己的线程上运行。 该方法在当前同步上下文上运行,并且仅在该方法处于活动状态时才在线程上使用时间。 您可以使用Task.Run将受CPU约束的工作移至后台线程,但是后台线程对仅等待结果可用的进程没有帮助。

在几乎每种情况下,基于异步的异步编程方法都优于现有方法。 特别是,此方法比IO绑定操作的BackgroundWorker更好,因为代码更简单,而且您不必防范竞争条件。 您可以在此处阅读有关此主题的更多信息。

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

Dan answered 2020-02-13T10:48:53Z
6 votes

异步并不意味着并行

异步仅表示并发。 实际上,即使使用显式线程也不能保证它们将同时执行(例如,当线程与同一个单核有亲缘关系时,或更常见的是,当机器中只有一个核开始时)。

因此,您不应期望异步操作同时发生在其他事物上。 异步仅表示它会最终在另一个时间发生(a(希腊)=不,同步(希腊)=在一起,khronos(希腊)=时间。=>异步=不在同一时间发生)。

注意:异步性的想法是,在调用时,您实际上并不关心代码何时真正运行。 这允许系统在可能的情况下利用并行性来执行操作。 它甚至可能立即运行。 它甚至可能发生在同一线程上……稍后再讨论。

当您执行ThreadPool异步操作时,您正在创建并发(com(拉丁)=一起,currere(拉丁)=运行。=>“并发” =一起运行)。 这是因为您要在继续操作之前要求异步操作完成。 可以说执行收敛。 这类似于连接线程的概念。


当异步不能并行时

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

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

  1. 可以在返回ThreadPool的任何对象上使用ThreadPool。收到async时,它可能已经完成。

    但是,这并不意味着它会同步运行。 实际上,它建议它异步运行并在获得ThreadPool实例之前完成。

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

    ThreadPool

    另外,如果ThreadPool方法没有ThreadPool,它将同步运行。

    注意:如您所知,返回ThreadPool的方法可能正在网络或文件系统等上等待…这样做并不意味着启动新ThreadPool或在async上排队。

  2. 在由单个线程处理的同步上下文下,结果将是同步执行ThreadPool,但会有一些开销。 UI线程就是这种情况,下面我将详细讨论。

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

    注意:最近,我编写了一个自定义ThreadPool,它在单个线程上运行任务。 您可以在创建(System.Threading.Tasks。)任务计划程序中找到它。 这将导致这样的ThreadPool,并致电ThreadPool

    默认值ThreadPool将使调用排队到ThreadPool。但是,当您等待该操作时,如果尚未在ThreadPool上运行,它将尝试从async中删除它,并以内联方式运行(在等待的同一线程上。) 线程仍在等待,因此它并不忙)。

    注意:一个明显的例外是带有标记ThreadPoolThreadPoolThreadPool ThreadPools将在单独的线程上运行。


你的问题

如果我有一个长时间运行的任务,该任务受CPU限制(比如说它在做大量的硬数学运算),那么异步运行该任务必须阻塞某些线程,对吗? 实际需要做一些数学运算。 如果我等待它,那么某些线程将被阻塞。

如果您正在执行计算,那么它们必须发生在某个线程上,那部分是正确的。

然而,中ThreadPoolThreadPool的美妙之处在于等待的线程没有被(后面有更多介绍)阻止。然而,这是很容易通过其计划正在等待,导致同步执行(这是在UI线程一个容易犯的错误)在同一线程上运行的任务等待拍摄自己的脚。

其中的ThreadPoolThreadPool的主要特点是,他们采取ThreadPool从调用者。 对于大多数线程,将导致使用默认值async(如前所述,使用await)。然而,对于UI线程这意味着发布任务到消息队列,这意味着他们将在UI线程上运行。 这样做的好处是您不必使用ThreadPoolThreadPool来访问UI组件。

在继续讨论如何通过UI线程ThreadPoolThreadPool而不阻塞它之前,我想指出的是,可以实现async,如果您在await上使用await,则您不会阻塞线程或使其处于空闲状态,相反,你让你的线程选择另一个await正在等待执行。当我向后移植任务的.NET2.0我尝试与此有关。

真正的异步方法的一个例子是什么,它们如何实际工作? 是否仅限于利用某些硬件功能的I / O操作,从而不会阻塞线程?

您似乎将异步与不阻塞线程混淆了。 如果您想要的是.NET中不需要阻塞线程的异步操作的示例,那么您可能会发现容易理解的一种方法是使用延续而不是ThreadPool。对于需要运行的延续, 在UI线程上,您可以使用ThreadPool

不要实施奇特的旋转等待。 我的意思是使用ThreadPoolThreadPool或类似的东西。

当您使用ThreadPool时,您是在告诉编译器以允许破坏该方法的方式重写该方法的代码。 结果类似于延续,语法更方便。 当线程到达ThreadPool时,将调度async,并且在当前await调用之后(该方法之外),线程可以自由继续。 完成Task后,将安排继续(在await之后)。

对于UI线程,这意味着一旦达到ThreadPool,就可以继续处理消息。 一旦完成了等待的ThreadPool,将安排继续(在async之后)。 因此,达到await并不意味着阻塞线程。

但是,盲目添加ThreadPoolThreadPool无法解决您的所有问题。

我向您提交了一个实验。 获取一个新的Windows Forms应用程序,放入ThreadPoolThreadPool,并添加以下代码:

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

    private async Task WorkAsync(int milliseconds)
    {
        Thread.Sleep(milliseconds);
    }

它阻止了UI。 正如前面提到的,发生的情况是ThreadPool自动使用了调用者线程的ThreadPool。 在这种情况下,这就是UI线程。 因此,async将在UI线程上运行。

这是发生了什么:

  • UI线程获取点击消息并调用点击事件处理程序
  • 在click事件处理程序中,UI线程达到ThreadPool
  • ThreadPool(并计划其继续)计划在当前同步上下文(即UI线程同步上下文)上运行……这意味着它将发布一条消息来执行该同步上下文
  • UI线程现在可以自由处理其他消息
  • UI线程选择消息以执行ThreadPool并安排其继续
  • UI线程继续调用ThreadPool
  • ThreadPool中,UI线程运行ThreadPool。UI现在无响应5秒钟。
  • 继续安排其余的click事件处理程序运行,这是通过为UI线程发布另一条消息来完成的
  • UI线程现在可以自由处理其他消息
  • UI线程选择消息以在click事件处理程序中继续
  • UI线程更新文本框

结果是同步执行,而产生开销。

是的,您应该改用ThreadPool。 那不是重点; 考虑将ThreadPool作为计算的代表。 关键是,仅在任何地方使用ThreadPoolasync都不会为您提供自动并行的应用程序。 最好选择要在后台线程上运行的内容(例如在await上)以及要在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);
    }

您会发现await不会阻止UI。 这是因为在这种情况下,由于ThreadPool,现在ThreadPool正在ThreadPool上运行。并且由于ThreadPoolasync,所以一旦代码到达await,UI线程就可以自由地继续工作。 在Task完成之后,由于编译器重写了该方法以精确地实现此目的,因此在await之后代码将恢复。

这是发生了什么:

  • UI线程获取点击消息并调用点击事件处理程序
  • 在click事件处理程序中,UI线程达到ThreadPool
  • ThreadPool(并计划其继续)计划在当前同步上下文(即UI线程同步上下文)上运行……这意味着它将发布一条消息来执行该同步上下文
  • UI线程现在可以自由处理其他消息
  • UI线程选择消息以执行ThreadPool,并在完成后安排其继续
  • UI线程继续调用ThreadPool,它将在ThreadPool上运行
  • UI线程现在可以自由处理其他消息

ThreadPool完成后,继续操作将安排其余的click事件处理程序运行,这是通过为UI线程发布另一条消息来完成的。 当UI线程在click事件处理程序中选择要继续的消息时,它将更新文本框。

Theraot answered 2020-02-13T10:53:38Z
4 votes

斯蒂芬的答案已经很不错了,所以我不再重复他的话。 我已经做了相当多的事情,在Stack Overflow(以及其他地方)上多次重复了相同的论点。

相反,让我集中讨论异步代码的一个重要的抽象问题:它不是绝对限定符。 说一段代码是异步的没有意义-它相对于其他代码总是异步的。 这很重要。

ConfigureAwait(false)的目的是在异步操作和一些连接的同步代码的基础上构建同步工作流。 您的代码看起来与代码本身完全同步。

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

事件的顺序由ConfigureAwait(false)调用指定。 B使用A的返回值,这意味着A必须在B之前运行。包含此代码的方法具有同步工作流,并且A和B这两个方法彼此同步。

这非常有用,因为同步工作流通常更容易考虑,更重要的是,许多工作流只是同步的。 如果B需要A的结果才能运行,则它必须在A2之后运行。 如果您需要发出HTTP请求以获取另一个HTTP请求的URL,则必须等待第一个请求完成;否则,请执行以下操作。 它与线程/任务调度无关。 也许我们可以称其为“固有的同步性”,而不是“偶然的同步性”,在这种情况下,您可以对不需要订购的东西强制订购。

你说:

在我看来,由于我主要从事UI开发,所以异步代码是不在UI线程上运行但在其他线程上运行的代码。

您正在描述相对于UI异步运行的代码。 对于异步而言,这无疑是非常有用的情况(人们不喜欢UI会停止响应)。 但这只是更普遍的原则的一个特例-允许事情彼此发生混乱。 同样,这也不是绝对的-您希望某些事件发生混乱(例如,当用户拖动窗口或进度条发生变化时,该窗口仍应重新绘制),而其他事件一定不能发生混乱(“处理”按钮) (在加载操作完成之前,请勿单击)。 此用例中的ConfigureAwait(false)与使用Task.Run原则上没有什么不同-它引入了许多相同的问题和好处。

这也是原始报价有趣的部分。 UI需要一个线程进行更新。 该线程调用事件处理程序,该事件处理程序可能正在使用ConfigureAwait(false)。这是否意味着使用Task.Run的行将允许UI响应用户输入进行更新? 没有。

首先,您需要了解ConfigureAwait(false)使用它的参数,就像它是一个方法调用一样。 在我的示例中,必须已调用Task.Run,然后才能通过await生成的代码执行任何操作,包括“将控制释放回UI循环”。 Task.WhenAll的返回值是await,而不是await theTaskResultOfA,代表“将来可能的值”-await生成的代码检查该值是否已经存在(在这种情况下,它仅在同一线程上继续) (这意味着我们可以将线程释放回UI循环)。 但是,无论哪种情况,必须从A返回Task<T>值本身。

考虑以下实现:

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

  return 42;
}

调用方需要ConfigureAwait(false)返回值(int任务); 由于该方法中没有Task.Runs,因此表示await。但这不能在睡眠完成之前发生,因为这两个操作相对于线程是同步的。 无论是否使用Task.WhenAll,调用者线程都会被阻塞一秒钟-阻塞发生在await本身,而不是await theTaskResultOfA中。

相反,请考虑以下情况:

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

  return 42;
}

一旦执行到ConfigureAwait(false),它就会看到等待的任务尚未完成,并把控制权返回给它的调用者。 呼叫者中的Task.Run就会将控制权返回给其呼叫者。 我们已经设法使某些代码相对于UI异步。 UI线程和A之间的同步是偶然的,因此我们将其删除。

这里的重要部分是:如果没有检查代码,就无法从外部区分这两种实现。 只有返回类型是方法签名的一部分-并不是说方法将异步执行,只是可以执行。 这可能是出于许多充分的原因,因此没有必要进行斗争-例如,当结果已经可用时,破坏执行线程是没有意义的:

var responseTask = GetAsync("http://www.google.com");

// Do some CPU intensive task
ComputeAllTheFuzz();

response = await responseTask;

我们需要做一些工作。 某些事件可以相对于其他事件异步运行(在这种情况下,ConfigureAwait(false)独立于HTTP请求)并且是异步的。 但是到了某个时候,我们需要回到同步工作流(例如,需要Task.Run和HTTP请求两者的结果)。 那就是await点,它再次同步执行(如果您有多个异步工作流,则可以使用Task.WhenAll)。 但是,如果HTTP请求在计算之前成功完成,则在await点释放控制没有意义-我们可以在同一线程上继续进行操作。 不会浪费CPU,也不会阻塞线程。 它可以完成有用的CPU工作。 但是,我们没有提供任何机会来更新UI。

当然,这就是为什么在更通用的异步方法中通常避免使用此模式的原因。 它对于异步代码的某些用法(避免浪费线程和CPU时间)很有用,而对于其他用途(使UI保持响应)则非常有用。 如果您期望这样的方法可以使UI保持响应,那么您将不会对结果感到满意。 但是,例如,如果将它用作Web服务的一部分,它将很好用-重点是避免浪费线程,不保持UI响应(这已经通过异步调用服务端点来提供-这样做没有任何好处。 在服务端也是如此)。

简而言之,ConfigureAwait(false)允许您编写与其调用方异步的代码。 它不会调用神奇的异步功能,它在所有方面都不是异步的,不会阻止您使用CPU或阻塞线程。 它只是为您提供了一些工具,可以轻松地从异步操作中创建出一个同步工作流,并将整个工作流的一部分呈现为相对于其调用方而言是异步的。

让我们考虑一个UI事件处理程序。如果各个异步操作恰好不需要执行线程(例如,异步I / O),则异步方法的一部分可以允许其他代码在原始线程上执行(并且UI在那些部分中保持响应)。当操作再次需要CPU /线程时,它可能需要也可能不需要原始线程才能继续工作。如果是这样,UI将在CPU工作期间再次被阻塞;如果没有(等待者使用ConfigureAwait(false)进行指定),则UI代码将并行运行。当然,假设有足够的资源来处理这两种情况。如果您需要UI始终保持响应状态,则不能将UI线程用于足够长的时间以至于无法引起注意-即便这意味着您必须包装不可靠的“通常是异步的,但有时会阻塞几秒钟”异步Task.Run中的方法。两种方法都有成本和收益-就像所有工程设计一样,这是一个折衷方案:)


  1. 当然,就抽象而言,它是完美的-每个抽象都会泄漏,并且在等待和其他异步执行方法中存在大量泄漏。
  2. 一个足够聪明的优化器可能会允许B的某些部分运行,直到实际需要A的返回值为止。 这就是您的CPU使用普通的“同步”代码(乱序执行)的方式。 但是,此类优化必须保留同步的外观-如果CPU错误判断了操作的顺序,则必须丢弃结果并给出正确的顺序。
Luaan answered 2020-02-13T10:55:42Z
3 votes

这是异步代码,它显示了async / await如何允许代码阻止并将控制权释放到另一个流,然后恢复控制,但不需要线程。

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();
}

enter image description here

因此,当您需要结果时,就是释放控制权和重新同步,这对于async / await至关重要(与线程配合使用非常好)

注意:在编写此代码时没有线程被阻塞:)

我认为有时混淆可能来自“任务”,这并不意味着某些事情在其自己的线程上运行。 这只是意味着要做的事情,异步/等待允许将任务分解为多个阶段,并将各个阶段协调为一个流程。

有点像做饭,您遵循食谱。 您需要先完成所有准备工作,然后才能组装碗碟进行烹饪。 因此,您打开烤箱,开始切割,磨碎东西等。然后等待烤箱的温度并等待准备工作。 您可以自己通过看起来似乎合乎逻辑的方式(任务/异步/等待)在任务之间交换来做到这一点,但是当您切碎胡萝卜(线程)以更快地完成任务时,您可以让其他人帮助磨碎奶酪。

Keith Nicholas answered 2020-02-13T10:56:24Z
translate from https://stackoverflow.com:/questions/43989344/confusion-regarding-threads-and-if-asynchronous-methods-are-truly-asynchronous-i