如何进行懒惰注射的异步初始化

时间:2015-09-10 21:55:07

标签: c# asynchronous dependency-injection simple-injector

让我们说我们想要注入一个昂贵的对象(让我们说它从数据库初始化),所以我们通常会使用某种工厂或Lazy<T> 。但是,如果我们将此对象注入使用异步操作方法的MVC或WebApi控制器,我们不希望在初始化Lazy对象时在昂贵的I / O操作上阻止这些方法,打败了使用异步的目的。

当然,我可以创建一个&#34; initlize&#34;异步的方法,但违反了许多原则。

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

3 个答案:

答案 0 :(得分:7)

最简单的方法是让你注入的东西是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);
}

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

答案 1 :(得分:4)

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

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

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

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

作为最后一点的一个例子,允许在后台加载数据可以简单地完成如下:

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

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

答案 2 :(得分:-1)

有两个错误的假设需要解决。

  

...创建起来很昂贵的对象(例如,它是从数据库进行初始化的),因此我们通常会使用某种工厂或Lazy<T>

这并不完全正确。 Lazy<T>并不能消除成本。对象图的实现是请求周期的一部分,因此迟早会发生。 Lazy<T>仅将成本推迟到评估Value为止。 仅在不需要评估的情况下有用。不是可选依赖项-您可能仅有条件地需要它,但如果需要,则可以。

  

但是,如果我们正在使用异步操作方法,则我们不想在初始化Lazy对象时在昂贵的I / O操作上阻止这些方法,这违背了使用异步的目的。

但这不是使用async的目的。 await会按照提示说的做:您的方法将 wait 等待所要完成的任务。 阻塞的是执行线程,该线程可以自由执行其他工作,直到CPU“ ping”异步工作完成(There Is No Thread)。

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

Scott's代码在语义上是正确的,Steven's更简洁。结合(我希望)这两种美德:

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

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

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

由于将昂贵的函数本身分配给Lazy<Task<...>>,因此直到并且除非评估await expensivezz.Value才会产生“费用”。等待时,同一线程将被释放以服务其他代码。

推论:

  • 如果仅需要条件依赖,则只需使用Lazy<T>
  • 如果您只需要保持昂贵的分辨率来阻止对象图的构建(并且确实如此!图构建应遵循与对象构建相同的语义,甚至不允许异步),则只需使用{{1} }。
  • 因为大多数“昂贵”的操作都是受I / O约束的,并且Task现在是处理这些操作的“方式”,所以async Task可能意味着Lazy<T>。 / li>