为啥不调用 Task<T>.Result 死锁?

Posted

技术标签:

【中文标题】为啥不调用 Task<T>.Result 死锁?【英文标题】:Why doesn't calling Task<T>.Result deadlock?为什么不调用 Task<T>.Result 死锁? 【发布时间】:2015-07-06 23:21:54 【问题描述】:

几个月前阅读this 的帖子后,我开始偏执地想要得到Task&lt;T&gt;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! 3 次,然后退出。这是HttpClient 特有的,可以安全地调用Result(因为写它的人已经用ConfigureAwait(false) 填充了它)?

【问题讨论】:

顺便说一句.. 在需要的地方不阻塞和使用 await 比在任何地方使用 ConfigureAwait 要简单得多。 @i3arnon 没错,但我正在编写一个支持同步和异步调用者的 API。 @JamesKo:我建议异步方法公开异步 API。但是,如果您必须同时支持同步和异步(例如,为了向后兼容),那么您可能会发现我最近的 MSDN article on brownfield async 很有用。 【参考方案1】:

Task.Result 只会在某些SynchronizationContexts 存在时阻止。在控制台应用程序中没有一个,因此在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 的包装器。所以无论如何它可能是“安全的”阻止它们。

【讨论】:

这很奇怪。我认为这仅适用于异步/等待。这里没有继续,所以我不明白在不同的线程上会安排什么。此外,如果Result 没有 阻塞,正如您所建议的,返回的方法是什么?空数组? @Asad 结果被阻止。它只是不阻塞单个 UI 线程(因为这不是 UI 应用程序)。它阻塞了主线程。 @i3arnon 不清楚您在说什么 UI 线程。我的观点是,Result 将始终处于阻塞状态,无论可用的同步上下文如何。如果该方法返回某事物的Task,则必须在Task.Result 的getter 向您返回值之前解决该事物。至少在我看来,这是在 UI 应用程序还是控制台应用程序中运行并不重要。 @Asad 在 UI 应用程序(winforms、wpf)中有一个可以与 UI 交互的 UI 线程。如果任何其他线程这样做,它将获得异常。 SC 确保继续在该线程上运行。 这里的关键是,如果您在 UI 线程上的异步方法上调用 Result 并且不使用 ConfigureAwait(false)。因此,UI 线程阻塞了异步调用,该调用正在等待 UI 线程在返回之前释放。

以上是关于为啥不调用 Task<T>.Result 死锁?的主要内容,如果未能解决你的问题,请参考以下文章

为啥要在 C# 中使用 Task<T> 而不是 ValueTask<T>?

为啥使用 IAsyncEnumerable 比返回 async/await Task<T> 慢?

使用 IMemoryCache 缓存结果 Task<T> 的正确方法

防止针对 Task<t> 进行单元测试

为啥我不能在我的 ArrayList<T> 上调用 Collections.sort()?

如果 Task<HttpResponseMessage>.IsCompleted 为真,为啥 Task<HttpResponseMessage>.Result 会抛出异常?