问题
Let's say we want to inject an 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>
. However, if we're injecting this object into an MVC or WebApi controller that is 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.
Certainly, I could create an "initlize" method that is async, but that violates a number of principles.
What's the best option for accessing and initializing the injected object in a lazy and async way?
回答1:
All the components that are part of your object graph and are auto-wired by the container should be very lightweight to create, because injection constructors should be simple. Anything that is either runtime data, or one-time data that is costly to create, should not be injected directly into the constructor of a component that is part of your object graph. Async even exaggerates this, because constructors can never be asynchronous; you can't use await in the body of a constructor.
So if a component depends on some expensive to create data, this data should be loaded lazily, outside of the constructor. This way the building of the object graph becomes fast and the creation of your controllers doesn't get blocked.
So as @ScottChamberlain already said, probably the nicest way is to mix Lazy<T>
with Task<T>
to become Lazy<Task<T>>
. You would probably gain the best results if you inject this Lazy<Task<T>>
into the constructor of your 'expensive component' (making that component itself lightweight again). This has a few clear benefits:
- the previously expensive object itself becomes simple; it not responsible anymore for the loading of the data.
- the object graph becomes fast and verifiable.
- it becomes easy to change the loading strategy from lazy to background loading, without anything in the system to change.
As an example of the last point, allowing the data to be loaded in the background can be simply done as follows:
Task<ExpensiveData> data = Task.Run(LoadExpensiveData);
container.RegisterSingleton<ISomeClass>(
new SomeClass(new Lazy<Task<ExpensiveData>>(() => data)));
回答2:
The easiest way is have the thing you inject be a Lazy<Task<T>>
, The factory would look something along the lines of
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;
}
Used as the following
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);
}
The function SomeSlowButAsyncronousCodeToGetFoo()
is not called until the first calling of _lazyFoo.Value
and subsequent calls will use the existing Task.Result
value and not re-call the factory.
回答3:
There's a couple incorrect assumptions presented in question that should be addressed.
... 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>
.
This is not entirely correct. Lazy<T>
doesn't make the cost go away. Materialization of the object graph is part of the request cycle, so it happens sooner or later. Lazy<T>
just defers the cost until Value
is evaluated. It is only useful in situations where you may not need to evaluate it. Not an optional dependency--you may only conditionally need it, but if you do, you do.
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.
But that is not the purpose of using async
. await
does just what it says on the tin: your method will wait for the awaited task to complete. What it doesn't block is the execution thread, which is freed to do other work in other until the CPU is "pinged" that the asynchronous work is completed (There Is No Thread).
Summing up: Lazy<T>
is for conditionally-required expensive dependencies. Task
(as used with async
) is for coordinating expensive I/O-bound (not-CPU-bound) execution so that CPU threds are not blocked. If you need to combine those, you get Lazy<Task<T>>
, as other answerers have suggested.
Scott's code is semantically correct, Steven's more succinct. Combining for (I hope) both virtues:
Task<ExpensiveData> LoadExpensiveData() {...}
container.RegisterSingleton<ISomeClass>(
new SomeClass(new Lazy<Task<ExpensiveData>>(() => LoadExpensiveData())));
...
public class Dependent
{
public Dependent(Lazy<Task<ExpensiveData>> expensivezz) {...}
...
}
Thanks to assigning the expensive function itself to Lazy<Task<...>>
the "expense" will not be incurred until and unless await expensivezz.Value
is evaluated. While waiting, the same thread will be freed to service other code.
Corollaries:
- If you only need conditional dependency, just use
Lazy<T>
. - If you only need to keep an expensive resolution from blocking construction of your object graph (and you do! graph construction should follow the same semantic as object construction which doesn't even allow for async) then just use
Task
. - Because most "expensive" operations are I/O-bound, and
async Task
s are now "the way" to handle these, it is probable thatLazy<T>
impliesLazy<Task<T>>
.
来源:https://stackoverflow.com/questions/32512266/how-to-perform-async-initalization-of-lazy-injection