异步懒加载使用PublicationOnly时出现死锁

4
假设以下代码:
public class ValuesController : ApiController
{
    // GET api/values
    public IEnumerable<string> Get()
    {
        Lazy<TimeSpan> lm = new Lazy<TimeSpan>(GetDataAsync1, System.Threading.LazyThreadSafetyMode.PublicationOnly);

        return new string[] { "value1", "value2", lm.Value.ToString() };
    }

    private TimeSpan GetDataAsync1()
    {

        return GetTS().ConfigureAwait(false).GetAwaiter().GetResult();

    }

    // I Cant change this method, and what is inside it...
    private async Task<TimeSpan> GetTS()
    {
        var sw = Stopwatch.StartNew();

        using (var client = new HttpClient())
        {
            var result = await client.GetAsync("https://www.google.com/");
        }

        sw.Stop();
        return sw.Elapsed;
    }
}

我的问题是从远程服务器获取一些数据,并想要将其缓存以供以后使用。由于在某个点上远程服务器可能会失败,我不想缓存异常,而只想缓存成功的结果... 因此,对于我来说保持等待值的方式不可行。

// Cant use this, because this caches failed exception as well
Lazy<Task...> lz = ...
await lz.Value

但是上面的代码片段,如预期所示会导致死锁,鉴于我无法更改GetTS函数,是否有可能强制Lazy与我的逻辑一起工作?


2
这个 GetTS().ConfigureAwait(false).GetAwaiter().GetResult() 会导致死锁! - Nick
永远不要在 Task 上调用 GetResult()。相反,应该使用 await - Alejandro
@canton7,理论上是的,那我就不需要懒惰了吗? - Arsen Mkrtchyan
正确,但是你当然需要自己的锁定。 - canton7
1
你可能会觉得这很有趣:强制异步方法只被调用一次。它包括了一个不使用Lazy<T>类的实现。 - Theodor Zoulias
显示剩余8条评论
2个回答

3
这个问题实际上与 Lazy<T> 无关。死锁是因为它在 阻塞异步代码 上。
在这段代码中:
private TimeSpan GetDataAsync1()
{
  return GetTS().ConfigureAwait(false).GetAwaiter().GetResult();
}

ConfigureAwait(false)不起作用。 ConfigureAwait 配置的是等待,而不是任务,因此没有await

最好的选择是完全使用async。 如果异常是一个问题,可以使用AsyncLazy<T>并传递AsyncLazyFlags.RetryOnFailure

如果无法完全使用async,则下一个最佳选项是完全同步。 如果两者都无法实现,则必须选择同步覆盖异步的方法;请注意,并非所有情况下都有可行的方法。


感谢@StephenCleary,这真的很有道理。我不知道AsyncLazyFlags.RetryOnFailure,这正是我想要的。我明白问题不在于Lazy侧,而是并非所有的FCL类都适合用于async工作。 - Arsen Mkrtchyan

1

实际上,我没有找到简单的方法来做到这一点,但是在 .net github 的功能请求讨论中提供了当前情况的完整图片。

GitHub: Add async support to System.Lazy

实际上,最后第五点 @StephenCleary 的回答解决了我的异常缓存问题。

  1. 重置异常也是一个重要的用例。我通过添加另一个异步延迟标志来解决这个问题,如果从委托中抛出异常,则将 AsyncLazy 重置为未初始化状态。所有现有的访问器都会看到异常,但下一个访问器将重试委托。

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