【问题标题】:Async/Await - is it *concurrent*?异步/等待 - 它是*并发*吗?
【发布时间】:2011-12-01 13:30:23
【问题描述】:

我一直在考虑 C# 5 中的新异步内容,但出现了一个特别的问题。

我知道await 关键字是一个简洁的编译器技巧/语法糖来实现continuation passing,其中方法的其余部分被分解为Task 对象并排队等待按顺序运行,但是其中控制权返回给调用方法。

我的问题是我听说目前这一切都在一个线程上。这是否意味着这种异步内容实际上只是将延续代码转换为Task 对象,然后在每个任务完成后调用Application.DoEvents() 的一种方式,然后再开始下一个任务?

或者我错过了什么? (这部分问题是修辞性的 - 我完全知道我错过了 something :))

【问题讨论】:

  • 你从哪里听说这个在单线程上运行的? Task,在任务并行库中使用,根据需要使用线程池创建多个线程。
  • 不是必然在一个线程上,也不是必然在多个线程上。您无法保证可以在哪个线程上执行延续。
  • 来自用户组的发言人。此外,虽然不是一个明确的声明,但我记得 Eric Lippert 在本文中提到它可以在单个线程上进行:msdn.microsoft.com/en-gb/magazine/hh456401.aspx。这样做的目的是您不会出现线程不足,并且可以开始在比目前 PC 更少功率的设备上编写响应式应用程序(想想 Windows 8 所针对的各种设备)。
  • @NeilBarnwell - 如果这是我看到的相同演示文稿,他们意味着 async/await 模式没有明确触发线程来处理它。延续可能发生在同一个线程上,也可能不会。如果我理解正确,它甚至可以由(某种程度上)任意现有线程处理。

标签: c# asynchronous concurrency async-await continuations


【解决方案1】:

async/await 背后的整个想法是它可以很好地执行延续传递,并且不会为操作分配新线程。延续可能发生在一个新线程上,它可能在同一个线程上继续。

【讨论】:

  • 啊,好吧。我认为这是我很长时间以来见过的最不透明的功能黑盒之一。这是你不能作弊的事情之一 - 你确实需要很好地了解它如何才能使用它(或者在我的情况下,甚至理解它)。
  • 它是否因此进行了优化,以确保来自 UI 线程的调用被编组回该线程以返回值,或者一旦您使用 async 关键字就全部关闭?
  • 当然,并不是说你曾经想使用你不完全理解的东西;)
  • 我想 UI 线程上的任何异步内容要么按规则返回到 UI 线程,要么修改为以至少自动执行委托到 UI 线程的方式返回,尽管我基于那绝对没有。
  • 呃,不,当然……不过说真的,有些事情至少可以让你暂时摆脱它。我敢打赌,有很多 C# 开发人员不了解字符串实习,也不知道为什么 ReaderWriterLockSlim 在某些情况下比 Monitor 更好。更多的人知道如何驾驶他们的汽车,但如果它坏了就无法修理它。我碰巧认为这是你甚至无法真正使用它的东西,除非你先摸索它。
【解决方案2】:

async/await 真正的“肉”(异步)部分通常是单独完成的,与调用者的通信是通过 TaskCompletionSource 完成的。写在这里http://blogs.msdn.com/b/pfxteam/archive/2009/06/02/9685804.aspx

TaskCompletionSource 类型有两个相关的用途,两者都由其名称暗示:它是创建任务的源,以及完成该任务的源。本质上,TaskCompletionSource 充当任务及其完成的生产者。

这个例子很清楚:

public static Task<T> RunAsync<T>(Func<T> function) 
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ => 
    { 
        try 
        {  
            T result = function(); 
            tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
    }); 
    return tcs.Task; 
}

通过TaskCompletionSource,您可以访问可以等待的Task 对象,但不是通过您创建多线程的 async/await 关键字。

请注意,当许多“慢”函数将转换为 async/await 语法时,您将不需要非常频繁地使用 TaskCompletionSource。他们将在内部使用它(但最终必须有一个TaskCompletionSource 才能获得异步结果)

【讨论】:

    【解决方案3】:

    它是并发,从某种意义上说,许多未完成的异步操作可能随时都在进行中。它可能是也可能不是多线程

    默认情况下,await 会将继续安排回“当前执行上下文”。如果不是null,则“当前执行上下文”定义为SynchronizationContext.Current,如果没有SynchronizationContext,则定义为TaskScheduler.Current

    您可以通过调用ConfigureAwait 并为continueOnCapturedContext 参数传递false 来覆盖此默认行为。在这种情况下,不会将继续安排回该执行上下文。这通常意味着它将在线程池线程上运行。

    除非您正在编写库代码,否则默认行为正是您想要的。 WinForms、WPF 和 Silverlight(即所有 UI 框架)提供 SynchronizationContext,因此继续在 UI 线程上执行(并且可以安全地访问 UI 对象)。 ASP.NET 还提供了一个SynchronizationContext,确保继续在正确的请求上下文中执行。

    其他线程(包括线程池线程、ThreadBackgroundWorker)不提供SynchronizationContext。因此默认情况下,控制台应用程序和 Win32 服务根本没有 SynchronizationContext。在这种情况下,继续在线程池线程上执行。这就是为什么使用await/async 的控制台应用程序演示包括对Console.ReadLine/ReadKey 的调用或在Task 上执行阻塞Wait

    如果您发现自己需要SynchronizationContext,您可以使用我的Nito.AsyncEx 库中的AsyncContext;它基本上只是提供了一个带有SynchronizationContextasync 兼容的“主循环”。我发现它对控制台应用程序和单元测试很有用(VS2012 现在内置了对async Task 单元测试的支持)。

    有关SynchronizationContext 的更多信息,请参阅my Feb MSDN article

    在任何时候都不会调用DoEvents 或等效项;相反,控制流返回,然后继续(函数的其余部分)计划稍后运行。这是一个更简洁的解决方案,因为它不会像使用 DoEvents 那样导致重入问题。

    【讨论】:

    • 如果允许提出改进建议 - 新的 async/await 功能允许以非常富有表现力的方式进行并发编程,但它仍然需要显式的分叉和连接。但在我看来,fork 和 join 使用 async/await 会更加优雅。
    • 优秀的答案。应该在更多的地方发布,然后堆叠溢出。
    • 关于 async/await 的文章有几十篇。但这是我遇到的第一次提到 SyncCtx。没有它,其余的就没有意义了。至于为什么 main() 需要 readline 不清楚,为什么会导致调用 continuation?
    • @Tuntable:它不会导致调用延续。它所做的只是阻止应用程序退出,直到延续有机会运行。此外,由于编写了此代码,.NET Core 已经支持async Task Main,这意味着您可以在Main 中使用await,从而阻止任务(Wait)或等待用户退出(ReadLine) 不再需要。 如果您使用的是 .NET Core。
    • @StephenCleary 谢谢。但这是否意味着等待的主线程在同一线程或不同线程中运行延续?这是一个很少被提及且令人困惑的关键问题(我认为只有在您关心竞争条件并编写实际正确的代码时才至关重要)。 await 的全部意义在于在同一个线程中运行 UI 工作。如果你有一个异步主线程,你可以“DoEvents”运行出色的延续而不停止吗?
    【解决方案4】:

    我喜欢解释它的方式是“await”关键字只是等待任务完成,但在等待时将执行交给调用线程。然后它返回任务的结果,并在任务完成后继续执行“await”关键字之后的语句。

    我注意到的一些人似乎认为 Task 与调用线程在同一个线程中运行,这是不正确的,可以通过尝试更改 await 调用方法中的 Windows.Forms GUI 元素来证明。但是,尽可能在调用线程中运行延续。

    它只是一种在任务完成时不必有回调委托或事件处理程序的巧妙方法。

    【讨论】:

      【解决方案5】:

      我觉得这个问题需要一个更简单的答案。所以我要过分简化了。

      事实是,如果您保存任务并且不等待它们,那么 async/await 是“并发的”。

      var a = await LongTask1(x);
      var b = await LongTask2(y);
      
      var c = ShortTask(a, b);
      

      不是并发的。 LongTask1 将在 LongTask2 启动之前完成。

      var a = LongTask1(x);
      var b = LongTask2(y);
      
      var c = ShortTask(await a, await b);
      

      是并发的。

      虽然我也敦促人们更深入地了解并阅读此内容,但您可以使用 async/await 进行并发,而且非常简单。

      【讨论】:

      • 我不确定这是否会产生如此大的影响,是吗?最终,await aawait b 将同步执行,因为它们是 ShortTask 的 args,并且在调用 LongTask2ShortTask 之间没有额外的指令。
      • @NeilBarnwell await a, await b 片段按顺序等待每个任务。但是任务已经开始了,你如何等待它们相对不重要。在您等待 a 任务时,b 任务也在运行(同时)。实际上没有什么可以阻止b 任务在a 之前完成,在这种情况下await b 语句根本不会等待,因为结果已经在那里了。
      • 那么这如何解决发帖者关于线程的问题?
      猜你喜欢
      • 2017-10-17
      • 1970-01-01
      • 1970-01-01
      • 2018-01-29
      • 1970-01-01
      • 2010-10-11
      • 2016-05-09
      • 1970-01-01
      • 2020-04-25
      相关资源
      最近更新 更多