Skip to content

Commit c51f4d2

Browse files
authored
Merge pull request #1271 from AArnott/fixAsyncLazyDispose
Fix hang in `AsyncLazy<T>.DisposeValue()` when `T : IAsyncDisposable`
2 parents 4f0613b + d36f8f2 commit c51f4d2

2 files changed

Lines changed: 55 additions & 13 deletions

File tree

src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ public class AsyncLazy<T>
4141
private AsyncLocal<object>? recursiveFactoryCheck;
4242

4343
/// <summary>
44-
/// The function to invoke to produce the task.
44+
/// An optional means to avoid deadlocks when synchronous APIs are called that must invoke async methods in user code.
4545
/// </summary>
46-
private Func<Task<T>>? valueFactory;
46+
private readonly JoinableTaskFactory? jobFactory;
4747

4848
/// <summary>
49-
/// The async pump to Join on calls to <see cref="GetValueAsync(CancellationToken)"/>.
49+
/// The function to invoke to produce the task.
5050
/// </summary>
51-
private JoinableTaskFactory? jobFactory;
51+
private Func<Task<T>>? valueFactory;
5252

5353
/// <summary>
5454
/// The result of the value factory.
@@ -64,7 +64,10 @@ public class AsyncLazy<T>
6464
/// Initializes a new instance of the <see cref="AsyncLazy{T}"/> class.
6565
/// </summary>
6666
/// <param name="valueFactory">The async function that produces the value. To be invoked at most once.</param>
67-
/// <param name="joinableTaskFactory">The factory to use when invoking the value factory in <see cref="GetValueAsync(CancellationToken)"/> to avoid deadlocks when the main thread is required by the value factory.</param>
67+
/// <param name="joinableTaskFactory">
68+
/// The <see cref="JoinableTaskFactory" /> to use for avoiding deadlocks when the <paramref name="valueFactory"/>
69+
/// or the constructed value's <see cref="System.IAsyncDisposable.DisposeAsync"/> method may require the main thread in the process.
70+
/// </param>
6871
public AsyncLazy(Func<Task<T>> valueFactory, JoinableTaskFactory? joinableTaskFactory = null)
6972
{
7073
Requires.NotNull(valueFactory, nameof(valueFactory));
@@ -203,7 +206,6 @@ public Task<T> GetValueAsync(CancellationToken cancellationToken)
203206
}
204207
finally
205208
{
206-
this.jobFactory = null;
207209
this.joinableTask = null;
208210
}
209211
};
@@ -285,11 +287,8 @@ public T GetValue(CancellationToken cancellationToken)
285287
}
286288
else
287289
{
288-
// Capture the factory as a local before comparing and dereferencing it since
289-
// the field can transition to null and we want to gracefully handle that race condition.
290-
JoinableTaskFactory? factory = this.jobFactory;
291-
return factory is object
292-
? factory.Run(() => this.GetValueAsync(cancellationToken))
290+
return this.jobFactory is JoinableTaskFactory jtf
291+
? jtf.Run(() => this.GetValueAsync(cancellationToken))
293292
: this.GetValueAsync(cancellationToken).GetAwaiter().GetResult();
294293
}
295294
}

test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,44 @@ public async Task Dispose_Disposable_Incomplete(DisposeStyle variety)
897897
await value.Disposed.WithCancellation(this.TimeoutToken);
898898
}
899899

900+
[Fact]
901+
public void DisposeValue_AsyncDisposableValueRequiresMainThread()
902+
{
903+
JoinableTaskContext context = this.InitializeJTCAndSC();
904+
SingleThreadedTestSynchronizationContext.IFrame frame = SingleThreadedTestSynchronizationContext.NewFrame();
905+
906+
AsyncLazy<SystemAsyncDisposable> lazy = new(
907+
delegate
908+
{
909+
return Task.FromResult(new SystemAsyncDisposable { YieldDuringDispose = true });
910+
},
911+
context.Factory);
912+
lazy.GetValue();
913+
914+
TaskCompletionSource<bool> delegateResult = new();
915+
SynchronizationContext.Current!.Post(
916+
delegate
917+
{
918+
try
919+
{
920+
lazy.DisposeValue();
921+
922+
delegateResult.SetResult(true);
923+
}
924+
catch (Exception ex)
925+
{
926+
delegateResult.SetException(ex);
927+
}
928+
finally
929+
{
930+
frame.Continue = false;
931+
}
932+
},
933+
null);
934+
SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current!, frame);
935+
delegateResult.Task.GetAwaiter().GetResult(); // rethrow any exceptions
936+
}
937+
900938
[Fact]
901939
public async Task Dispose_NonDisposable_Incomplete()
902940
{
@@ -1066,10 +1104,15 @@ private class Disposable : DisposableBase, IDisposableObservable
10661104

10671105
private class SystemAsyncDisposable : DisposableBase, System.IAsyncDisposable
10681106
{
1069-
public ValueTask DisposeAsync()
1107+
internal bool YieldDuringDispose { get; set; }
1108+
1109+
public async ValueTask DisposeAsync()
10701110
{
10711111
this.disposalEvent.Set();
1072-
return default;
1112+
if (this.YieldDuringDispose)
1113+
{
1114+
await Task.Yield();
1115+
}
10731116
}
10741117
}
10751118

0 commit comments

Comments
 (0)