【问题标题】:Is async/await suitable for methods that are both IO and CPU bound?async/await 是否适用于 IO 和 CPU 绑定的方法?
【发布时间】:2013-01-31 13:19:33
【问题描述】:

MSDN 文档似乎声明 asyncawait 适用于 IO-bound 任务,而 Task.Run 应用于 CPU-bound 任务。

我正在开发一个应用程序,该应用程序执行 HTTP 请求以检索 HTML 文档,然后对其进行解析。我有一个看起来像这样的方法:

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync())
        return await Task.Run(() => LoadHtmlDocument(contentStream)); //CPU-bound
}

asyncawait 这样的用法好用吗,还是我用得太多了?

【问题讨论】:

    标签: c# asynchronous async-await c#-5.0


    【解决方案1】:

    适用于await 任何异步操作(即由Task 表示)。

    关键点在于,对于 IO 操作,只要有可能,您都希望使用提供的方法,即其核心的异步方法,而不是在阻塞同步方法上使用 Task.Run。如果您在执行 IO 时阻塞了一个线程(甚至是线程池线程),那么您就没有充分利用 await 模型的真正力量。

    一旦您创建了一个代表您的操作的Task,您就不再关心它是否受 CPU 或 IO 限制。对于调用者来说,这只是一些需要await-ed 的异步操作。

    【讨论】:

      【解决方案2】:

      有几件事情需要考虑:

      • 在 GUI 应用程序中,您希望在 UI 线程上执行的代码尽可能少。在这种情况下,使用Task.Run() 将 CPU 绑定操作卸载到另一个线程可能是一个好主意。尽管您的代码的用户可以自己执行此操作,如果他们愿意的话。
      • 在类似 ASP.NET 应用程序中,没有 UI 线程,您只关心性能。在这种情况下,使用Task.Run() 而不是直接运行代码会产生一些开销,但如果操作实际上需要一些时间,这应该不会很重要。 (此外,返回同步上下文会产生一些开销,这也是为什么您应该在库代码中对大多数 awaits 使用 ConfigureAwait(false) 的另一个原因。)
      • 如果您的方法是异步的(顺便说一句,它也应该反映在方法的名称中,而不仅仅是它的返回类型),人们会期望它不会阻塞同步上下文线程,即使对于 CPU 密集型工作也是如此。

      权衡一下,我认为在这里使用await Task.Run() 是正确的选择。它确实有一些开销,但也有一些优势,这可能很重要。

      【讨论】:

        【解决方案3】:

        已经有两个很好的答案,但要添加我的 0.02...

        如果您谈论的是消耗异步操作,async/await 非常适用于 I/O 密集型和 CPU 密集型。

        我认为 MSDN 文档确实略微倾向于生成异步操作,在这种情况下,您确实希望将 TaskCompletionSource(或类似的)用于 I/O 绑定和 Task.Run (或类似的)用于 CPU 绑定。创建初始 Task 包装器后,最好由asyncawait使用

        对于您的特定示例,这实际上取决于LoadHtmlDocument 将花费多少时间。如果您删除Task.Run,您将在调用LoadPage 的同一上下文中执行它(可能在UI 线程上)。 Windows 8 指南规定,任何花费超过 50 毫秒的操作都应进行 async...请记住,您的开发人员计算机上的 50 毫秒可能会在客户端计算机上更长...

        所以如果你能保证LoadHtmlDocument的运行时间小于50ms,你可以直接执行:

        public async Task<HtmlDocument> LoadPage(Uri address)
        {
          using (var httpResponse = await new HttpClient().GetAsync(address)) //IO-bound
          using (var responseContent = httpResponse.Content)
          using (var contentStream = await responseContent.ReadAsStreamAsync()) //IO-bound
            return LoadHtmlDocument(contentStream); //CPU-bound
        }
        

        但是,我会推荐 ConfigureAwait @svick 提到的:

        public async Task<HtmlDocument> LoadPage(Uri address)
        {
          using (var httpResponse = await new HttpClient().GetAsync(address)
              .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
          using (var responseContent = httpResponse.Content)
          using (var contentStream = await responseContent.ReadAsStreamAsync()
              .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
            return LoadHtmlDocument(contentStream); //CPU-bound
        }
        

        对于ConfigureAwait,如果HTTP 请求没有立即(同步)完成,那么(在这种情况下)这将导致LoadHtmlDocument 在线程池线程上执行,而无需显式调用Task.Run

        如果您对 async 在此级别的性能感兴趣,您应该查看 Stephen Toub 的 videoMSDN article 关于该主题的内容。他有大量有用的信息。

        【讨论】:

        • Thought ConfigureAwait(false) 意味着任务不应该在它开始的上下文中恢复,当它完成时。不是哪个线程完成任务的工作。
        • @mmcrae:你的理解是正确的。当LoadPage 恢复时,它在线程池线程上运行,然后执行LoadHtmlDocument(同时在该线程池线程上)。
        • 我认为需要注意的是,在上面的示例中,LoadHtmlDocument 基本上仍然处于阻塞状态,并且无论如何都没有被卸载。
        • @davidcarr - 根据答案,这仅在某些情况下是正确的:“使用 ConfigureAwait,如果 HTTP 请求没有立即(同步)完成, 那么这将(在这种情况下)导致 LoadHtmlDocument 在线程池线程上执行...” 但我同意,为了确保它始终关闭 UI 线程,应该在 @987654347 上使用 await Task.Run @调用。
        • @ToolmakerSteve 是否意味着即使使用ConfigureAwait(false),如果它同步完成(你永远不知道),LoadHtmlDocument 仍然会在 UI 线程上执行吗?如果LoadHtmlDocument 阻塞,UI 会冻结?
        猜你喜欢
        • 2019-02-27
        • 2020-03-26
        • 2018-08-23
        • 1970-01-01
        • 2021-05-10
        • 1970-01-01
        • 2015-09-26
        • 2018-03-24
        相关资源
        最近更新 更多