Skip to content

Commit f578222

Browse files
authored
Merge pull request #791 from microsoft/dev/lifengl/fixInconsistentDependencyLogic
An inconsistence whether dependencies of completed tasks should be included leads corrupted data structure
2 parents d58bfe8 + 4c7314e commit f578222

File tree

3 files changed

+146
-10
lines changed

3 files changed

+146
-10
lines changed

src/Microsoft.VisualStudio.Threading/JoinableTask.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,9 @@ internal void CompleteOnCurrentThread()
940940
}
941941
else if (tryAgainAfter is object)
942942
{
943+
// prevent referencing tasks which may be GCed during the waiting cycle.
944+
visited?.Clear();
945+
943946
ThreadingEventSource.Instance.WaitSynchronouslyStart();
944947
this.owner.WaitSynchronously(tryAgainAfter);
945948
ThreadingEventSource.Instance.WaitSynchronouslyStop();
@@ -1070,7 +1073,7 @@ private static bool TryDequeueSelfOrDependencies(IJoinableTaskDependent currentN
10701073

10711074
if (work is null)
10721075
{
1073-
if (joinableTask?.IsFullyCompleted != true)
1076+
if (joinableTask?.IsCompleteRequested != true)
10741077
{
10751078
foreach (IJoinableTaskDependent? item in JoinableTaskDependencyGraph.GetDirectDependentNodes(currentNode))
10761079
{

src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ internal static bool CleanUpPotentialUnreachableDependentItems(JoinableTask sync
242242
if (possibleUnreachableItems is object && possibleUnreachableItems.Count > 0)
243243
{
244244
var reachableNodes = new HashSet<IJoinableTaskDependent>();
245-
var syncTaskItem = (IJoinableTaskDependent)syncTask;
245+
IJoinableTaskDependent syncTaskItem = syncTask;
246246

247247
JoinableTaskDependentData.ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(syncTaskItem, reachableNodes, possibleUnreachableItems);
248248

@@ -455,7 +455,17 @@ internal static void AddSelfAndDescendentOrJoinedJobs(IJoinableTaskDependent tas
455455

456456
if (taskOrCollection is JoinableTask thisJoinableTask)
457457
{
458-
if (thisJoinableTask.IsFullyCompleted || !joinables.Add(thisJoinableTask))
458+
if (thisJoinableTask.IsCompleteRequested)
459+
{
460+
if (!thisJoinableTask.IsFullyCompleted)
461+
{
462+
joinables.Add(thisJoinableTask);
463+
}
464+
465+
return;
466+
}
467+
468+
if (!joinables.Add(thisJoinableTask))
459469
{
460470
return;
461471
}
@@ -485,7 +495,6 @@ internal static void OnSynchronousTaskStartToBlockWaiting(JoinableTask syncTask,
485495
Requires.NotNull(syncTask, nameof(syncTask));
486496

487497
pendingRequestsCount = 0;
488-
taskHasPendingRequests = null;
489498

490499
taskHasPendingRequests = AddDependingSynchronousTask(syncTask, syncTask, ref pendingRequestsCount);
491500
}
@@ -504,7 +513,11 @@ internal static void OnSynchronousTaskEndToBlockWaiting(JoinableTask syncTask)
504513
lock (syncTask.Factory.Context.SyncContextLock)
505514
{
506515
// Remove itself from the tracking list, after the task is completed.
507-
RemoveDependingSynchronousTask(syncTask, syncTask, force: true);
516+
IJoinableTaskDependent syncTaskItem = syncTask;
517+
if (syncTaskItem.GetJoinableTaskDependentData().dependingSynchronousTaskTracking is object)
518+
{
519+
RemoveDependingSynchronousTask(syncTask, syncTask, force: true);
520+
}
508521

509522
if (syncTask.PotentialUnreachableDependents is object && syncTask.PotentialUnreachableDependents.Count > 0)
510523
{
@@ -537,6 +550,11 @@ internal static void ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(IJoinabl
537550
return;
538551
}
539552

553+
if ((taskOrCollection as JoinableTask)?.IsCompleteRequested == true)
554+
{
555+
return;
556+
}
557+
540558
WeakKeyDictionary<IJoinableTaskDependent, int>? dependencies = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes;
541559
if (dependencies is object)
542560
{
@@ -846,11 +864,7 @@ private static void RemoveDependingSynchronousTask(IJoinableTaskDependent taskOr
846864
Requires.NotNull(syncTask, nameof(syncTask));
847865
Assumes.True(Monitor.IsEntered(taskOrCollection.JoinableTaskContext.SyncContextLock));
848866

849-
var syncTaskItem = (IJoinableTaskDependent)syncTask;
850-
if (syncTaskItem.GetJoinableTaskDependentData().dependingSynchronousTaskTracking is object)
851-
{
852-
RemoveDependingSynchronousTaskFrom(new IJoinableTaskDependent[] { taskOrCollection }, syncTask, force);
853-
}
867+
RemoveDependingSynchronousTaskFrom(new IJoinableTaskDependent[] { taskOrCollection }, syncTask, force);
854868
}
855869

856870
/// <summary>

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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4116,6 +4116,44 @@ public void GetAwaiterCompletedTDoesNotLock()
41164116
lockHolder.Wait();
41174117
}
41184118

4119+
[Fact]
4120+
public void JoinableTaskCleanUpDependenciesForLoopDependencies()
4121+
{
4122+
this.SimulateUIThread(async delegate
4123+
{
4124+
var joinableTaskCollection = new JoinableTaskCollection(this.context, refCountAddedJobs: true);
4125+
JoinableTaskFactory taskFactory = this.context.CreateFactory(joinableTaskCollection);
4126+
((DerivedJoinableTaskFactory)taskFactory).AssumeConcurrentUse = true;
4127+
4128+
bool isMainThreadBlockedResult = true;
4129+
var unblockTask2 = new AsyncManualResetEvent();
4130+
JoinableTask? childTask = null;
4131+
4132+
this.context.Factory.Run(async () =>
4133+
{
4134+
await TaskScheduler.Default.SwitchTo(alwaysYield: true);
4135+
4136+
childTask = await this.SpinOffMainThreadTaskForJoinableTaskDependenciesHandledAfterTaskCompletion(taskFactory, joinableTaskCollection, unblockTask2);
4137+
});
4138+
4139+
using (this.context.SuppressRelevance())
4140+
{
4141+
isMainThreadBlockedResult = await taskFactory.RunAsync(() =>
4142+
{
4143+
return Task.FromResult(this.context.IsMainThreadBlocked());
4144+
});
4145+
}
4146+
4147+
using (this.context.SuppressRelevance())
4148+
{
4149+
unblockTask2.Set();
4150+
childTask!.Task.Wait();
4151+
}
4152+
4153+
Assert.False(isMainThreadBlockedResult);
4154+
});
4155+
}
4156+
41194157
protected override JoinableTaskContext CreateJoinableTaskContext()
41204158
{
41214159
return new DerivedJoinableTaskContext();
@@ -4126,6 +4164,87 @@ private static async void SomeFireAndForgetMethod()
41264164
await Task.Yield();
41274165
}
41284166

4167+
private async Task<JoinableTask> SpinOffMainThreadTaskForJoinableTaskDependenciesHandledAfterTaskCompletion(
4168+
JoinableTaskFactory joinableTaskFactory,
4169+
JoinableTaskCollection joinableTaskCollection,
4170+
AsyncManualResetEvent unblockTask2)
4171+
{
4172+
JoinableTask? task2 = null;
4173+
Task? spinOffTask = null;
4174+
4175+
JoinableTask? joinableTask = null;
4176+
4177+
var mainThreadJoinedCollection = new JoinableTaskCollection(this.context);
4178+
await TaskScheduler.Default.SwitchTo(alwaysYield: true);
4179+
4180+
using (this.context.SuppressRelevance())
4181+
{
4182+
// this task creates a circular loop.
4183+
task2 = joinableTaskFactory.RunAsync(async () =>
4184+
{
4185+
joinableTaskCollection.Join();
4186+
await unblockTask2.WaitAsync();
4187+
});
4188+
4189+
var unblockTask1 = new AsyncManualResetEvent();
4190+
var spinOffIsReady = new AsyncManualResetEvent();
4191+
var spinOffIsDone = new AsyncManualResetEvent();
4192+
4193+
joinableTask = joinableTaskFactory.RunAsync(async () =>
4194+
{
4195+
joinableTaskCollection.Join();
4196+
4197+
spinOffTask = Task.Run(async () =>
4198+
{
4199+
JoinableTaskFactory.MainThreadAwaiter awaiter = this.context.Factory.SwitchToMainThreadAsync().GetAwaiter();
4200+
awaiter.OnCompleted(() =>
4201+
{
4202+
spinOffIsDone.Set();
4203+
});
4204+
4205+
spinOffIsReady.Set();
4206+
4207+
await spinOffIsDone.WaitAsync();
4208+
});
4209+
4210+
await spinOffIsReady.WaitAsync();
4211+
await unblockTask1.WaitAsync();
4212+
});
4213+
4214+
// Increase refcount
4215+
mainThreadJoinedCollection.Add(joinableTask);
4216+
joinableTaskCollection.Add(joinableTask);
4217+
4218+
unblockTask1.Set();
4219+
4220+
await joinableTask.Task;
4221+
}
4222+
4223+
await this.context.Factory.SwitchToMainThreadAsync();
4224+
4225+
// Due to circular reference, this add/remove reference to lead incompleted state
4226+
// the joinableTaskCollection will retain refcount to the JTF.Run task.
4227+
using (joinableTaskCollection.Join())
4228+
{
4229+
}
4230+
4231+
using (mainThreadJoinedCollection.Join())
4232+
{
4233+
// This IsMainThreadBlocked triggers the incompleted state to be cleaned up.
4234+
// JTF.Run joins the completed task through the middle collection to expose the
4235+
// inconsistent between two logic, so the recomputation won't clean up the circular
4236+
// dependency loop correctly.
4237+
await this.context.Factory.RunAsync(() =>
4238+
{
4239+
return Task.FromResult(this.context.IsMainThreadBlocked());
4240+
});
4241+
4242+
await spinOffTask!;
4243+
}
4244+
4245+
return task2;
4246+
}
4247+
41294248
private async Task SomeOperationThatMayBeOnMainThreadAsync()
41304249
{
41314250
await Task.Yield();

0 commit comments

Comments
 (0)