如何进行惰性注入的异步初始化

How to perform async initalization of lazy injection

假设我们想要注入一个创建成本很高的对象(假设它从数据库进行初始化),所以我们通常会使用某种工厂或 Lazy<T>。但是,如果我们将此对象注入到使用异步操作方法的 MVC 或 WebApi 控制器中,我们不想在初始化 Lazy 对象时在昂贵的 I/O 操作上阻止这些方法,这会破坏使用异步的目的。

当然,我可以创建一个 "initlize" 异步方法,但这违反了许多原则。

以惰性和异步方式访问和初始化注入对象的最佳选择是什么?

最简单的方法是让你注入的东西是 Lazy<Task<T>>,工厂看起来类似于

private Lazy<Task<Foo>> LazyFooFactory()
{
    return new Lazy<Task<Foo>>(InitFoo);
}

private async Task<Foo> InitFoo()
{
    //Other code as needed
    Foo result = await SomeSlowButAsyncronousCodeToGetFoo();
    //Other code as needed
    return result;
}

用作以下

private readonly Lazy<Task<Foo>> _lazyFoo

public SomeClass(Lazy<Task<Foo>> lazyFoo)
{
    _lazyFoo = lazyFoo;
}

public async Task SomeFunc()
{
    //This should return quickly once the task as been started the first call
    // then will use the existing Task for subsequent calls.
    Task<Foo> fooTask = _lazyFoo.Value; 

    //This awaits for SomeSlowButAsyncronousCodeToGetFoo() to finish the first calling
    // then will used the existing result to return immediately for subsequent calls.
    var foo = await fooTask;

    DoStuffWithFoo(foo);
}

直到第一次调用 _lazyFoo.Value 才会调用函数 SomeSlowButAsyncronousCodeToGetFoo(),后续调用将使用现有的 Task.Result 值并且不会重新调用工厂。

作为对象图的一部分并由容器自动连接的所有组件都应该非常轻量级地创建,因为 injection constructors should be simple。任何运行时数据或创建成本高昂的一次性数据都不应直接注入到作为对象图一部分的组件的构造函数中。异步甚至夸大了这一点,因为构造函数永远不可能是异步的;您不能在构造函数的主体中使用 await。

因此,如果一个组件依赖于一些昂贵的数据来创建数据,则应该在构造函数之外延迟加载这些数据。这样,对象图的构建变得快速,并且控制器的创建不会被阻止。

正如@ScottChamberlain 所说,最好的方法可能是将 Lazy<T>Task<T> 混合成为 Lazy<Task<T>>。如果将此 Lazy<Task<T>> 注入到 'expensive component' 的构造函数中(使该组件本身再次轻量级),您可能会获得最佳结果。这有一些明显的好处:

  • 以前昂贵的对象本身变得简单;它不再负责数据的加载。
  • 对象图变得快速且可验证。
  • 将加载策略从惰性加载更改为后台加载变得很容易,无需更改系统中的任何内容。

作为最后一点的例子,让数据在后台加载可以简单地做如下:

Task<ExpensiveData> data = Task.Run(LoadExpensiveData);

container.RegisterSingleton<ISomeClass>(
    new SomeClass(new Lazy<Task<ExpensiveData>>(() => data)));

有几个不正确的假设应该得到解决。

... object that is expensive to create (let's say it does initialization from a database), so we would typically use some kind of factory or Lazy<T>.

这并不完全正确。 Lazy<T> 不会让成本消失。对象图的物化是请求周期的一部分,所以它迟早会发生。 Lazy<T> 只是推迟成本直到 Value 被评估。它仅在您可能不需要评估它的情况下有用。不是 可选的 依赖项——您可能只是有条件地需要它,但如果您需要,您就需要。

However, if we're ... using async action methods, we don't want to block these methods on the expensive I/O operation when the Lazy object is initialized, which defeats the purpose of using async.

但这不是使用 async 的目的。 await 就像它在锡罐上所说的那样:您的方法将 等待 等待任务完成。它阻塞的是执行线程,在CPU是"pinged"异步工作完成之前,它可以自由地做其他工作( There Is No Thread).

总结:Lazy<T> 用于条件要求的昂贵依赖项。 Task(与 async 一起使用)用于协调昂贵的 I/O-bound(非 CPU 绑定)执行,以便 CPU 线程不会被阻塞。如果您需要将它们结合起来,您会得到 Lazy<Task<T>>,正如其他回答者所建议的那样。

code is semantically correct, 更简洁。结合(我希望)这两种美德:

Task<ExpensiveData> LoadExpensiveData() {...}

container.RegisterSingleton<ISomeClass>(
  new SomeClass(new Lazy<Task<ExpensiveData>>(() => LoadExpensiveData())));

...
public class Dependent 
{
  public Dependent(Lazy<Task<ExpensiveData>> expensivezz) {...}
  ...
}

由于将昂贵的函数本身分配给 Lazy<Task<...>>,因此 "expense" 不会产生,除非 await expensivezz.Value 被评估。在等待期间,同一线程将被释放以服务于其他代码。

推论:

  • 如果你只需要条件依赖,就用Lazy<T>
  • 如果您只需要避免昂贵的解决方案阻止对象图的构造(您确实做到了!图构造应遵循与甚至不允许异步的对象构造相同的语义),那么只需使用 Task
  • 因为大多数 "expensive" 操作是 I/O-bound,而 async Task 现在是 "the way" 来处理这些,很可能 Lazy<T> 意味着 Lazy<Task<T>>.