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

时间:2017-05-15 21:51:03

标签: c# .net asynchronous

我正在阅读async / await,当Task.Yield可能有用并遇到this post.时,我对该帖子的下方有疑问:

  

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

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

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

问题:

如果我有一个长时间运行的CPU绑定任务(假设它正在进行大量的硬数学运算),那么异步运行该任务必须阻塞一些线程吗?有些东西实际上要做数学。如果我等待它,那么一些线程就会被阻止。

什么是真正异步方法的示例,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I / O操作,因此没有线程被阻止?

7 个答案:

答案 0 :(得分:109)

  

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

要求澄清你好。

  

在我看来,由于我主要使用UI开发,因此异步代码是不在UI线程上运行的代码,而是在其他一些线程上。

这种看法很常见,但却是错误的。不要求异步代码在任何第二个线程上运行。

想象一下,你正在做早餐。你在烤面包机里放了一些烤面包,当你在等待吐司的时候,你从昨天开始收到你的邮件,支付一些账单,嘿,吐司就出现了。你完成支付账单然后去烤面包。

你在哪里雇用第二个工人来看烤面包机?

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

  

如果我有一个长时间运行的CPU绑定任务(假设它正在进行大量的硬数学运算),那么异步运行该任务必须阻塞一些线程吗?实际上必须要做一些事情。

在这里,我会给你一个难以解决的问题。这是一列100个数字;请手动添加。因此,您将第一个添加到第二个并进行总计。然后将运行总计添加到第三个并获得总计。然后,哦,地狱,第二页的数字丢失了。记住你在哪里,然后去做一些吐司。哦,当烤面包敬酒时,一封信到了剩下的号码。当你完成涂抹吐司的时候,继续加上这些数字,记得下次有空的时候吃吐司。

您雇佣其他员工添加号码的部分在哪里? 计算成本高昂的工作不需要同步,也不需要阻止线程。使计算工作可能异步的事情是能够阻止它,记住你的位置,去做其他的事情,记住在之后做什么,然后从你离开的地方继续。

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

  

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

不,不,不。这是你误解中最重要的部分。 await并不意味着“以异步方式开始这项工作”。 await表示“我在这里有异步生成的结果可能不可用。如果它不可用,在此线程上找到其他工作,这样我们就不会阻止线程。等待是您刚才所说的相反

  

什么是真正异步方法的示例,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I / O操作,因此没有线程被阻止?

异步工作通常涉及自定义硬件或多线程,但它不需要。

不要考虑工人。想想工作流程。异步的本质是将工作流分解为小部分,这样可以确定这些部分必须发生的顺序,然后依次执行每个部分< / em>,但允许彼此没有依赖关系的部分进行交错

在异步工作流程中,您可以轻松地检测工作流中位置表示部件之间的依赖关系。这些部分标有await。这就是await的含义:后面的代码取决于完成的工作流的这一部分,所以如果它没有完成,去找一些其他任务要做,并在完成任务后再回到这里。重点是让工人保持工作,即使在将来需要产生结果的世界里。

答案 1 :(得分:26)

  

我正在阅读async / await

我可以推荐我的async intro吗?

  

当Task.Yield可能有用时

几乎没有。我发现它在进行单元测试时偶尔会有用。

  

在我看来,由于我主要使用UI开发,因此异步代码是不在UI线程上运行的代码,而是在其他一些线程上运行。

Asynchronous code can be threadless

  

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

我会说那是对的。我对不阻塞任何线程(并且不同步)的操作使用术语“真正的异步”。我还对出现异步的操作使用术语“假异步”,但只能以这种方式工作,因为它们运行或阻塞线程池线程。

  

如果我有一个长时间运行的CPU绑定任务(假设它正在进行大量的硬数学运算),那么异步运行该任务必须阻塞一些线程吗?实际上必须要做一些事情。

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

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

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

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

  

真正异步方法的示例是什么?它们实际上如何工作?

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

  

这些是否仅限于利用某些硬件功能的I / O操作,因此没有线程被阻止?

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

答案 2 :(得分:10)

  
    

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

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

  

如果我有一个长时间运行的任务是CPU绑定的(让我们说它做了很多硬数学运算),那么异步运行该任务必须阻止一些线程吗?有些东西实际上要做数学。如果我等待它,那么一些线程就会被阻止。

阻塞是一个明确定义的术语 - 这意味着你的线程在等待某些东西(I / O,互斥体等)时已经产生了它的执行窗口。所以你的数学线程不被认为是阻塞的:它实际上正在执行工作。

  

什么是真正异步方法的示例,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I / O操作,因此没有线程被阻止?

A&#34;真正的异步方法&#34;将是一个永远不会阻止的人。它通常最终涉及I / O,但它也可能意味着await当你想要当前的线程用于其他东西(如在UI开发中)或当你试图引入时,你的重数学代码并行:

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

答案 3 :(得分:6)

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

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

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

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

答案 4 :(得分:6)

异步并不意味着并行

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

因此,您不应期望异步操作与其他操作同时发生。异步只意味着它会发生,最终在另一个时间 a (希腊)=没有, syn (希腊)=在一起, khronos (希腊语)=时间。=&gt; 异步 =不同时发生。)

注意:异步性的想法是,在调用时,您并不关心代码实际运行的时间。如果可能,这允许系统利用并行性来执行操作。它甚至可能立即运行。它甚至可能发生在同一个线程上...稍后会发生。

当您await异步操作时,您正在创建并发 com (拉丁语)=在一起, currere (拉丁语) )= run。=&gt;&#34;并发&#34; = 一起运行)。这是因为您要求异步操作在继续之前完成。我们可以说执行收敛了。这类似于加入线程的概念。

异步时不能并行

  

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

这可以通过三种方式实现:

  1. 可以对返回await的任何内容使用Task。当您收到Task时,它可能已经完成。

    然而,仅此一点并不意味着它同步运行。事实上,它建议它在异步运行并在获得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的方法可能正在网络上或文件系统上等待......这样做并不意味着开始新的Thread或在ThreadPool

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

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

    注意:最近我编写了一个在单个线程上运行任务的自定义SyncrhonizationContext。您可以在Creating a (System.Threading.Tasks.)Task scheduler找到它。这会导致此TaskScheduler调用FromCurrentSynchronizationContext

    默认TaskScheduler会将调用排入ThreadPool。然而,当你等待操作时,如果它没有在ThreadPool上运行,它将尝试从ThreadPool中删除它并在内联运行它(在等待的同一线程上...线程无论如何都在等,所以它不忙。)

    注意:一个值得注意的例外是标有LongRunningTaskLongRunning Tasks will run on a separate thread

  5. 您的问题

      

    如果我有一个长时间运行的任务是CPU绑定的(让我们说它做了很多硬数学运算),那么异步运行该任务必须阻止一些线程吗?有些东西实际上要做数学。如果我等待它,那么一些线程就会被阻止。

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

    然而,asyncawait的美妙之处在于等待线程不必被阻止(稍后会更多)。然而,通过将等待的任务安排在正在等待的同一线程上运行,很容易让自己陷入困境,从而导致同步执行(这在UI线程中是一个容易出错的错误)。

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

    在我从UI线程进入如何await Task而不阻止它之前,我想要注意,如果你{{1}可以实现TaskSchedulerawait上,你不会阻塞你的线程或让它空闲,而是让你的线程选择另一个等待执行的Task当我backporting Tasks for .NET 2.0时,我尝试了这个。

      

    什么是真正异步方法的示例,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I / O操作,因此没有线程被阻止?

    您似乎将异步无法阻塞线程混淆。如果您想要的是.NET中不需要阻塞线程的异步操作示例,那么您可能会发现易于掌握的方法是使用continuations而不是Task。对于需要在UI线程上运行的延续,您可以使用TaskScheduler.FromCurrentSynchronizationContext

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

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

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

    然而,盲目地添加awaitasync并不会解决您的所有问题。

    我向你提交了一个实验。获取新的Windows窗体应用程序,放入ButtonTextBox,然后添加以下代码:

    await

    它会阻止用户界面。如前所述, 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。在这种情况下,这是UI线程。因此,SynchronizationContext将在UI线程上运行。

    这就是:

    • UI线程获取单击消息并调用click事件处理程序
    • 在click事件处理程序中,UI线程到达WorkAsync
    • await WorkAsync(5000)(以及安排其继续)计划在当前同步上下文上运行,这是UI线程同步上下文...意味着它发布了一条消息来执行它
    • UI线程现在可以自由处理更多消息
    • UI线程选择要执行WorkAsync(5000)的消息并安排其继续
    • UI线程使用continuation
    • 调用WorkAsync(5000)
    • WorkAsync(5000)中,UI线程运行WorkAsync。用户界面现在无法响应5秒钟。
    • 继续计划要运行的其余click事件处理程序,这是通过为UI线程发布另一条消息来完成的。
    • UI线程现在可以自由处理更多消息
    • UI线程选择要在Click事件处理程序中继续的消息
    • UI线程更新文本框

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

    是的,您应该使用Task.Delay代替。这不是重点;考虑Thread.Sleep进行一些计算。关键是,只需使用Sleepasync,无法为您提供自动并行的应用程序。选择你想在后台线程上运行什么(例如在await上)以及你想在UI线程上运行什么要好得多。

    现在,请尝试以下代码:

    ThreadPool

    您会发现await不会阻止UI。这是因为在这种情况下Thread.Sleep现在正在 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); } 上运行,感谢Task.Run。感谢ThreadPoolbutton1_Click,一旦代码到达async,UI线程就可以继续工作了。完成await之后,代码将在Task之后恢复,这要归功于编译器重写方法以准确允许。

    这就是:

    • UI线程获取单击消息并调用click事件处理程序
    • 在click事件处理程序中,UI线程到达await
    • await Task.Run(() => Work(5000))(以及安排其继续)计划在当前同步上下文上运行,这是UI线程同步上下文...意味着它发布了一条消息来执行它
    • UI线程现在可以自由处理更多消息
    • UI线程选择要执行Task.Run(() => Work(5000))的消息并在完成后安排其继续
    • UI线程会继续调用Task.Run(() => Work(5000)),这将在Task.Run(() => Work(5000))上运行
    • UI线程现在可以自由处理更多消息

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

答案 5 :(得分:4)

斯蒂芬的答案已经很好了,所以我不会重复他所说的话;我已经在Stack Overflow(和其他地方)多次重复相同的论点做了我公平的分享。

相反,让我关注一个关于异步代码的重要抽象事物:它不是绝对限定符。没有必要说一段代码是异步的 - 它总是与其他东西异步。这非常重要。

await的目的是在异步操作和一些连接同步代码之上构建同步工作流。您的代码 与代码本身完全同步 1

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

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

这非常有用,因为同步工作流通常更容易思考,更重要的是,许多工作流只是 同步。如果B需要运行A的结果,则必须在A 2 之后运行。如果您需要发出HTTP请求以获取另一个HTTP请求的URL,则必须等待第一个请求完成;它与线程/任务调度无关。也许我们可以称之为&#34;固有的同步性,除了&#34;意外的同步性&#34;在那里强制订购不需要订购的东西。

你说:

  

在我看来,由于我主要使用UI开发,因此异步代码是不在UI线程上运行的代码,而是在其他一些线程上运行。

您正在描述与UI异步运行的代码。对于异步来说,这当然是一个非常有用的案例(人们不喜欢停止响应的UI)。但它只是一个更一般原则的特定情况 - 允许事情相互之间发生故障。同样,它不是绝对的 - 您希望某些事件无序发生(例如,当用户拖动窗口或进度条发生变化时,窗口仍应重绘),同时其他人必须不按顺序发生(在加载操作完成之前不得单击“处理”按钮)。在这个用例中,await 与原则上使用Application.DoEvents不同 - 它引入了许多相同的问题和好处。

这也是原始报价变得有趣的部分。 UI需要更新线程。该线程调用事件处理程序,该处理程序可能正在使用await。是否意味着使用await的行将允许UI响应用户输入自行更新?否。

首先,您需要了解await使用其参数,就像它是方法调用一样。在我的示例中,在A生成的代码可以执行任何操作之前,必须已调用await,包括&#34;将控制权释放回UI循环&#34;。 A的返回值为Task<T>,而不仅仅是T,代表未来可能的值&#34; - 和await - 生成的代码检查以查看值是否已存在(在这种情况下它只是在同一个线程上继续)或不是(这意味着我们将线程释放回UI循环)。但在任何一种情况下,都必须从Task<T>返回A值本身

考虑这个实现:

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

  return 42;
}

调用者需要A来返回一个值(int的任务);由于方法中没有await s,这意味着return 42;。但是在睡眠结束之前不会发生这种情况,因为这两个操作与线程是同步的。调用者线程将被阻止一秒钟,无论它是否使用await - 阻止位于A()本身,而不是await theTaskResultOfA

相反,请考虑一下:

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

  return 42;
}

一旦执行到await,它就会看到正在等待的任务尚未完成并将控制权返还给其调用者;然后调用者中的await将控制权返回给调用者。我们设法使一些代码与UI异步。 UI线程与A之间的同步性是偶然的,我们将其删除了。

这里的重要部分是:在没有检查代码的情况下,无法从外部区分这两种实现。只有返回类型是方法签名的一部分 - 它不会说该方法将异步执行,只有可能。这可能有很多好的理由,因此没有必要与之作斗争 - 例如,当结果已经可用时,没有必要打破执行的线程:

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

// Do some CPU intensive task
ComputeAllTheFuzz();

response = await responseTask;

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

这当然是为什么在更通用的异步方法中通常会避免使用这种模式。它对于异步代码的某些使用(避免浪费线程和CPU时间)非常有用,但对其他代码则不然(保持UI响应)。如果您希望这种方法能够保持UI响应,那么您对结果不满意。但是,如果你将它作为Web服务的一部分使用,它会很好用 - 重点是避免浪费线程,而不是保持UI响应(已经通过异步调用服务端点提供了 - 那里& #39;在服务方面再次做同样的事情没有好处。)

简而言之,await允许您编写与其调用者异步的代码。它没有调用异步的神奇力量,它与所有东西都不是异步的,它不会阻止你使用CPU或阻塞线程。它只是为您提供了一些工具,可以轻松地从异步操作中创建同步工作流,并将整个工作流的一部分呈现为与其调用者异步。

让我们考虑一下UI事件处理程序。如果各个异步操作碰巧不需要执行线程(例如异步I / O),则异步方法的一部分可能允许其他代码在原始线程上执行(并且UI在这些部分中保持响应)。当操作再次需要CPU /线程时,它可能需要或不需要原始线程来继续工作。如果是这样,UI将在CPU工作期间再次被阻止;如果它没有( awaiter 使用ConfigureAwait(false)指定此项),则UI代码将并行运行。当然,假设有足够的资源来处理这两者。如果你需要UI始终保持响应,你不能使用UI线程进行任何足够长的执行时间 - 即使这意味着你必须包装一个不可靠的&#34;通常是异步的,但有时会阻塞一些秒&#34; Task.Run中的异步方法。这两种方法都有成本和收益 - 与所有工程一样,这是一种权衡:)

  1. 当然,就抽象而言是完美的 - 每个抽象都会泄漏,并且等待和其他异步执行方法存在大量泄漏。
  2. 一个足够聪明的优化器可能允许B的某些部分运行,直到实际需要A的返回值为止;这就是你的CPU用正常的&#34;同步&#34;代码(Out of order execution)。但是,这样的优化必须保持同步性的外观 - 如果CPU误判了操作的顺序,它必须丢弃结果并提供正确的排序。

答案 6 :(得分:3)

这是异步代码,显示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的关键结果(适用于线程)时,释放控制和重新同步

注意:在制作此代码时没有阻止任何线程:)

我认为有时混淆可能来自“任务”,这并不意味着某些东西在自己的线程上运行。它只是意味着要做的事情,async / await允许将任务分解为阶段并将这些不同的阶段协调为流程。

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