【问题标题】:What's the difference between using .Result inside a Task body and using await in async method?在任务正文中使用 .Result 和在异步方法中使用 await 有什么区别?
【发布时间】:2019-03-11 21:03:32
【问题描述】:

我设法在互联网上找到了一些类似问题的答案,但没有人解释得足以让我理解下面代码的不同之处。

我知道 await 与 .Result 不同,它不会阻塞调用线程。但是,如果我们试图从一个任务中访问这个属性,并且无论如何都不会阻止它呢?

比如这个有什么区别

public static Task PrintPageAsync(string url)
{
    return Task.Run(() =>
    {
        WebRequest webRequest = WebRequest.Create(url);
        WebResponse response = webRequest.GetResponseAsync().Result;
        using (StreamReader reader = new StreamReader(response.GetResponseStream()))
        {
            string text = reader.ReadToEndAsync().Result;
            Console.WriteLine(text);
        }
    });
}

还有这个

public static async Task PrintPageAsync(string url)
{
    WebRequest webRequest = WebRequest.Create(url);
    WebResponse response = await webRequest.GetResponseAsync();
    using (StreamReader reader = new StreamReader(response.GetResponseStream()))
    {
        string text = await reader.ReadToEndAsync();
        Console.WriteLine(text);
    }
}

【问题讨论】:

  • 没关系,区别还是会阻塞。像这样将异步与阻塞混合在一起可能会导致代码锁定。
  • 这很有趣,“它会阻塞”是什么意思?我在 Main 方法中使用它,如下所示: PrintPageAsync(@"en.wikipedia.org/wiki/Instruction-level_parallelism"); while (true) { Console.Write("*"); } 无论我使用哪一个,它都不会阻塞。它开始立即打印星星,一段时间后它会打印页面并在此之后仍在打印星星。
  • 那是因为第一个是在另一个线程中启动的。在这种情况下,您不妨调用同步的ReadToEnd。删除Task.Run 看看会发生什么。
  • 我想我明白了,但我的意思是当我使用Task.Run时两者有什么区别,所以它从另一个线程开始。调用它时它的行为是否会与异步方法不同?
  • 最终区别在于使用了多少线程。对于这个简单的示例来说,差别不大,但如果这是在 Web 服务调用后面的服务器上的代码,那么当您收到许多请求并且服务器由于一堆阻塞线程而变慢时,您就会遇到扩展问题。

标签: c# multithreading asynchronous async-await task


【解决方案1】:

由于第一个示例在线程池线程上启动了一个新任务,当有等效的同步方法时,调用异步方法是不必要的。它没有任何好处,只是不必要地增加了管理异步任务所涉及的一些开销。只需使用.GetResponse() 代替.GetResponseAsync().Result.ReadToEnd() 代替.ReadToEndAsync().Result

await 与 .Result 不同,不会阻塞调用线程

这并不总是正确的。等待的方法可以(部分或完全)同步执行,如果它们决定这样做的话。

比如,有什么区别吗?

这两个例子有很多不同之处。尽管它们在某些情况下可能无关紧要,但在另一种情况下却可能至关重要:

  1. AggregateException

    当任务中发生异常或任务被取消时,调用Task.Result 会将异常包装在AggregateException 中。相反,awaiting 这个任务会抛出原来的异常。所以catching特定异常时一定要小心。

  2. 线程

    在第一个示例中,整个方法将在同一个(线程池)线程上执行。第二个示例可以在几个不同的线程上执行,具体取决于当前的SynchronizationContext。应避免对线程关联敏感的代码。

  3. SynchronizationContext

    第一个示例将在没有SynchronizationContext 的情况下执行,而第二个示例将在每个await 之后恢复原始SynchronizationContext。对于控制台应用程序,这无关紧要。但在 WPF 或 WinForms 应用程序中,UI 元素只能从相应的同步上下文中访问。

  4. 异步执行

    在第一个示例中,PrintPageAsync 将在新任务排队等待执行后立即返回,而第二个示例将同步执行到第一个 await(甚至可能在此之后)。这会对 GUI 响应产生严重影响,尤其是当异步方法使用WebRequest 时,因为GetResponseAsync() 方法同步执行 DNS 解析(请参阅How to create HttpWebRequest without interrupting async/await?)。因此,当从 UI 线程调用使用 WebRequest 的方法时,建议将代码包装在 Task.Run() 中。

【讨论】:

    【解决方案2】:

    .Result 将同步执行您的代码,即您无视 Task 和整个 TPL 背后的本质。正如它所说,await 是编译器以一种古老的“回调”方式(又名典型的JavaScript)重写您的方法的标记,这是 异步 完成方式完全相同的计算。

    更简单:您应该尽可能选择await 而不是.Result

    【讨论】:

    • 为什么重要?如果我们在 Task 的 body 中使用它,它不只是阻塞在任务内部,并且调用方法不受影响吗?无论我使用哪一个,它们都会给我相同的结果。例如。 PrintPageAsync(@"en.wikipedia.org/wiki/Instruction-level_parallelism"); while (true){ Console.Write("*"); } 在 Main 方法中使用这个打印星星,一段时间后打印页面的内容并再次打印星星。
    • 我认为你必须更深入地了解异步是什么。它会一直阻塞,直到您获得底层计算的结果,或者它被取消/异常失败。无论哪种方式都意味着 sync 执行与Task 的目的相反。
    • 我理解其中的区别,但我认为由于我们在另一个线程中启动任务,它不会阻塞 调用线程,当我们阻塞时使用 .Result,我们阻塞了任务的线程,而不是调用线程。我完全错了吗?此外,正如我所提到的,两种方法的输出(使用我之前评论中的代码时)是相同的。如果其中一个是阻塞的,而另一个不是,那为什么输出是一样的?
    • @pink.Overload 您的示例只是滥用任务。除非绝对必要,否则不要将 Task 包装到 Task 中。这样的代码在线程队列消耗方面非常低效。实际上,在您的示例中 calling threade (Task.Run(...)) 没有被阻止;创建一个 - 被阻止。不仅如此,取决于多种因素,可能不会创建新线程,在这种情况下,您将阻塞调用线程。长话短说,不要违背Task(简单明了)的规则,否则有一天它会伤害你。
    • 我明白了。您能否指定在我的代码中将Task 包装成Task 的位置,以便我知道应该避免什么?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-31
    • 1970-01-01
    • 2015-02-12
    • 1970-01-01
    • 2014-02-09
    • 1970-01-01
    相关资源
    最近更新 更多