Skip to content

Commit

Permalink
Add AsyncLazy<T>.Dispose() method
Browse files Browse the repository at this point in the history
This addresses a common need for a class with an `AsyncLazy<T>` field where the value is disposable to ensure on the class disposal that it can dispose of the value if and when it is constructed.
  • Loading branch information
AArnott committed Aug 31, 2023
1 parent 4a97489 commit 0e383a3
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 7 deletions.
136 changes: 133 additions & 3 deletions src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ namespace Microsoft.VisualStudio.Threading;
/// A thread-safe, lazily and asynchronously evaluated value factory.
/// </summary>
/// <typeparam name="T">The type of value generated by the value factory.</typeparam>
/// <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 <see cref="DisposeValueAsync"/> method but does not implement <see cref="IDisposable"/>.
/// </remarks>
public class AsyncLazy<T>
{
/// <summary>
Expand All @@ -20,6 +25,11 @@ public class AsyncLazy<T>
/// </summary>
private static readonly object RecursiveCheckSentinel = new object();

/// <summary>
/// A value set on the <see cref="value"/> field when this object is disposed.
/// </summary>
private static readonly Task<T> DisposedSentinel = Task.FromException<T>(new ObjectDisposedException(nameof(AsyncLazy<T>)));

/// <summary>
/// The object to lock to provide thread-safety.
/// </summary>
Expand Down Expand Up @@ -65,34 +75,50 @@ public AsyncLazy(Func<Task<T>> valueFactory, JoinableTaskFactory? joinableTaskFa
/// <summary>
/// Gets a value indicating whether the value factory has been invoked.
/// </summary>
/// <remarks>
/// This returns <see langword="false" /> after a call to <see cref="DisposeValue"/>.
/// </remarks>
public bool IsValueCreated
{
get
{
// This is carefully written to interact well with the DisposeValueAsync method
// without requiring a lock here.
bool result = Volatile.Read(ref this.valueFactory) is null;
Interlocked.MemoryBarrier();
return this.valueFactory is null;
result &= Volatile.Read(ref this.value) != DisposedSentinel;
return result;
}
}

/// <summary>
/// Gets a value indicating whether the value factory has been invoked and has run to completion.
/// </summary>
/// <remarks>
/// This returns <see langword="false" /> after a call to <see cref="DisposeValue"/>.
/// </remarks>
public bool IsValueFactoryCompleted
{
get
{
Interlocked.MemoryBarrier();
return this.value is object && this.value.IsCompleted;
Task<T>? value = Volatile.Read(ref this.value);
return value is object && value.IsCompleted && value != DisposedSentinel;
}
}

/// <summary>
/// Gets a value indicating whether <see cref="DisposeValue"/> has already been called.
/// </summary>
public bool IsValueDisposed => Volatile.Read(ref this.value) == DisposedSentinel;

/// <summary>
/// Gets the task that produces or has produced the value.
/// </summary>
/// <returns>A task whose result is the lazily constructed value.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown when the value factory calls <see cref="GetValueAsync()"/> on this instance.
/// </exception>
/// <exception cref="ObjectDisposedException">Thrown after <see cref="DisposeValue"/> is called.</exception>
public Task<T> GetValueAsync() => this.GetValueAsync(CancellationToken.None);

/// <summary>
Expand All @@ -108,6 +134,7 @@ public bool IsValueFactoryCompleted
/// <exception cref="InvalidOperationException">
/// Thrown when the value factory calls <see cref="GetValueAsync()"/> on this instance.
/// </exception>
/// <exception cref="ObjectDisposedException">Thrown after <see cref="DisposeValue"/> is called.</exception>
public Task<T> GetValueAsync(CancellationToken cancellationToken)
{
if (!((this.value is object && this.value.IsCompleted) || this.recursiveFactoryCheck.Value is null))
Expand Down Expand Up @@ -234,6 +261,109 @@ public T GetValue(CancellationToken cancellationToken)
}
}

/// <summary>
/// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
/// </summary>
/// <remarks>
/// <para>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.</para>
/// <para>Calling this method will put this object into a disposed state where future calls to obtain the value will throw <see cref="ObjectDisposedException"/>.</para>
/// <para>If the value has already been produced and implements <see cref="IDisposable"/> or <see cref="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.</para>
/// <para>If prior calls to obtain the value are in flight when this method is called, those calls <em>may</em> complete and their callers may obtain the value, although <see cref="IDisposable.Dispose"/>
/// may have been or will soon be called on the value, leading those users to experience a <see cref="ObjectDisposedException"/>.</para>
/// <para>Note all conditions based on the value implementing <see cref="IDisposable"/> or <see cref="IAsyncDisposable"/> is based on the actual value, rather than the <typeparamref name="T"/> type argument.
/// This means that although <typeparamref name="T"/> may be <c>IFoo</c> (which does not implement <see cref="IDisposable"/>), the concrete type that implements <c>IFoo</c> may implement <see cref="IDisposable"/>
/// and thus be treated as a disposable object as described above.</para>
/// </remarks>
public void DisposeValue()
{
if (!this.IsValueDisposed)
{
if (this.jobFactory is JoinableTaskFactory jtf)
{
jtf.Run(this.DisposeValueAsync);
}
else
{
this.DisposeValueAsync().GetAwaiter().GetResult();
}
}
}

/// <summary>
/// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
/// </summary>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// <para>Calling this method will put this object into a disposed state where future calls to obtain the value will throw <see cref="ObjectDisposedException"/>.</para>
/// <para>If the value has already been produced and implements <see cref="IDisposable"/>, <see cref="IAsyncDisposable"/>, or <see cref="System.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.</para>
/// <para>If prior calls to obtain the value are in flight when this method is called, those calls <em>may</em> complete and their callers may obtain the value, although <see cref="IDisposable.Dispose"/>
/// may have been or will soon be called on the value, leading those users to experience a <see cref="ObjectDisposedException"/>.</para>
/// <para>Note all conditions based on the value implementing <see cref="IDisposable"/> or <see cref="IAsyncDisposable"/> is based on the actual value, rather than the <typeparamref name="T"/> type argument.
/// This means that although <typeparamref name="T"/> may be <c>IFoo</c> (which does not implement <see cref="IDisposable"/>), the concrete type that implements <c>IFoo</c> may implement <see cref="IDisposable"/>
/// and thus be treated as a disposable object as described above.</para>
/// </remarks>
public async Task DisposeValueAsync()
{
Task<T>? localValueTask = null;
object? localValue = default;
lock (this.syncObject)
{
if (this.value == DisposedSentinel)
{
return;
}

switch (this.value?.Status)
{
case TaskStatus.RanToCompletion:
// We'll dispose of the value inline, outside the lock.
localValue = this.value.Result;
break;
case TaskStatus.Faulted:
case TaskStatus.Canceled:
// Nothing left to do.
break;
default:
// We'll schedule the value for disposal outside the lock so it can be synchronous with the value factory,
// but will not execute within our lock.
localValueTask = this.value;
break;
}

// Shut out all future callers from obtaining the value.
this.value = DisposedSentinel;

// We want value to be set before valueFactory is cleared so that IsValueCreated never returns true incorrectly.
Interlocked.MemoryBarrier();

// Release associated memory.
this.joinableTask = null;
this.valueFactory = null;
}

if (localValueTask is not null)
{
localValue = await localValueTask.ConfigureAwait(false);
}

if (localValue is System.IAsyncDisposable systemAsyncDisposable)
{
await systemAsyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else if (localValue is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else if (localValue is IDisposable disposable)
{
disposable.Dispose();
}
}

/// <summary>
/// Renders a string describing an uncreated value, or the string representation of the created value.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.IsAbandoned.get -> bool
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.IsAbandoned.get -> bool
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.IsAbandoned.get -> bool
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser.IsAbandoned.get -> bool
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Loading

0 comments on commit 0e383a3

Please sign in to comment.