Skip to content

Commit

Permalink
Merge pull request #1254 from AArnott/asyncLazySuppressRelevance
Browse files Browse the repository at this point in the history
Add `AsyncLazy<T>.SuppressRelevance()` method
  • Loading branch information
AArnott authored Oct 31, 2023
2 parents b323b1f + e864cda commit 12e1dd1
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 4 deletions.
95 changes: 95 additions & 0 deletions src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,65 @@ public T GetValue(CancellationToken cancellationToken)
}
}

/// <summary>
/// Marks the code that follows as irrelevant to the receiving <see cref="AsyncLazy{T}"/> value factory.
/// </summary>
/// <returns>A value to dispose of to restore relevance into the value factory.</returns>
/// <remarks>
/// <para>In some cases asynchronous work may be spun off inside a value factory.
/// When the value factory does <em>not</em> 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 <see cref="InvalidOperationException"/> being thrown from
/// <see cref="GetValueAsync(CancellationToken)"/> if the value factory has not completed already,
/// which can introduce non-determinstic failures in the program.</para>
/// <para>A <c>using</c> block around the spun off code can help your program achieve reliable behavior, as shown below.</para>
/// <example>
/// <code><![CDATA[
/// 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);
/// }
/// }
/// ]]></code>
/// </example>
/// <para>If the <see cref="AsyncLazy{T}"/> was created with a <see cref="JoinableTaskFactory"/>,
/// this method also calls <see cref="JoinableTaskContext.SuppressRelevance"/> on the <see cref="JoinableTaskFactory.Context"/>
/// associated with that factory.
/// </para>
/// </remarks>
public RevertRelevance SuppressRelevance() => new RevertRelevance(this);

/// <summary>
/// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
/// </summary>
Expand Down Expand Up @@ -373,4 +432,40 @@ public override string ToString()
? (this.value.Status == TaskStatus.RanToCompletion ? $"{this.value.Result}" : Strings.LazyValueFaulted)
: Strings.LazyValueNotCreated;
}

/// <summary>
/// A structure that hides relevance of a block of code from a particular <see cref="AsyncLazy{T}"/> and the <see cref="JoinableTaskContext"/> it was created with.
/// </summary>
public readonly struct RevertRelevance : IDisposable
{
private readonly AsyncLazy<T>? owner;
private readonly object? oldCheckValue;
private readonly JoinableTaskContext.RevertRelevance? joinableRelevance;

/// <summary>
/// Initializes a new instance of the <see cref="RevertRelevance"/> struct.
/// </summary>
/// <param name="owner">The instance that created this value.</param>
internal RevertRelevance(AsyncLazy<T> owner)
{
Requires.NotNull(owner, nameof(owner));
this.owner = owner;

(this.oldCheckValue, owner.recursiveFactoryCheck.Value) = (owner.recursiveFactoryCheck.Value, null);
this.joinableRelevance = owner.jobFactory?.Context.SuppressRelevance();
}

/// <summary>
/// Reverts the async local and thread static values to their original values.
/// </summary>
public void Dispose()
{
if (this.owner is object)
{
this.owner.recursiveFactoryCheck.Value = this.oldCheckValue;
}

this.joinableRelevance?.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
96 changes: 96 additions & 0 deletions test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,102 @@ public async Task ExecutionContextFlowsFromFirstCaller_JTF()
await asyncLazy.GetValueAsync();
}

[Fact]
public async Task SuppressRelevance_WithoutJTF()
{
AsyncManualResetEvent allowValueFactoryToFinish = new();
Task<int>? fireAndForgetTask = null;
AsyncLazy<int> asyncLazy = null!;
asyncLazy = new AsyncLazy<int>(
async delegate
{
using (asyncLazy.SuppressRelevance())
{
fireAndForgetTask = FireAndForgetCodeAsync();
}

await allowValueFactoryToFinish;
return 1;
},
null);

bool fireAndForgetCodeAsyncEntered = false;
Task<int> 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<int> 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<int>? fireAndForgetTask = null;
AsyncLazy<int> asyncLazy = null!;
asyncLazy = new AsyncLazy<int>(
async delegate
{
using (asyncLazy.SuppressRelevance())
{
fireAndForgetTask = FireAndForgetCodeAsync();
}

await allowValueFactoryToFinish;
return 1;
},
jtf);

bool fireAndForgetCodeAsyncEntered = false;
bool fireAndForgetCodeAsyncReachedUIThread = false;
jtf.Run(async delegate
{
Task<int> 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<int> 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()
{
Expand Down

0 comments on commit 12e1dd1

Please sign in to comment.