Task.Result和.GetAwaiter.GetResult()是相同的吗?

546

我最近在阅读一些使用大量异步方法的代码,但有时需要同步执行它们。这段代码执行以下操作:

Foo foo = GetFooAsync(...).GetAwaiter().GetResult();

这是一样的吗

Foo foo = GetFooAsync(...).Result;

10
GetResult 的文档中可以看到:“此类型及其成员旨在供编译器使用。” 其他人不应该使用它。 - spender
50
这被称为“异步同步”,除非你知道任务是如何实现的,否则可能是一个非常糟糕的主意。在许多情况下(例如在MVC中的async/await方法),它可能会立即死锁。 - Marc Gravell
12
不要在异步代码上阻塞当你使用异步代码时,避免在其上使用阻塞同步调用。这个错误是因为在异步代码中使用同步调用会导致线程池的资源被浪费,同时也会降低应用程序的吞吐量。相反,尽可能地使用异步等待,并将异步操作封装在 async 方法中。如果必须使用同步调用,请使用 ConfigureAwait(false) 来避免出现上下文切换的开销,从而提高性能。 - I4V
96
在现实世界中,我们有构造函数,我们需要实现“无等待”的接口,并且到处都存在异步方法。我希望使用一些可以直接运行的东西,而不必担心为什么它是“危险的”,“不能使用”或“无论如何都要避免”。每次我尝试处理异步操作时,都会让我感到头痛。 - Larry
完全同意Larry。我正在从我的单体应用程序调用一个新的“async”微服务,并且到处使用“async/await”意味着我必须更新100多个文件。相当头痛。 - wilmol
7个回答

309

Task.GetAwaiter().GetResult()优于Task.WaitTask.Result,因为它传播异常而不是将其包装在AggregateException中。然而,这三种方法都会导致死锁和线程池耗尽问题。应该避免使用它们,转而使用async/await

下面的引用解释了为什么Task.WaitTask.Result没有简单地包含Task.GetAwaiter().GetResult()的异常传播行为(由于“非常高的兼容性标准”)。

正如我之前提到的,我们有很高的兼容性标准,因此我们避免了破坏性变化。因此,Task.Wait 保留了其始终包装的原始行为。但是,在某些高级情况下,您可能希望获得类似于Task.Wait所使用的同步阻塞的行为,但是您希望原始异常传播而不是被包含在AggregateException中。为了实现这一点,您可以直接针对任务的awaiter进行操作。当您编写“await task;”时,编译器将其转换为使用Task.GetAwaiter()方法的用法,该方法返回一个具有GetResult()方法的实例。在故障的任务上使用GetResult()将传播原始异常(这就是“await task;”如何获得其行为的方式)。因此,如果您想直接调用此传播逻辑,可以使用“task.GetAwaiter().GetResult()”。

https://devblogs.microsoft.com/pfxteam/task-exception-handling-in-net-4-5/

GetResult” 实际上意味着“检查任务是否存在错误”。一般情况下,我尽力避免在异步任务上同步阻塞。然而,在少数情况下,我会违反这个指导方针。在那些罕见的情况下,我的首选方法是使用 GetAwaiter().GetResult(),因为它保留了任务异常,而不是将它们包装在一个 AggregateException 中。

https://blog.stephencleary.com/2014/12/a-tour-of-task-part-6-results.html


4
基本上,Task.GetAwaiter().GetResult()等同于await task。我认为第一个选项在方法无法标记为async(例如构造函数)时使用。如果是这样,那么它与@It'sNotALie的最佳答案产生了冲突。 - OlegI
10
@OlegI说:Task.GetAwaiter().GetResult() 更类似于 Task.WaitTask.Result(因为它们三个都会同步阻塞并有潜在死锁风险),但是 Task.GetAwaiter().GetResult() 具有等待异步任务时异常传播的行为。 - Nitin Agarwal
5
看以下引用:“使用ConfigureAwait(false)来避免死锁是一种危险的做法。您将不得不对阻塞代码调用的所有方法的传递闭包中的每个等待都使用ConfigureAwait(false),包括所有第三方和第二方代码。使用ConfigureAwait(false)来避免死锁最多只是一个技巧)...更好的解决方案是“不要在异步代码上阻塞”。”- https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html - Nitin Agarwal
22
我不明白。Task.Wait和Task.Result是设计上有缺陷的吗?为什么它们没有被弃用? - osexpert
2
这个语句“不要在异步代码上阻塞”真的让我很沮丧。我正在将旧的控制台应用程序从.NET 3.x和4.5转换为.NET 6.0以获得LTS支持。我正在为Repository和Service类使用异步调用,但是一些原始代码并没有设置为异步操作,所以我必须阻塞异步调用。既然我一开始就在STA线程中,如果异步调用使用GetAwaiter().GetResult(),这样做是否可以接受?请记住,这不是理论,而是一个实际的例子,因此我希望得到一个实际的用例答案。 - ScottyMacDev
显示剩余5条评论

252

编辑:本文写于我13岁时,已经过时。我建议使用Nitin Agarwal的答案代替。

基本上是这样的。不过有一个小差别:如果Task失败了,GetResult()会直接抛出引起失败的异常,而Task.Result会抛出一个AggregateException。然而,既然是async,为什么要使用它们中的任何一个呢?100倍更好的选择是使用await

另外,你不应该使用GetResult()。它只是用于编译器的,不是给你用的。但如果你不想要烦人的AggregateException,可以使用它。


28
如果你的单元测试框架支持异步单元测试,那么就不需要担心这个问题。我认为大多数框架的最新版本都支持异步单元测试。 - svick
15
MSTest、xUnit 和 NUnit 都支持“async Task”单元测试,而且早就支持了。 - Stephen Cleary
29
反对使用100倍增加是不够的 - 如果您正在调整旧代码并且需要使用await,则使用它会使情况恶化1000倍,并需要重写代码。 - stuck
17
我不同意。 - Stephen Cleary
101
更好的选择是使用await。我讨厌这样的说法,如果可以,我会在前面加上await。但是,在我尝试使异步代码与非异步代码(就像在Xamarin中经常发生的情况)协同工作时,我经常不得不使用ContinueWith等方法来防止死锁UI。编辑:我知道这很旧了,但这并不能减轻我的沮丧,因为找到没有替代方案的答案无法解决在某些情况下你不能简单地使用await的问题。 - Thomas F.
显示剩余13条评论

78

https://github.com/aspnet/Security/issues/59

“最后一个提示:尽量避免使用Task.ResultTask.Wait,因为它们总是将内部异常封装在AggregateException中,并用通用消息(出现了一个或多个错误)替换原始消息,这使得调试更加困难。即使不应该经常使用同步版本,您也应该强烈考虑改用Task.GetAwaiter().GetResult()。”


24
这里引用的来源是别人的引用,没有参考资料。需要考虑上下文:我可以看到很多人在阅读这篇文章后盲目地在各处使用GetAwaiter().GetResult()。 - Jack Ukleja
2
那么我们不应该使用它吗? - tofutim
19
如果两个任务都以异常结束,那么在这种情况下执行 Task.WhenAll(task1, task2).GetAwaiter().GetResult(); 时将会失去第二个任务。 - Monsignor
1
这是另一个例子:https://github.com/aspnet/AspNetCore/issues/13611 - George Chakhidze

47

另一个区别在于当 async 函数返回的仅是 Task 而不是 Task<T> 时,您将无法使用

GetFooAsync(...).Result;

然而

GetFooAsync(...).GetAwaiter().GetResult();

仍然有效。

我知道问题中的示例代码是针对Task<T>的情况,但是问题是一般性的。


1
这是不正确的。查看我的fiddle,它使用了完全相同的结构: https://dotnetfiddle.net/B4ewH8 - wojciech_rak
5
在你的代码中,你正在使用GetIntAsync()函数的Result属性,它返回的是Task<int>而不仅仅是Task。我建议您再次阅读我的答案。 - Nuri Tasdemir
2
你说得对,一开始我误解了你的回答,认为在返回 Task 的函数内部无法使用 GetFooAsync(...).Result。现在我明白了,因为在 C# 中没有 void 属性(Task.Result 是一个属性),但是你当然可以调用 void 方法。 - wojciech_rak
2
“Task” 没有返回值,因此我们期望 “.Result” 是一个错误。然而,“task.GetAwaiter().GetResult()” 仍然有效,这是违反直觉的,值得强调一下。 - H2ONaCl

34

正如先前提到的,如果您可以使用await。如果您需要像您提到的那样同步运行代码,.GetAwaiter().GetResult().Result.Wait()会有死锁风险,正如许多评论/答案中所说的那样。由于大多数人喜欢一行代码,因此您可以在.Net 4.5<中使用这些。

通过异步方法获取值:

var result = Task.Run(() => asyncGetValue()).Result;

同步调用异步方法

Task.Run(() => asyncMethod()).Wait();
由于使用Task.Run,不会发生死锁问题。
更新:如果调用线程来自线程池,则可能会导致死锁问题。以下情况会发生:一个新任务被排队到队列的末尾,将最终执行该任务的线程池线程阻塞,直到该任务执行。
来源:https://dev59.com/7WQm5IYBdhLWcg3w6CWr#32429753https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d

如果您投反对票,请说明原因。否则很难改进答案。 - Ogglas
2
为什么它可以防止死锁?我意识到 Task.Run 将工作转移到了 ThreadPool,但我们仍然在 这个 线程上等待该工作完成。 - Mike
1
@Mike 使用.Result.Wait() 的问题在于如果您阻塞了应该处理任务的线程,那么就没有线程来完成任务。您可以在此处了解更多信息:https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d - Ogglas
奇怪,我会使用Task.Run(async () => await blahblah());来执行任务。 - undefined

25
我检查了TaskOfResult.cs的源代码(TaskOfResult.cs的源代码):
如果Task没有完成,Task.Result将在getter中调用Task.Wait()方法。
public TResult Result
{
    get
    {
        // If the result has not been calculated yet, wait for it.
        if (!IsCompleted)
        {
            // We call NOCTD for two reasons: 
            //    1. If the task runs on another thread, then we definitely need to notify that thread-slipping is required.
            //    2. If the task runs inline but takes some time to complete, it will suffer ThreadAbort with possible state corruption.
            //         - it is best to prevent this unless the user explicitly asks to view the value with thread-slipping enabled.
            //#if !PFX_LEGACY_3_5
            //                    Debugger.NotifyOfCrossThreadDependency();  
            //#endif
            Wait();
        }

        // Throw an exception if appropriate.
        ThrowIfExceptional(!m_resultWasSet);

        // We shouldn't be here if the result has not been set.
        Contract.Assert(m_resultWasSet, "Task<T>.Result getter: Expected result to have been set.");

        return m_result;
    }
    internal set
    {
        Contract.Assert(m_valueSelector == null, "Task<T>.Result_set: m_valueSelector != null");

        if (!TrySetResult(value))
        {
            throw new InvalidOperationException(Strings.TaskT_TransitionToFinal_AlreadyCompleted);
        }
    }
}

如果我们调用TaskGetAwaiter方法,Task将会包装TaskAwaiter<TResult>GetAwaiter()源代码),(TaskAwaiter源代码):
public TaskAwaiter GetAwaiter()
{
    return new TaskAwaiter(this);
}

如果我们调用TaskAwaiter<TResult>GetResult()方法,它将调用Task.Result属性,而Task.Result将调用TaskWait()方法( GetResult()源代码):
public TResult GetResult()
{
    TaskAwaiter.ValidateEnd(m_task);
    return m_task.Result;
}

这是ValidateEnd(Task task)的源代码(ValidateEnd(Task task)的源代码):
internal static void ValidateEnd(Task task)
{
    if (task.Status != TaskStatus.RanToCompletion)
         HandleNonSuccess(task);
}

private static void HandleNonSuccess(Task task)
{
    if (!task.IsCompleted)
    {
        try { task.Wait(); }
        catch { }
    }
    if (task.Status != TaskStatus.RanToCompletion)
    {
        ThrowForNonSuccess(task);
    }
}

这是我的结论:
可以看到,GetResult() 调用了 TaskAwaiter.ValidateEnd(...),因此 Task.ResultGetAwaiter.GetResult() 不同。
我认为 GetAwaiter().GetResult() 是一个更好的选择,而不是使用 .Result,因为前者不会包装异常。
我在《C# 7 in a Nutshell》(Joseph Albahari & Ben Albahari)的第582页上读到了这个信息。
引用如下: 如果前置任务出现故障,当继续代码调用 awaiter.GetResult() 时,异常将被重新抛出。我们可以直接访问前置任务的 Result 属性,而不是调用 GetResult。调用 GetResult 的好处是,如果前置任务出现故障,异常将直接抛出,而不会被包装在 AggregateException 中,从而使捕获块更简单、更清晰。

来源:C# 7 in a Nutshell的第582页


2
如果一个任务失败,当继续执行代码调用awaiter.GetResult()时,异常会被重新抛出。我们可以直接访问任务的Result属性而不是调用GetResult。调用GetResult的好处在于,如果任务失败,异常会直接抛出而不会被包装在AggregateException中,从而可以更简单、更清晰地捕获异常。
对于非泛型任务,GetResult()具有void返回值。它的有用功能仅在于重新抛出异常。
来源:C# 7.0权威指南

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接