调用异步委托时防止 Lazy<T> 缓存异常

Max*_*ich 5 .net c# asynchronous lazy-loading lazy-evaluation

我需要一个简单的AsyncLazy<T>,它的行为完全相同,Lazy<T>但正确支持处理异常并避免缓存它们。

具体我遇到的问题如下:

我可以这样写一段代码:

public class TestClass
{
    private int i = 0;

    public TestClass()
    {
        this.LazyProperty = new Lazy<string>(() =>
        {
            if (i == 0)
                throw new Exception("My exception");

            return "Hello World";

        }, LazyThreadSafetyMode.PublicationOnly);
    }

    public void DoSomething()
    {
        try
        {
            var res = this.LazyProperty.Value;
            Console.WriteLine(res);
            //Never gets here
        }
        catch { }
        i++;       
        try
        {
            var res1 = this.LazyProperty.Value;
            Console.WriteLine(res1);
            //Hello World
        }
        catch { }

    }

    public Lazy<string> LazyProperty { get; }

}
Run Code Online (Sandbox Code Playgroud)

注意LazyThreadSafetyMode.PublicationOnly的使用。

如果初始化方法在任何线程上引发异常,则该异常将传播到该线程的 Value 属性之外。不缓存异常。

然后我通过以下方式调用它。

TestClass _testClass = new TestClass();
_testClass.DoSomething();
Run Code Online (Sandbox Code Playgroud)

它的工作方式与您预期的完全一样,其中第一个结果因发生异常而被省略,结果保持未缓存状态,随后尝试读取该值成功返回“Hello World”。

不幸的是,如果我将代码更改为这样的:

public Lazy<Task<string>> AsyncLazyProperty { get; } = new Lazy<Task<string>>(async () =>
{
    if (i == 0)
        throw new Exception("My exception");

    return await Task.FromResult("Hello World");
}, LazyThreadSafetyMode.PublicationOnly);
Run Code Online (Sandbox Code Playgroud)

代码在第一次被调用时失败,随后对该属性的调用被缓存(因此永远无法恢复)。

这在某种程度上是有道理的,因为我怀疑异常实际上从未在任务之外冒泡,但是我无法确定的是一种通知Lazy<T>任务/对象初始化失败并且不应缓存的方法。

任何人都能够提供任何意见?

编辑:

感谢您的回答伊万。我已经成功地获得了一个包含您的反馈的基本示例,但事实证明我的问题实际上比上面演示的基本示例更复杂,毫无疑问,这个问题会影响处于类似情况的其他人。

因此,如果我将我的财产签名更改为这样的内容(根据 Ivans 的建议)

this.LazyProperty = new Lazy<Task<string>>(() =>
{
    if (i == 0)
        throw new NotImplementedException();

    return DoLazyAsync();
}, LazyThreadSafetyMode.PublicationOnly);
Run Code Online (Sandbox Code Playgroud)

然后像这样调用它。

await this.LazyProperty.Value;
Run Code Online (Sandbox Code Playgroud)

代码有效。

但是,如果您有这样的方法

this.LazyProperty = new Lazy<Task<string>>(() =>
{
    return ExecuteAuthenticationAsync();
}, LazyThreadSafetyMode.PublicationOnly);
Run Code Online (Sandbox Code Playgroud)

然后它本身调用另一个异步方法。

private static async Task<AccessTokenModel> ExecuteAuthenticationAsync()
{
    var response = await AuthExtensions.AuthenticateAsync();
    if (!response.Success)
        throw new Exception($"Could not authenticate {response.Error}");

    return response.Token;
}
Run Code Online (Sandbox Code Playgroud)

懒惰缓存bug再次出现,问题可以重现。

这是重现问题的完整示例:

this.AccessToken = new Lazy<Task<string>>(() =>
{
    return OuterFunctionAsync(counter);
}, LazyThreadSafetyMode.PublicationOnly);

public Lazy<Task<string>> AccessToken { get; private set; }

private static async Task<bool> InnerFunctionAsync(int counter)
{
    await Task.Delay(1000);
    if (counter == 0)
        throw new InvalidOperationException();
    return false;
}

private static async Task<string> OuterFunctionAsync(int counter)
{
    bool res = await InnerFunctionAsync(counter);
    await Task.Delay(1000);
    return "12345";
}

try
{
    var r = await this.AccessToken.Value;
}
catch (Exception ex) { }

counter++;

try
{
    //Retry is never performed, cached task returned.
    var r1 = await this.AccessToken.Value;

}
catch (Exception ex) { }
Run Code Online (Sandbox Code Playgroud)

Ste*_*ary 5

问题是如何Lazy<T>定义“失败”会干扰如何Task<T>定义“失败”。

对于Lazy<T>“失败”的初始化,它必须引发异常。这是完全自然和可接受的,尽管它是隐式同步的。

对于Task<T>“失败”,异常被捕获并放置在任务上。这是异步代码的正常模式。

两者结合会导致问题。该Lazy<T>部分Lazy<Task<T>>只是将“失败”,如果异常直接拉高,而async格局Task<T>不会直接传播异常。所以async工厂方法总是会(同步地)“成功”,因为它们返回一个Task<T>. 到这Lazy<T>一步其实已经完成了;它的值已生成(即使Task<T>尚未完成)。

您可以轻松构建自己的AsyncLazy<T>类型。您不必仅针对一种类型依赖 AsyncEx:

public sealed class AsyncLazy<T>
{
  private readonly object _mutex;
  private readonly Func<Task<T>> _factory;
  private Lazy<Task<T>> _instance;

  public AsyncLazy(Func<Task<T>> factory)
  {
    _mutex = new object();
    _factory = RetryOnFailure(factory);
    _instance = new Lazy<Task<T>>(_factory);
  }

  private Func<Task<T>> RetryOnFailure(Func<Task<T>> factory)
  {
    return async () =>
    {
      try
      {
        return await factory().ConfigureAwait(false);
      }
      catch
      {
        lock (_mutex)
        {
          _instance = new Lazy<Task<T>>(_factory);
        }
        throw;
      }
    };
  }

  public Task<T> Task
  {
    get
    {
      lock (_mutex)
        return _instance.Value;
    }
  }

  public TaskAwaiter<T> GetAwaiter()
  {
    return Task.GetAwaiter();
  }

  public ConfiguredTaskAwaitable<T> ConfigureAwait(bool continueOnCapturedContext)
  {
    return Task.ConfigureAwait(continueOnCapturedContext);
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 感谢您的回复和创建 AsyncEx!从领域专家那里得到回复总是很高兴。 (2认同)