diff --git a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
index 55a9448cc..057d4621a 100644
--- a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
+++ b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
@@ -12,6 +12,11 @@ namespace Microsoft.VisualStudio.Threading;
/// A thread-safe, lazily and asynchronously evaluated value factory.
///
/// The type of value generated by the value factory.
+///
+/// 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 method but does not implement .
+///
public class AsyncLazy
{
///
@@ -20,6 +25,11 @@ public class AsyncLazy
///
private static readonly object RecursiveCheckSentinel = new object();
+ ///
+ /// A value set on the field when this object is disposed.
+ ///
+ private static readonly Task DisposedSentinel = Task.FromException(new ObjectDisposedException(nameof(AsyncLazy)));
+
///
/// The object to lock to provide thread-safety.
///
@@ -65,27 +75,42 @@ public AsyncLazy(Func> valueFactory, JoinableTaskFactory? joinableTaskFa
///
/// Gets a value indicating whether the value factory has been invoked.
///
+ ///
+ /// This returns after a call to .
+ ///
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;
}
}
///
/// Gets a value indicating whether the value factory has been invoked and has run to completion.
///
+ ///
+ /// This returns after a call to .
+ ///
public bool IsValueFactoryCompleted
{
get
{
- Interlocked.MemoryBarrier();
- return this.value is object && this.value.IsCompleted;
+ Task? value = Volatile.Read(ref this.value);
+ return value is object && value.IsCompleted && value != DisposedSentinel;
}
}
+ ///
+ /// Gets a value indicating whether has already been called.
+ ///
+ public bool IsValueDisposed => Volatile.Read(ref this.value) == DisposedSentinel;
+
///
/// Gets the task that produces or has produced the value.
///
@@ -93,6 +118,7 @@ public bool IsValueFactoryCompleted
///
/// Thrown when the value factory calls on this instance.
///
+ /// Thrown after is called.
public Task GetValueAsync() => this.GetValueAsync(CancellationToken.None);
///
@@ -108,6 +134,7 @@ public bool IsValueFactoryCompleted
///
/// Thrown when the value factory calls on this instance.
///
+ /// Thrown after is called.
public Task GetValueAsync(CancellationToken cancellationToken)
{
if (!((this.value is object && this.value.IsCompleted) || this.recursiveFactoryCheck.Value is null))
@@ -234,6 +261,109 @@ public T GetValue(CancellationToken cancellationToken)
}
}
+ ///
+ /// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
+ ///
+ ///
+ /// 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 .
+ /// If the value has already been produced and implements or , 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
+ /// may have been or will soon be called on the value, leading those users to experience a .
+ /// Note all conditions based on the value implementing or is based on the actual value, rather than the type argument.
+ /// This means that although may be IFoo (which does not implement ), the concrete type that implements IFoo may implement
+ /// and thus be treated as a disposable object as described above.
+ ///
+ public void DisposeValue()
+ {
+ if (!this.IsValueDisposed)
+ {
+ if (this.jobFactory is JoinableTaskFactory jtf)
+ {
+ jtf.Run(this.DisposeValueAsync);
+ }
+ else
+ {
+ this.DisposeValueAsync().GetAwaiter().GetResult();
+ }
+ }
+ }
+
+ ///
+ /// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// Calling this method will put this object into a disposed state where future calls to obtain the value will throw .
+ /// If the value has already been produced and implements , , or 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
+ /// may have been or will soon be called on the value, leading those users to experience a .
+ /// Note all conditions based on the value implementing or is based on the actual value, rather than the type argument.
+ /// This means that although may be IFoo (which does not implement ), the concrete type that implements IFoo may implement
+ /// and thus be treated as a disposable object as described above.
+ ///
+ public async Task DisposeValueAsync()
+ {
+ Task? 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();
+ }
+ }
+
///
/// Renders a string describing an uncreated value, or the string representation of the created value.
///
diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt
index f47308efb..c5cb0a27a 100644
--- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt
@@ -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!
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt
index f47308efb..c5cb0a27a 100644
--- a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt
@@ -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!
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt
index f47308efb..c5cb0a27a 100644
--- a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt
@@ -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!
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt
index f47308efb..c5cb0a27a 100644
--- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt
@@ -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!
\ No newline at end of file
+Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValue() -> void
+Microsoft.VisualStudio.Threading.AsyncLazy.DisposeValueAsync() -> System.Threading.Tasks.Task!
+Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool
\ No newline at end of file
diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
index 02d598949..9c3e3981e 100644
--- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
+++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
@@ -6,6 +6,7 @@
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft;
using Microsoft.VisualStudio.Threading;
using Xunit;
using Xunit.Abstractions;
@@ -18,6 +19,13 @@ public AsyncLazyTests(ITestOutputHelper logger)
{
}
+ public enum DisposeStyle
+ {
+ IDisposable,
+ SystemIAsyncDisposable,
+ ThreadingIAsyncDisposable,
+ }
+
[Fact]
public async Task Basic()
{
@@ -644,6 +652,106 @@ public async Task ExecutionContextFlowsFromFirstCaller_JTF()
await asyncLazy.GetValueAsync();
}
+ [Fact]
+ public async Task Dispose_ValueType_Completed()
+ {
+ AsyncLazy lazy = new(() => Task.FromResult(3));
+ lazy.GetValue();
+ lazy.DisposeValue();
+ await this.AssertDisposedLazyAsync(lazy);
+ }
+
+ [Theory, CombinatorialData]
+ public async Task Dispose_Disposable_Completed(DisposeStyle variety)
+ {
+ AsyncLazy