为什么这个任务会导致死锁?

Why does this task cause a deadlock?

.NET 4.7.2 网站

using System;
using System.Threading.Tasks;
using System.Web;

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack)
        {
            string input = Input.Text;
            bool iWantDeadlock = input == "yes";
            string key = iWantDeadlock
                ? GetHashFragmentAsync(input).GetResultSafely()
                : Task.Run(() => GetHashFragmentAsync(input)).Result;
            Response.Redirect(key);
        }
    }

    private static async Task<string> GetHashFragmentAsync(string key)
    {
        await Task.Delay(100);
        return "#" + HttpUtility.UrlEncode(key);
    }
}

public static class TaskExtensions
{
    public static T GetResultSafely<T>(this Task<T> task)
    {
        return Task.Run(() => task).Result;
    }
}

我做了一个非常简单的页面,有一个文本框,提交后将输入放在页面的 hashfragment 中。

我正在阅读有关任务和死锁的内容(在我 运行 阅读其他人的一些代码后导致了死锁)。

解决方案似乎是这样做的:

Task.Run(() => GetHashFragmentAsync(input)).Result;

所以我想,为了清晰和易于使用,让我们将其作为一个扩展。

public static class TaskExtensions
{
    public static T GetResultSafely<T>(this Task<T> task)
    {
        return Task.Run(() => task).Result;
    }
}

然而,这会导致死锁。代码是相同的,但工作方式却大不相同。 有人可以解释这种行为吗?

因为当您使用 GetHashFragmentAsync(input).GetResultSafely() 时,您实际上有 2 个任务,而不是 1 个。

GetHashFragmentAsync(input) returns 已经开始任务。然后你调用 GetResultSafely() 创建另一个任务。

当您在 UI 线程上等待任务时,您会死锁,因为任务无法返回到同步上下文线程。当你有两个任务时,第二个任务可以同步等待,因为父任务不在 UI 线程上,而是在没有同步上下文的 ThreadPool 线程上。

当你调用任何基于 IO 的代码时,你不应该调用 Task.Run。简单写

protected async void EventHandlerMethod(object sender, EventArgs e)
{
    await GetHashFragmentAsync(input);
}

事情是这样的

GetHashFragmentAsync(input) // Start Task number 1
.GetResultSafely() // Start task number 2 from task number 1 (no synchronization context)
// .Result returns back to task 1

第二种情况

Task.Run(() => GetHashFragmentAsync(input)) // UI thread so, capture synchronization context
.Result // Wait for UI thread to finish (deadlock)

当您调用 GetHashFragmentAsync(input) 时,C# async/await 机制会捕获当前同步上下文。方法 returns 一个依赖于 UI 线程的启动任务。您尝试使用 Task.Run 移动关键 UI 线程的任务,但为时已晚。

GetHashFragmentAsync(input) 必须已经在非 UI 线程上调用。将其包裹在 Task.Run.

这是一个通过工厂工作的辅助方法:

public static T GetResultSafely<T>(Func<Task<T>> task)
{
    return Task.Run(() => task()).Result;
}

在线程池上调用工厂。

我应该说,通常最好的解决方案是一直使用异步 API,或者保持完全同步。出于正确性和性能原因,混合是有问题的。但它绝对可以安全地完成。