Class AsyncLazy<T>
- Namespace
- Microsoft.VisualStudio.Threading
- Assembly
- Microsoft.VisualStudio.Threading.dll
A thread-safe, lazily and asynchronously evaluated value factory.
public class AsyncLazy<T>
Type Parameters
T
The type of value generated by the value factory.
- Inheritance
-
AsyncLazy<T>
- Inherited Members
Remarks
This class does not itself carry any resources needful of disposing. But the value factory may produce a value that needs to be disposed of, which is why this class carries a DisposeValueAsync() method but does not implement IDisposable.
Constructors
AsyncLazy(Func<Task<T>>, JoinableTaskFactory?)
Initializes a new instance of the AsyncLazy<T> class.
public AsyncLazy(Func<Task<T>> valueFactory, JoinableTaskFactory? joinableTaskFactory = null)
Parameters
valueFactory
Func<Task<T>>The async function that produces the value. To be invoked at most once.
joinableTaskFactory
JoinableTaskFactoryThe JoinableTaskFactory to use for avoiding deadlocks when the
valueFactory
or the constructed value's DisposeAsync() method may require the main thread in the process.
Properties
IsValueCreated
Gets a value indicating whether the value factory has been invoked.
public bool IsValueCreated { get; }
Property Value
Remarks
This returns false after a call to DisposeValue().
IsValueDisposed
Gets a value indicating whether DisposeValue() has already been called.
public bool IsValueDisposed { get; }
Property Value
IsValueFactoryCompleted
Gets a value indicating whether the value factory has been invoked and has run to completion.
public bool IsValueFactoryCompleted { get; }
Property Value
Remarks
This returns false after a call to DisposeValue().
SuppressRecursiveFactoryDetection
Gets a value indicating whether to suppress detection of a value factory depending on itself.
public bool SuppressRecursiveFactoryDetection { get; init; }
Property Value
Remarks
A value factory that truly depends on itself (e.g. by calling GetValueAsync() on the same instance) would deadlock, and by default this class will throw an exception if it detects such a condition. However this detection relies on the .NET ExecutionContext, which can flow to "spin off" contexts that are not awaited by the factory, and thus could legally await the result of the value factory without deadlocking.
When this flows improperly, it can cause InvalidOperationException to be thrown, but only when the value factory has not already been completed, leading to a difficult to reproduce race condition. Such a case can be resolved by calling SuppressRelevance() around the non-awaited fork in ExecutionContext, or the entire instance can be configured to suppress this check by setting this property to true.
When this property is set to true, the recursive factory check will not be performed, but SuppressRelevance() will still call into SuppressRelevance() if a JoinableTaskFactory was provided to the constructor.
Methods
DisposeValue()
Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
public void DisposeValue()
Remarks
This call will block on disposal (which may include construction of the value itself if it has already started but not yet finished) if it is the first call to dispose of the value.
Calling this method will put this object into a disposed state where future calls to obtain the value will throw ObjectDisposedException.
If the value has already been produced and implements IDisposable or IAsyncDisposable, it will be disposed of. If the value factory has already started but has not yet completed, its value will be disposed of when the value factory completes.
If prior calls to obtain the value are in flight when this method is called, those calls may complete and their callers may obtain the value, although Dispose() may have been or will soon be called on the value, leading those users to experience a ObjectDisposedException.
Note all conditions based on the value implementing IDisposable or IAsyncDisposable is based on the actual value, rather than the T
type argument.
This means that although T
may be IFoo
(which does not implement IDisposable), the concrete type that implements IFoo
may implement IDisposable
and thus be treated as a disposable object as described above.
DisposeValueAsync()
Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
public Task DisposeValueAsync()
Returns
- Task
A task that completes when the value has been disposed of, or immediately if the value has already been disposed of or has been scheduled for disposal by a prior call.
Remarks
Calling this method will put this object into a disposed state where future calls to obtain the value will throw ObjectDisposedException.
If the value has already been produced and implements IDisposable, IAsyncDisposable, or IAsyncDisposable it will be disposed of. If the value factory has already started but has not yet completed, its value will be disposed of when the value factory completes.
If prior calls to obtain the value are in flight when this method is called, those calls may complete and their callers may obtain the value, although Dispose() may have been or will soon be called on the value, leading those users to experience a ObjectDisposedException.
Note all conditions based on the value implementing IDisposable or IAsyncDisposable is based on the actual value, rather than the T
type argument.
This means that although T
may be IFoo
(which does not implement IDisposable), the concrete type that implements IFoo
may implement IDisposable
and thus be treated as a disposable object as described above.
GetValue()
Gets the lazily computed value.
public T GetValue()
Returns
- T
The lazily constructed value.
Exceptions
- InvalidOperationException
Thrown when the value factory calls GetValueAsync() on this instance.
GetValue(CancellationToken)
Gets the lazily computed value.
public T GetValue(CancellationToken cancellationToken)
Parameters
cancellationToken
CancellationTokenA token whose cancellation indicates that the caller no longer is interested in the result. Note that this will not cancel the value factory (since other callers may exist). But when this token is canceled, the caller will experience an OperationCanceledException immediately and a dis-joining of any JoinableTask that may have occurred as a result of this call.
Returns
- T
The lazily constructed value.
Exceptions
- InvalidOperationException
Thrown when the value factory calls GetValueAsync() on this instance.
- OperationCanceledException
Thrown when
cancellationToken
is canceled before the value is computed.
GetValueAsync()
Gets the task that produces or has produced the value.
public Task<T> GetValueAsync()
Returns
- Task<T>
A task whose result is the lazily constructed value.
Exceptions
- InvalidOperationException
Thrown when the value factory calls GetValueAsync() on this instance.
- ObjectDisposedException
Thrown after DisposeValue() is called.
GetValueAsync(CancellationToken)
Gets the task that produces or has produced the value.
public Task<T> GetValueAsync(CancellationToken cancellationToken)
Parameters
cancellationToken
CancellationTokenA token whose cancellation indicates that the caller no longer is interested in the result. Note that this will not cancel the value factory (since other callers may exist). But this token will result in an expediant cancellation of the returned Task, and a dis-joining of any JoinableTask that may have occurred as a result of this call.
Returns
- Task<T>
A task whose result is the lazily constructed value.
Exceptions
- InvalidOperationException
Thrown when the value factory calls GetValueAsync() on this instance.
- ObjectDisposedException
Thrown after DisposeValue() is called.
SuppressRelevance()
Marks the code that follows as irrelevant to the receiving AsyncLazy<T> value factory.
public AsyncLazy<T>.RevertRelevance SuppressRelevance()
Returns
- AsyncLazy<T>.RevertRelevance
A value to dispose of to restore relevance into the value factory.
Remarks
In some cases asynchronous work may be spun off inside a value factory. When the value factory does not require this work to finish before the value factory can complete, it can be useful to use this method to mark that code as irrelevant to the value factory. In particular, this can be necessary when the spun off task may actually include code that may itself await the completion of the value factory itself. Such a situation would lead to an InvalidOperationException being thrown from GetValueAsync(CancellationToken) if the value factory has not completed already, which can introduce non-determinstic failures in the program.
A using
block around the spun off code can help your program achieve reliable behavior, as shown below.
class MyClass {
private readonly AsyncLazy<int> numberOfApples;
public MyClass() {
this.numberOfApples = new AsyncLazy<int>(async delegate {
// We have some fire-and-forget code to run.
// This is *not* relevant to the value factory, which is allowed to complete without waiting for this code to finish.
using (this.numberOfApples.SuppressRelevance()) {
this.FireOffNotificationsAsync();
}
// This code is relevant to the value factory, and must complete before the value factory can complete.
return await this.CountNumberOfApplesAsync();
});
}
public event EventHandler? ApplesCountingHasBegun;
public async Task<int> GetApplesCountAsync(CancellationToken cancellationToken) {
return await this.numberOfApples.GetValueAsync(cancellationToken);
}
private async Task<int> CountNumberOfApplesAsync() {
await Task.Delay(1000);
return 5;
}
private async Task FireOffNotificationsAsync() {
// This may call to 3rd party code, which may happen to call back into GetApplesCountAsync (and thus into our AsyncLazy instance),
// but such calls should *not* be interpreted as value factory reentrancy. They should just wait for the value factory to finish.
// We accomplish this by suppressing relevance of the value factory while this code runs (see the caller of this method above).
this.ApplesCountingHasBegun?.Invoke(this, EventArgs.Empty);
}
}
If the AsyncLazy<T> was created with a JoinableTaskFactory, this method also calls SuppressRelevance() on the Context associated with that factory.
ToString()
Renders a string describing an uncreated value, or the string representation of the created value.
public override string ToString()