diff --git a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs index 057d4621a..630bc2b6f 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs @@ -261,6 +261,65 @@ public T GetValue(CancellationToken cancellationToken) } } + /// + /// Marks the code that follows as irrelevant to the receiving value factory. + /// + /// A value to dispose of to restore relevance into the value factory. + /// + /// 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 being thrown from + /// 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. + /// + /// numberOfApples; + /// + /// public MyClass() { + /// this.numberOfApples = new AsyncLazy(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 GetApplesCountAsync(CancellationToken cancellationToken) { + /// return await this.numberOfApples.GetValueAsync(cancellationToken); + /// } + /// + /// private async Task 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 was created with a , + /// this method also calls on the + /// associated with that factory. + /// + /// + public RevertRelevance SuppressRelevance() => new RevertRelevance(this); + /// /// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail. /// @@ -373,4 +432,40 @@ public override string ToString() ? (this.value.Status == TaskStatus.RanToCompletion ? $"{this.value.Result}" : Strings.LazyValueFaulted) : Strings.LazyValueNotCreated; } + + /// + /// A structure that hides relevance of a block of code from a particular and the it was created with. + /// + public readonly struct RevertRelevance : IDisposable + { + private readonly AsyncLazy? owner; + private readonly object? oldCheckValue; + private readonly JoinableTaskContext.RevertRelevance? joinableRelevance; + + /// + /// Initializes a new instance of the struct. + /// + /// The instance that created this value. + internal RevertRelevance(AsyncLazy owner) + { + Requires.NotNull(owner, nameof(owner)); + this.owner = owner; + + (this.oldCheckValue, owner.recursiveFactoryCheck.Value) = (owner.recursiveFactoryCheck.Value, null); + this.joinableRelevance = owner.jobFactory?.Context.SuppressRelevance(); + } + + /// + /// Reverts the async local and thread static values to their original values. + /// + public void Dispose() + { + if (this.owner is object) + { + this.owner.recursiveFactoryCheck.Value = this.oldCheckValue; + } + + this.joinableRelevance?.Dispose(); + } + } } diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt index c5cb0a27a..1a9888196 100644 --- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string! 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 +Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void +Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance \ 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 c5cb0a27a..1a9888196 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string! 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 +Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void +Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance \ 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 c5cb0a27a..1a9888196 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string! 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 +Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void +Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance \ 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 c5cb0a27a..1a9888196 100644 --- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string! 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 +Microsoft.VisualStudio.Threading.AsyncLazy.IsValueDisposed.get -> bool +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance +Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance.Dispose() -> void +Microsoft.VisualStudio.Threading.AsyncLazy.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy.RevertRelevance \ 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 9c3e3981e..2077d81c3 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs @@ -652,6 +652,102 @@ public async Task ExecutionContextFlowsFromFirstCaller_JTF() await asyncLazy.GetValueAsync(); } + [Fact] + public async Task SuppressRelevance_WithoutJTF() + { + AsyncManualResetEvent allowValueFactoryToFinish = new(); + Task? fireAndForgetTask = null; + AsyncLazy asyncLazy = null!; + asyncLazy = new AsyncLazy( + async delegate + { + using (asyncLazy.SuppressRelevance()) + { + fireAndForgetTask = FireAndForgetCodeAsync(); + } + + await allowValueFactoryToFinish; + return 1; + }, + null); + + bool fireAndForgetCodeAsyncEntered = false; + Task lazyValue = asyncLazy.GetValueAsync(); + Assert.True(fireAndForgetCodeAsyncEntered); + allowValueFactoryToFinish.Set(); + + // Assert that the value factory was allowed to finish. + Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken)); + + // Assert that the fire-and-forget task was allowed to finish and did so without throwing. + Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken)); + + async Task FireAndForgetCodeAsync() + { + fireAndForgetCodeAsyncEntered = true; + return await asyncLazy.GetValueAsync(); + } + } + + [Fact] + public async Task SuppressRelevance_WithJTF() + { + JoinableTaskContext? context = this.InitializeJTCAndSC(); + SingleThreadedTestSynchronizationContext.IFrame frame = SingleThreadedTestSynchronizationContext.NewFrame(); + + JoinableTaskFactory? jtf = context.Factory; + AsyncManualResetEvent allowValueFactoryToFinish = new(); + Task? fireAndForgetTask = null; + AsyncLazy asyncLazy = null!; + asyncLazy = new AsyncLazy( + async delegate + { + using (asyncLazy.SuppressRelevance()) + { + fireAndForgetTask = FireAndForgetCodeAsync(); + } + + await allowValueFactoryToFinish; + return 1; + }, + jtf); + + bool fireAndForgetCodeAsyncEntered = false; + bool fireAndForgetCodeAsyncReachedUIThread = false; + jtf.Run(async delegate + { + Task lazyValue = asyncLazy.GetValueAsync(); + Assert.True(fireAndForgetCodeAsyncEntered); + await Task.Delay(AsyncDelay); + Assert.False(fireAndForgetCodeAsyncReachedUIThread); + allowValueFactoryToFinish.Set(); + + // Assert that the value factory was allowed to finish. + Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken)); + }); + + // Run a main thread pump so the fire-and-forget task can finish. + SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current!, frame); + + // Assert that the fire-and-forget task was allowed to finish and did so without throwing. + Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken)); + + async Task FireAndForgetCodeAsync() + { + fireAndForgetCodeAsyncEntered = true; + + // Yield the caller's thread. + // Resuming will require the main thread, since the caller was on the main thread. + await Task.Yield(); + + fireAndForgetCodeAsyncReachedUIThread = true; + + int result = await asyncLazy.GetValueAsync(); + frame.Continue = false; + return result; + } + } + [Fact] public async Task Dispose_ValueType_Completed() {