【问题标题】:Is async/await useless for truly synchronous code?async/await 对于真正的同步代码是无用的吗?
【发布时间】:2020-04-20 11:49:42
【问题描述】:

我有一些代码需要读取网页(它是 XML)然后处理它。在我得到 XML 之前,我无能为力。

我正在执行一项任务的整个阅读和过程。但是在任务本身中,调用 HttpWebRequest.GetResponseAsync() 而不是 HttpWebRequest.GetResponse() 有什么好处吗?我无法将可取消的令牌传递给 GetResponseAsync(),因此在这种情况下,我认为我应该进行阻塞调用。

这是基于我(仍在学习)对等待的理解。据我了解:

  1. 在异步方法上调用 await 意味着不要阻塞该方法并立即返回一个 Task 对象。
  2. 对 Task 对象调用 await 意味着阻塞,直到任务完成或被取消。

更新: 我忘记了一条重要信息——这是任务中的代码。所以我已经没有阻止用户界面。问题是,在这个从功能上讲是一系列顺序操作的后台任务中,使用异步调用有什么价值吗?

【问题讨论】:

  • await 不会阻止,无论其目标如何。另外,使用HttpClient 而不是HttpWebRequestHttpClient 确实支持 CacellationToken
  • 问题集中在错误的细节上。是否使用异步代码取决于应用程序中发生的其他事情。其中“其他”通常是用户界面。有时需要处理大量客户端服务请求的服务器式应用程序。如果您从慢速 Web 服务器获得的延迟无关紧要,请不要使用异步代码。
  • 嗨,戴夫。希望事情进展顺利。阻塞调用在等待结果时阻塞线程。 await 不会阻塞线程。如果线程是 UI 线程(长时间阻塞 UI 线程是个坏主意)或者您有很多同时操作(您可能会用完线程),这种区别可能很重要。
  • 嗨,大卫。这就是为什么 async / await 很难。一个人需要先弄清楚单词。您需要记住,阻塞意味着在进程中保持线程处于活动状态,这就是您需要在心理上映射短语“阻塞”的方式。等待和阻塞是不一样的。 await GetResponseAsnyc() 等待但不会阻塞 GetResponse() 等待的位置,并阻塞进程/操作系统内存。 async / await 的主要目的是内存管理。它的并行化方面更多的是一个副产品,因为它通过 TPL / Task 表现出来,您可能需要或不需要等待。

标签: .net asynchronous async-await


【解决方案1】:

我知道你在问await麻烦是什么时候异步代码按顺序执行步骤(即当一个函数既可以是异步函数又可以是非异步函数时)。

在您的示例中,使用HttpWebRequest(这是没有实际意义的,因为我们都应该使用HttpClient,但我离题了):

传统的阻塞代码 - 假设这是一个 WinForms 上下文:

// (Error handling omitted for brevity)

void PopulateTextBox() {

    using( HttpWebRequest req = HttpWebRequest.CreateHttp( "https://www.bing.com" ) )
    using( HttpWebResponse res = (HttpWebResponse)req.GetResponse() )
    using( Stream body = res.GetResponseStream() )
    using( StreamReader rdr = new StreamReader( body ) )
    {
        this.textbox.Text = rdr.ReadToEnd();
    }
}

和异步代码等效:

// Note that HttpWebResponse does not have a `GetResponseStreamAsync()` method.
// (Error-handling and ConfigureAwait omitted for brevity)

async Task PopulateTextBoxAsync() {

    using( HttpWebRequest req = HttpWebRequest.CreateHttp( "https://www.bing.com" ) )
    using( HttpWebResponse res = (HttpWebResponse)( await req.GetResponseAsync() ) )
    using( Stream body = res.GetResponseStream() ) // HttpWebResponse does not have a `GetResponseStreamAsync()` method
    using( StreamReader rdr = new StreamReader( body ) )
    {
        this.textbox.Text = await rdr.ReadToEndAsync();
    }
}

鉴于所有不同的程序步骤都是按顺序进行的,因此很容易忽略异步代码的优势 - 但是:

  1. await 不阻塞,线程让给调度程序(就像在 Windows 3.xx 天一样) - 允许 GUI 线程泵送窗口消息,允许处理您可能仍希望允许的 UX 事件并允许重绘窗口的程序(例如,如果用户在运行此方法时调整窗口大小)。同步代码不允许这样做,并且用户在暂时冻结的窗口中体验不佳 - 如果您曾经在低速或高延迟连接上使用过 Outlook 或 SQL Server Management Studio,您就会知道我在说什么,并且会降低用户对您产品质量的信心。

  2. 您可以争论 w.r.t 冻结窗口“嗯,这就是 BackgroundWorker(或一般的后台线程)的用途!”是的 - 这就是它们在 2005 年被添加的原因,但问题是 它们无法扩展 。如果您有任何超出微不足道的应用程序的内容,则您无法为要完成的每个新的非 UI 冻结任务启动新的后台线程,因为 Windows 中的每个线程都会为您的进程的私有内存使用量增加超过一兆字节(由于默认堆栈大小)。通过使用在同一线程(或线程池中的现有线程)上运行的异步代码,您不会导致程序的内存增加。此外,在 Windows 上实例化线程也不是特别便宜。

    • 这就是为什么当今所有很酷的 Web 服务器和 Web 应用程序平台(如 Nginx、Node.JS、ASP.NET Core 等)都在异步 IO 上占很大比重,因为这意味着线程不会被阻塞 - 它会大量增加网络服务器可以同时处理的连接数。始于 1990 年代的“每个连接一个线程”的想法(具有讽刺意味的是,为了简化套接字编程)只是在遇到扩展问题时才使它变得复杂——因为那时一台 1990 年代后期的具有 256MB 内存的机器就不能处理 1,000 个同时连接而不会出现大量页面抖动,嗯!)
    • 有趣的笔记:For web-applications in IIS, the default thread size is 256KB

回到HttpWebRequest 的例子,考虑到上述情况,以下优势变得显而易见:

  • 使用任何“后台”功能(如BackgroundWorker,或后台线程,或async/await)相比:

    • 用户仍然可以与程序的 GUI 进行交互(即使您禁用控件以防止触发状态中断事件),例如调整窗口大小和重新绘制窗口。
    • Windows 不会告诉用户您的程序已“停止响应”。
  • 与使用BackgroundWorker 或特别是后台线程相比:

    • 您不必处理每个BackgroundWorker 的设置和处理结果(它们出现在单独的事件处理程序方法中,而不是启动器中)。
      • 或者处理可变的类状态,因为BackgroundWorker 真的让你使用可变的Form/UserControl 状态,而且随着我多年来在 SWE 工作中获得的经验,我学会了厌恶 em> 可变状态(可变状态使并发可重入代码的工作变得非常困难)。
    • 由于没有创建新线程,内存使用减少。
    • 改进的规模:您可以触发数千个请求,您的程序会很好 - 但如果您使用后台线程触发一千个 HttpWebRequest 实例,您会遇到问题.

最后,HttpClient(我们都应该使用它而不是 HttpWebRequest只有提供异步方法 - 所以你必须使用await(或ContinueWith,或IAsyncResult-适配器)来使用它。

【讨论】:

  • 这很有帮助。但是一个后续问题。阅读docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… 它有代码“Task eggTask = FryEggs(2); Egg eggs = await eggsTask;” - 在任务完成之前,第二个等待不会阻塞吗?还是我完全误解了这一切?
  • @DavidThielen 需要注意的是,async/await 仅对非 CPU 绑定操作有意义(即 IO,但也有其他场景) . W.R.T. await eggsTask,它不会“阻塞”——当煎蛋操作(表示异步 IO 操作)完成时,线程将自己返回给调度程序。这是因为线程返回给调度程序,允许同一线程执行窗口消息泵,否则它会被阻塞(导致 UI 冻结)。
  • @DavidThielen 在没有其他并发处理(例如 Web 服务器请求)或 UI 线程(例如命令行实用程序)的单线程应用程序中,则无需生成线程返回到调度程序 - 但是这仍然是一个好主意,因为被调用的异步操作可能仍希望将调度程序用于自己的目的(例如 IO 超时等)。无论如何,在操作系统内核内部,所有 IO 都是异步的。
  • 我认为示例中的部分问题是它从不显示 ApplyButter(toast); 的代码。但是在某个地方必须有代码说等到这些异步任务完成。这是在哪里/如何完成的?谢谢
  • @DavidThielen 整个计算机中的任何地方都没有任何代码说“等到这些任务完成”,因为异步操作与正常的阻塞函数调用根本不同。
【解决方案2】:

是的,这毫无意义。我不明白为什么在需要使用返回值的单个线程中使用 async/await 会有所作为。选择的答案以 WinForms 为例,它可以演示阻塞,但是在服务器上处理 WEB 请求时,没有任何东西可以被“阻塞”,因为后续请求有自己的线程。

【讨论】:

猜你喜欢
  • 2021-04-05
  • 1970-01-01
  • 1970-01-01
  • 2016-05-30
  • 2021-07-30
  • 2013-06-08
  • 2014-09-28
  • 2021-06-08
相关资源
最近更新 更多