为什么调用Task<T>.Result不会造成死锁?

8

几个月前,我读了这篇文章后,变得对获取Task<T>Result产生了妄想,并不断地用ConfigureAwait(false)Task.Run包装我的所有调用。然而,由于某种原因,以下代码成功完成:

public static void Main(string[] args)
{
    var arrays = DownloadMany();

    foreach (var array in arrays);
}

IEnumerable<byte[]> DownloadMany()
{
    string[] links = { "http://google.com", "http://microsoft.com", "http://apple.com" };

    using (var client = new HttpClient())
    {
        foreach (var uri in links)
        {
            Debug.WriteLine("Still here!");
            yield return client.GetByteArrayAsync(uri).Result; // Why doesn't this deadlock?
        }
    }
}

代码会打印三次“Still here!”然后退出。这个问题是否只适用于HttpClient,即在它上面调用Result是安全的(就像编写它的人已经使用ConfigureAwait(false)进行了处理)?

顺便说一下,不阻塞并在需要的地方使用await比随处使用ConfigureAwait要简单得多。 - i3arnon
@i3arnon 是的,但我正在编写一个支持同步和异步调用者的API。 - James Ko
1
@JamesKo:我建议异步方法应该公开异步API。但是,如果你必须支持同步和异步(例如为了向后兼容),那么你可能会发现我最近在MSDN关于棕地异步的文章很有用。 - Stephen Cleary
1个回答

12

Task.Result只在特定的SynchronizationContext存在时才会阻塞。在控制台应用程序中,没有这样的上下文,所以继续调度在ThreadPool上。就像使用ConfigureAwait(false)时一样。

例如,在UI线程中有一个上下文,它将继续调度到单个UI线程。如果您在UI线程上同步等待Task.Result,而任务只能在UI线程上完成,则会发生死锁。

此外,死锁取决于GetByteArrayAsync的实现。仅当它是异步方法并且其等待不使用ConfigureAwait(false)时,才会发生死锁。

如果您想要的话,可以使用Stephen Cleary的AsyncContext将适当的SynchronizationContext添加到控制台应用程序中,以测试您的代码是否会在UI应用程序(或ASP.Net)中阻塞。


关于HttpClient(以及大多数.NET)返回任务的方法:它们在技术上不是异步的。它们不使用asyncawait关键字,只是返回一个任务。通常是Task.Factory.FromAsync的包装器。因此,阻止它们可能是“安全”的。


1
这很奇怪。我以为这只适用于async/await。这里没有continuation,所以我不明白会在不同的线程上安排什么。此外,如果像你所建议的那样,Result并不是阻塞的,那么这个方法返回什么?空数组? - Asad Saeeduddin
3
结果正在阻塞。它只是没有阻塞单个UI线程(因为这不是一个UI应用程序),而是阻塞主线程。 - i3arnon
1
@Asad 在 UI 应用程序(winforms、wpf)中,有一个单独的 UI 线程可以与 UI 进行交互。如果任何其他线程这样做,它将会收到异常。SC 确保连续运行在该线程上。 - i3arnon
2
关键在于,如果您在 UI 线程上调用 Result 并且不使用 ConfigureAwait(false) 调用异步方法。因此,UI 线程会阻塞在等待 UI 线程空闲之前返回的异步调用上。 - Jacob
1
@Asad UI应用程序不会以相同的方式工作。 UI应用程序可能会出现死锁,但控制台应用程序不会。 - i3arnon
显示剩余3条评论

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