调用异步委托时

时间:2019-06-01 06:55:52

标签: 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

  

如果初始化方法在任何线程上引发异常,则   异常从该线程的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) { }

2 个答案:

答案 0 :(得分:4)

为了帮助您了解这里发生的事情,有一个简单的程序:

static void Main()
{
    var numberTask = GetNumberAsync( 0 );

    Console.WriteLine( numberTask.Status );
    Console.ReadLine();
}


private static async Task<Int32> GetNumberAsync( Int32 number )
{
    if ( number == 0 )
        throw new NotSupportedException();

    await Task.Delay( 1000 );

    return number;
}

尝试一下,您将看到该程序的输出为Faulted。该方法始终返回结果,即Task捕获了异常。

为什么会发生捕获?由于该方法的async修饰符而发生。在幕后,该方法的实际执行使用AsyncMethodBuilder捕获异常并将其设置为任务的结果。

我们该如何更改?

private static Task<Int32> GetNumberAsync( Int32 number )
{
    if ( number == 0 )
        throw new NotSupportedException();

    return GetNumberReallyAsync();

    async Task<Int32> GetNumberReallyAsync()
    {
        await Task.Delay( 1000 );

        return number;
    }
}

在此示例中,您可以看到该方法没有async修饰符,因此该异常未被捕获为错误的Task。

因此,要使您的示例按需运行,您需要删除异步并等待:

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

    return Task.FromResult("Hello World");
}, LazyThreadSafetyMode.PublicationOnly);

答案 1 :(得分:3)

问题是Lazy<T>定义“失败”的方式与Task<T>定义“失败”的方式相冲突。

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

要使Task<T>失败,将捕获异常并将其放置在任务上。这是异步代码的正常模式。

将两者结合会导致问题。如果直接引发异常,并且Lazy<T>的{​​{1}}模式不会直接传播异常,则Lazy<Task<T>>的{​​{1}}部分将“失败”。因此async工厂方法将总是(同步)“成功”,因为它们返回了Task<T>。至此async部分已经完成;其值已生成(即使Task<T>尚未完成)。

您可以轻松构建自己的Lazy<T>类型。您不必仅依赖于那种类型的AsyncEx:

Task<T>