【发布时间】:2019-06-01 06:55:51
【问题描述】:
我需要一个简单的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的使用。
如果初始化方法在任何线程上抛出异常,则 异常从该线程的 Value 属性中传播出来。这 异常没有被缓存。
然后我按以下方式调用它。
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) { }
【问题讨论】:
-
您包含 Ivan 答案的最新编辑已更改代码,使其行为方式与现在相同。您应该使用嵌套的
async函数保持代码原样,而父函数GetNumbersAsync没有async关键字——这将在GetNumbersAsync停止缓存,允许异常冒泡。您需要 C# 7 才能使用嵌套函数。 -
查看the following SharpLab code 了解嵌套函数代码是如何编译的..
-
感谢@spender,该库看起来很有希望,直到您发现它似乎不支持禁用异常缓存。也希望避免再次引用另一个库,但开始接受我可能无法避免它。 :-(
-
这个库github.com/StephenCleary/AsyncEx 似乎可以正确处理异常重试。微软版本似乎不处理重试github.com/microsoft/vs-threading/blob/master/src/…。如果我可以只使用 Lazy
但看起来越来越不可能,那就太好了。
标签: c# .net asynchronous lazy-loading lazy-evaluation