c# - 调用异步委托(delegate)时防止 Lazy<T> 缓存异常

标签 c# .net 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; }

}

注意 LazyThreadSafetyMode.PublicationOnly 的使用.

If the initialization method throws an exception on any thread, the exception is propagated out of the Value property on that thread. The exception is not cached.

然后我按以下方式调用它。

TestClass _testClass = new TestClass();
_testClass.DoSomething();

它的工作方式与您预期的完全一样,第一个结果由于发生异常而被省略,结果保持未缓存状态,随后读取该值的尝试成功返回“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);

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

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

有人能提供任何意见吗?

编辑:

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

所以如果我将我的属性签名更改为这样的东西(根据 Ivans 的建议)

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

    return DoLazyAsync();
}, LazyThreadSafetyMode.PublicationOnly);

然后像这样调用它。

await this.LazyProperty.Value;

代码有效。

但是如果你有这样的方法

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

然后它自己调用另一个 Async 方法。

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;
}

延迟缓存错误再次出现,问题可以重现。

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

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) { }

最佳答案

问题是如何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);
  }
}

关于c# - 调用异步委托(delegate)时防止 Lazy<T> 缓存异常,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56404540/

相关文章:

.net - 用自定义异步计时器类替换 Threading.Timer?

javascript - 函数以错误的顺序执行

c# - 关于 Facebook SDK 的问题。 Facebook 图片 URL 每次登录都会更改吗?如何在 Unity C# 中从 Facebook SDK 7.9 获取图像 URL 和 Facebook ID?

c# - 如何正确解析具有任意 namespace 的 XML 文档

c# - 用于连接到 Bugzilla 的 .NET API

c# - 检索具有指定类型的字段的值

grails - Grails 中的异步编程

c# - For 循环未在整个循环持续时间内运行

c# - 观察新插入的 SQL 表的有效方法

.net - 什么 .NET 字典支持 "find nearest key"操作?