Skip to content

Commit d9d54cf

Browse files
author
Lifeng Lu
committed
Delay the logic to clean up unreachable JTF dependencies which are still marked as blocking tasks due to loop dependencies.
1 parent eb6588c commit d9d54cf

File tree

2 files changed

+173
-54
lines changed

2 files changed

+173
-54
lines changed

src/Microsoft.VisualStudio.Threading/JoinableTask.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,15 @@ internal WeakReference<JoinableTask> WeakSelf
380380
}
381381
}
382382

383+
/// <summary>
384+
/// Gets or sets potential unreacable dependent nodes.
385+
/// This is a special collection only used in sychronized task when there are other tasks which are marked to block it through ref-count code.
386+
/// However, it is possible the reference count is retained by loop-dependencies. This collection tracking those items,
387+
/// so the clean-up logic can run when it becomes necessary.
388+
/// </summary>
389+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
390+
internal HashSet<IJoinableTaskDependent>? PotentialUnreachableDependents { get; set; }
391+
383392
/// <summary>
384393
/// Gets the flags set on this task.
385394
/// </summary>
@@ -1078,7 +1087,9 @@ private bool TryDequeueSelfOrDependencies(bool onMainThread, ref HashSet<IJoinab
10781087

10791088
if (this.pendingEventSource is object)
10801089
{
1081-
if (this.pendingEventSource.TryGetTarget(out JoinableTask? pendingSource) && JoinableTaskDependencyGraph.IsDependingSynchronousTask(pendingSource, this))
1090+
if (this.pendingEventSource.TryGetTarget(out JoinableTask? pendingSource) &&
1091+
(pendingSource == this ||
1092+
(this.PotentialUnreachableDependents == null && JoinableTaskDependencyGraph.IsDependingSynchronousTask(pendingSource, this))))
10821093
{
10831094
ExecutionQueue? queue = onMainThread ? pendingSource.mainThreadQueue : pendingSource.threadPoolQueue;
10841095
if (queue is object && !queue.IsCompleted && queue.TryDequeue(out work))
@@ -1105,8 +1116,28 @@ private bool TryDequeueSelfOrDependencies(bool onMainThread, ref HashSet<IJoinab
11051116
visited.Clear();
11061117
}
11071118

1108-
if (TryDequeueSelfOrDependencies(this, onMainThread, visited, out work))
1119+
var foundWork = TryDequeueSelfOrDependencies(this, onMainThread, visited, out work);
1120+
1121+
HashSet<IJoinableTaskDependent>? visitedNodes = visited;
1122+
if (visitedNodes != null && this.PotentialUnreachableDependents != null)
1123+
{
1124+
// We walked the dependencies tree and use this information to update the PotentialUnreachableDependents list.
1125+
this.PotentialUnreachableDependents.RemoveWhere(n => visitedNodes.Contains(n));
1126+
if (this.PotentialUnreachableDependents.Count == 0)
1127+
{
1128+
this.PotentialUnreachableDependents = null;
1129+
}
1130+
1131+
if (!foundWork && this.PotentialUnreachableDependents != null)
1132+
{
1133+
JoinableTaskDependencyGraph.RemoveUnreachableDependentItems(this, this.PotentialUnreachableDependents, visitedNodes);
1134+
}
1135+
}
1136+
1137+
if (foundWork)
11091138
{
1139+
Assumes.NotNull(work);
1140+
11101141
tryAgainAfter = null;
11111142
return true;
11121143
}

src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs

Lines changed: 140 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,47 @@ internal static HashSet<JoinableTask> GetDependentTasksFromCandidates(IEnumerabl
219219
return results;
220220
}
221221

222+
/// <summary>
223+
/// Computes dependency graph to clean up all potential unreachable dependents items.
224+
/// </summary>
225+
/// <param name="syncTask">A thread blocking sychornizing task.</param>
226+
internal static void CleanUpPotentialUnreachableDependentItems(JoinableTask syncTask)
227+
{
228+
Requires.NotNull(syncTask, nameof(syncTask));
229+
230+
// a set of tasks may form a dependent loop, so it will make the reference count system
231+
// not to work correctly when we try to remove the synchronous task.
232+
// To get rid of those loops, if a task still tracks the synchronous task after reducing
233+
// the reference count, we will calculate the entire reachable tree from the root. That will
234+
// tell us the exactly tasks which need track the synchronous task, and we will clean up the rest.
235+
HashSet<IJoinableTaskDependent>? unreachableItems = syncTask.PotentialUnreachableDependents;
236+
if (unreachableItems is object)
237+
{
238+
var reachableNodes = new HashSet<IJoinableTaskDependent>();
239+
var syncTaskItem = (IJoinableTaskDependent)syncTask;
240+
241+
JoinableTaskDependentData.ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(syncTaskItem, reachableNodes, unreachableItems);
242+
243+
// force to remove all invalid items
244+
JoinableTaskDependentData.RemoveUnreachableDependentItems(syncTask, unreachableItems, reachableNodes);
245+
}
246+
}
247+
248+
/// <summary>
249+
/// Force to clean up all unreachable dependent item, so they are not marked to block the syncTask.
250+
/// </summary>
251+
/// <param name="syncTask">The thread blocking task.</param>
252+
/// <param name="unreachableItems">Unreachable dependent items.</param>
253+
/// <param name="reachableItems">All reachable items.</param>
254+
internal static void RemoveUnreachableDependentItems(JoinableTask syncTask, HashSet<IJoinableTaskDependent> unreachableItems, HashSet<IJoinableTaskDependent> reachableItems)
255+
{
256+
Requires.NotNull(syncTask, nameof(syncTask));
257+
Requires.NotNull(unreachableItems, nameof(unreachableItems));
258+
Requires.NotNull(reachableItems, nameof(reachableItems));
259+
260+
JoinableTaskDependentData.RemoveUnreachableDependentItems(syncTask, unreachableItems, reachableItems);
261+
}
262+
222263
/// <summary>
223264
/// Preserve data for the JoinableTask dependency tree. It is holded inside either a <see cref="JoinableTask"/> or a <see cref="JoinableTaskCollection"/>.
224265
/// Do not call methods/properties directly anywhere out of <see cref="JoinableTaskDependencyGraph"/>.
@@ -438,6 +479,59 @@ internal static void OnSynchronousTaskEndToBlockWaiting(JoinableTask syncTask)
438479
}
439480
}
440481

482+
/// <summary>
483+
/// Compute all reachable nodes from a synchronous task. Because we use the result to clean up invalid
484+
/// items from the remain task, we will remove valid task from the collection, and stop immediately if nothing is left.
485+
/// </summary>
486+
/// <param name="taskOrCollection">The current joinableTask or collection owns the data.</param>
487+
/// <param name="reachableNodes">All reachable dependency nodes. This is not a completed list, if there is no remain node.</param>
488+
/// <param name="remainNodes">Remain dependency nodes we want to check. After the execution, it will retain non-reachable nodes.</param>
489+
internal static void ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(IJoinableTaskDependent taskOrCollection, HashSet<IJoinableTaskDependent> reachableNodes, HashSet<IJoinableTaskDependent> remainNodes)
490+
{
491+
Requires.NotNull(taskOrCollection, nameof(taskOrCollection));
492+
Requires.NotNull(remainNodes, nameof(remainNodes));
493+
Requires.NotNull(reachableNodes, nameof(reachableNodes));
494+
if ((taskOrCollection as JoinableTask)?.IsFullyCompleted != true)
495+
{
496+
if (reachableNodes.Add(taskOrCollection))
497+
{
498+
if (remainNodes.Remove(taskOrCollection) && remainNodes.Count == 0)
499+
{
500+
// no remain task left, quit the loop earlier
501+
return;
502+
}
503+
504+
WeakKeyDictionary<IJoinableTaskDependent, int>? dependencies = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes;
505+
if (dependencies is object)
506+
{
507+
foreach (KeyValuePair<IJoinableTaskDependent, int> item in dependencies)
508+
{
509+
ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(item.Key, reachableNodes, remainNodes);
510+
if (remainNodes.Count == 0)
511+
{
512+
return;
513+
}
514+
}
515+
}
516+
}
517+
}
518+
}
519+
520+
/// <summary>
521+
/// Force to clean up all unreachable dependent item, so they are not marked to block the syncTask.
522+
/// </summary>
523+
/// <param name="syncTask">The thread blocking task.</param>
524+
/// <param name="unreachableItems">Unreachable dependent items.</param>
525+
/// <param name="reachableItems">All reachable items.</param>
526+
internal static void RemoveUnreachableDependentItems(JoinableTask syncTask, HashSet<IJoinableTaskDependent> unreachableItems, HashSet<IJoinableTaskDependent> reachableItems)
527+
{
528+
HashSet<IJoinableTaskDependent>? remainPlaceHold = null;
529+
foreach (IJoinableTaskDependent? unreachableItem in unreachableItems)
530+
{
531+
JoinableTaskDependentData.RemoveDependingSynchronousTask(unreachableItem, syncTask, reachableItems, ref remainPlaceHold);
532+
}
533+
}
534+
441535
/// <summary>
442536
/// Gets all dependent nodes registered in the <see cref="childDependentNodes"/>
443537
/// This method is expected to be used with the JTF lock.
@@ -472,15 +566,43 @@ internal bool HasDirectDependency(IJoinableTaskDependent dependency)
472566
/// </summary>
473567
internal bool HasMainThreadSynchronousTaskWaiting()
474568
{
569+
var hasDoneCleanUp = false;
570+
475571
DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking;
476572
while (existingTaskTracking is object)
477573
{
574+
DependentSynchronousTask? nextTrackingTask = existingTaskTracking.Next;
478575
if ((existingTaskTracking.SynchronousTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread)
479576
{
480-
return true;
577+
if (existingTaskTracking.SynchronousTask.PotentialUnreachableDependents != null)
578+
{
579+
// This might remove the current tracking item from the linked list, so we capture next node first.
580+
CleanUpPotentialUnreachableDependentItems(existingTaskTracking.SynchronousTask);
581+
582+
// We need check it again after the cleanup work has finished.
583+
hasDoneCleanUp = true;
584+
}
585+
else
586+
{
587+
return true;
588+
}
481589
}
482590

483-
existingTaskTracking = existingTaskTracking.Next;
591+
existingTaskTracking = nextTrackingTask;
592+
}
593+
594+
if (hasDoneCleanUp)
595+
{
596+
existingTaskTracking = this.dependingSynchronousTaskTracking;
597+
while (existingTaskTracking is object)
598+
{
599+
if ((existingTaskTracking.SynchronousTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread)
600+
{
601+
return true;
602+
}
603+
604+
existingTaskTracking = existingTaskTracking.Next;
605+
}
484606
}
485607

486608
return false;
@@ -712,7 +834,7 @@ private static void RemoveDependingSynchronousTaskFrom(IReadOnlyList<IJoinableTa
712834
Requires.NotNull(syncTask, nameof(syncTask));
713835

714836
HashSet<IJoinableTaskDependent>? reachableNodes = null;
715-
HashSet<IJoinableTaskDependent>? remainNodes = null;
837+
HashSet<IJoinableTaskDependent>? remainNodes = syncTask.PotentialUnreachableDependents;
716838

717839
if (force)
718840
{
@@ -724,61 +846,27 @@ private static void RemoveDependingSynchronousTaskFrom(IReadOnlyList<IJoinableTa
724846
RemoveDependingSynchronousTask(task, syncTask, reachableNodes, ref remainNodes);
725847
}
726848

727-
if (!force && remainNodes is object && remainNodes.Count > 0)
849+
if (remainNodes is object && remainNodes.Count > 0)
728850
{
729-
// a set of tasks may form a dependent loop, so it will make the reference count system
730-
// not to work correctly when we try to remove the synchronous task.
731-
// To get rid of those loops, if a task still tracks the synchronous task after reducing
732-
// the reference count, we will calculate the entire reachable tree from the root. That will
733-
// tell us the exactly tasks which need track the synchronous task, and we will clean up the rest.
734-
reachableNodes = new HashSet<IJoinableTaskDependent>();
735-
var syncTaskItem = (IJoinableTaskDependent)syncTask;
736-
ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(syncTaskItem, reachableNodes, remainNodes);
851+
if (force)
852+
{
853+
Assumes.True(reachableNodes!.Count == 0);
854+
RemoveUnreachableDependentItems(syncTask, remainNodes, reachableNodes);
737855

738-
// force to remove all invalid items
739-
HashSet<IJoinableTaskDependent>? remainPlaceHold = null;
740-
foreach (IJoinableTaskDependent? remainTask in remainNodes)
856+
syncTask.PotentialUnreachableDependents = null;
857+
}
858+
else if (syncTask.PotentialUnreachableDependents != remainNodes)
741859
{
742-
RemoveDependingSynchronousTask(remainTask, syncTask, reachableNodes, ref remainPlaceHold);
860+
// a set of tasks may form a dependent loop, so it will make the reference count system
861+
// not to work correctly when we try to remove the synchronous task.
862+
// It will require full dependency scanning to clean them up, which is quite expensive,
863+
// so we keep tracking them, and clean them up when it becomes essential.
864+
syncTask.PotentialUnreachableDependents = remainNodes;
743865
}
744866
}
745-
}
746-
747-
/// <summary>
748-
/// Compute all reachable nodes from a synchronous task. Because we use the result to clean up invalid
749-
/// items from the remain task, we will remove valid task from the collection, and stop immediately if nothing is left.
750-
/// </summary>
751-
/// <param name="taskOrCollection">The current joinableTask or collection owns the data.</param>
752-
/// <param name="reachableNodes">All reachable dependency nodes. This is not a completed list, if there is no remain node.</param>
753-
/// <param name="remainNodes">Remain dependency nodes we want to check. After the execution, it will retain non-reachable nodes.</param>
754-
private static void ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(IJoinableTaskDependent taskOrCollection, HashSet<IJoinableTaskDependent> reachableNodes, HashSet<IJoinableTaskDependent> remainNodes)
755-
{
756-
Requires.NotNull(taskOrCollection, nameof(taskOrCollection));
757-
Requires.NotNull(remainNodes, nameof(remainNodes));
758-
Requires.NotNull(reachableNodes, nameof(reachableNodes));
759-
if ((taskOrCollection as JoinableTask)?.IsFullyCompleted != true)
867+
else
760868
{
761-
if (reachableNodes.Add(taskOrCollection))
762-
{
763-
if (remainNodes.Remove(taskOrCollection) && remainNodes.Count == 0)
764-
{
765-
// no remain task left, quit the loop earlier
766-
return;
767-
}
768-
769-
WeakKeyDictionary<IJoinableTaskDependent, int>? dependencies = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes;
770-
if (dependencies is object)
771-
{
772-
foreach (KeyValuePair<IJoinableTaskDependent, int> item in dependencies)
773-
{
774-
ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(item.Key, reachableNodes, remainNodes);
775-
if (remainNodes.Count == 0)
776-
{
777-
return;
778-
}
779-
}
780-
}
781-
}
869+
syncTask.PotentialUnreachableDependents = null;
782870
}
783871
}
784872

0 commit comments

Comments
 (0)