Skip to content

Commit 98acf4e

Browse files
authored
Merge pull request #1340 from AArnott/libtemplateUpdate
Fix hang in `AsyncLazy<T>.DisposeValueAsync`
2 parents 678cc14 + 0cc0cea commit 98acf4e

File tree

2 files changed

+33
-2
lines changed

2 files changed

+33
-2
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ public void DisposeValue()
384384
/// </returns>
385385
/// <remarks>
386386
/// <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>
387-
/// <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.
387+
/// <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.
388388
/// If the value factory has already started but has not yet completed, its value will be disposed of when the value factory completes.</para>
389389
/// <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"/>
390390
/// may have been or will soon be called on the value, leading those users to experience a <see cref="ObjectDisposedException"/>.</para>
@@ -394,6 +394,7 @@ public void DisposeValue()
394394
/// </remarks>
395395
public async Task DisposeValueAsync()
396396
{
397+
JoinableTask<T>? localJoinableTask = null;
397398
Task<T>? localValueTask = null;
398399
object? localValue = default;
399400
lock (this.syncObject)
@@ -417,6 +418,7 @@ public async Task DisposeValueAsync()
417418
// We'll schedule the value for disposal outside the lock so it can be synchronous with the value factory,
418419
// but will not execute within our lock.
419420
localValueTask = this.value;
421+
localJoinableTask = this.joinableTask;
420422
break;
421423
}
422424

@@ -431,7 +433,11 @@ public async Task DisposeValueAsync()
431433
this.valueFactory = null;
432434
}
433435

434-
if (localValueTask is not null)
436+
if (localJoinableTask is not null)
437+
{
438+
localValue = await localJoinableTask;
439+
}
440+
else if (localValueTask is not null)
435441
{
436442
localValue = await localValueTask.ConfigureAwait(false);
437443
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,31 @@ public async Task Dispose_CalledTwice_Disposable_Completed()
988988
await this.AssertDisposedLazyAsync(lazy);
989989
}
990990

991+
[Fact]
992+
public void DisposeValue_MidFactoryThatContestsForMainThread()
993+
{
994+
JoinableTaskContext context = this.InitializeJTCAndSC();
995+
996+
AsyncLazy<object> lazy = new(
997+
async delegate
998+
{
999+
// Ensure the caller keeps control of the UI thread,
1000+
// so that the request for the main thread comes in when it's controlled by others.
1001+
await Task.Yield();
1002+
await context.Factory.SwitchToMainThreadAsync(this.TimeoutToken);
1003+
return new();
1004+
},
1005+
context.Factory);
1006+
1007+
Task<object> lazyFactory = lazy.GetValueAsync(this.TimeoutToken);
1008+
1009+
// Make a JTF blocking call on the main thread that won't return until the factory completes.
1010+
context.Factory.Run(async delegate
1011+
{
1012+
await lazy.DisposeValueAsync().WithCancellation(this.TimeoutToken);
1013+
});
1014+
}
1015+
9911016
[Fact(Skip = "Hangs. This test documents a deadlock scenario that is not fixed (by design, IIRC).")]
9921017
public async Task ValueFactoryRequiresReadLockHeldByOther()
9931018
{

0 commit comments

Comments
 (0)