AsyncFunction().GetAwaiter().GetResult() 和 Task.Run(() => AsyncFunction).GetAwaiter().GetResult() 有什么区别?

What's the difference between AsyncFunction().GetAwaiter().GetResult() and Task.Run(() => AsyncFunction).GetAwaiter().GetResult()?

我试着理解GetAwaiter().GetResult()之间的概念。我知道如果可能的话不应该使用它,但在这种情况下我不能使用 async/await 所以我必须从同步问题中的异步函数获得结果。

让我们考虑一下这个简单的 Blazor 服务器应用程序:

@page "/"

<button @onclick="GetAwaiterGetResultOnTask">Task GetAwaiter().GetResult()</button>
<button @onclick="GetAwaiterGetResultOnFunction">Function GetAwaiter().GetResult()</button>

<p>@_message</p>
@code {

    private string _message;
    private async Task<string> AsyncFunction(string message)
    {
        await Task.Delay(500);

        return message;
    }

    private void GetAwaiterGetResultOnTask()
    {
        _message = Task.Run(() => AsyncFunction("Message from GetAwaiterGetResultOnTask")).GetAwaiter().GetResult();
    }

    private void GetAwaiterGetResultOnFunction()
    {
        _message = AsyncFunction("Message from GetAwaiterGetResultOnFunction").GetAwaiter().GetResult();
    }
}

调用函数GetAwaiterGetResultOnFunction将导致死锁。但是,当我调用 GetAwaiterGetResultOnTask 时,它不会。

这些函数之间的主要区别是什么?为什么在 Task.Run 上调用时不会导致死锁?

await 知道一个叫做 SynchronizationContext 的东西。如果存在这样的上下文(SynchronizationContext.Current 不为空),那么 await 之后的继续将传递给该上下文,因为它应该知道如何更好地处理它(除非您明确告诉不要这样做,使用 await someTask.ConfigureAwait(continueOnCapturedContext: false))

大多数 UI 框架不喜欢从多个线程同时访问 UI ,因为它通常会导致各种难以调试的问题。他们利用 SynchronizationContext 来强制执行单一流程。要做到这一点 - 他们通常将回调发布到 SynchronizationContext 在某种队列中,并在单个“UI”线程上一个一个地执行它们。

Blazor 也这样做。 GetAwaiterGetResultOnFunction 是你的案例是在那个“UI”线程上执行的,SynchronizationContext 可用。当执行到达 AsyncFunction 内的 await Task.Delay 时 - 当前上下文被捕获,并且注意到当 Task.Delay 完成时 - 函数的其余部分应该发布到上下文以供执行。

然后通过对从 AsyncFunction 返回的任务执行 GetResult 来阻塞“UI”线程。然后 Task.Delay 完成,函数的其余部分将发布到 Blazor 的 SynchronizationContext 以执行。但是,要做到这一点,Blazor 需要您刚刚阻止的相同 UI 线程,从而导致死锁。

当您调用 GetAwaiterGetResultOnTask 时 - AsyncFunction 不会 运行 在 Blazor 的“UI”线程上 - 它 运行 在其他一些(线程池)线程。 SynchronizationContext 为空,await Task.Delay 之后的部分将在某些线程池线程上 运行 而不需要“UI” 线程来完成。那么就没有死锁了。