使用 PublicationOnly 的异步惰性死锁

Deadlock on Async lazy with PublicationOnly

假设以下代码

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 按照我的逻辑工作?

实际上我发现没有简单的方法可以做到这一点,但是 .net 中的功能请求讨论 github 给出了当前情况的完整描述

GitHub: Add async support to System.Lazy

实际上从上一个@StephenCleary 开始的第 5 点回答了我关于异常缓存的问题

  1. Resetting on exceptions is also an important use case. I've addressed this by adding another async lazy flag that will reset the AsyncLazy to an uninitialized state if an exception is thrown from the delegate. All existing accessors see the exception, but the next accessor will retry the delegate.

这个问题实际上与Lazy<T>无关。死锁是因为它是 blocking on asynchronous code.

在此代码中:

private TimeSpan GetDataAsync1()
{
  return GetTS().ConfigureAwait(false).GetAwaiter().GetResult();
}

ConfigureAwait(false)什么都不做。 ConfigureAwait 配置 awaits,而不是 tasks,那里没有 await

最好的选择是去 async all the way. If exceptions are a concern, you can use AsyncLazy<T> 并通过 AsyncLazyFlags.RetryOnFailure

如果您不能一直 async ,下一个最佳选择是一直同步。如果您不能执行其中任何一项,那么您将不得不选择 sync-over-async hack;请注意,没有适用于所有情况的技巧。