Improve ContinueWith perf with NotOn* options (#35575)
authorStephen Toub <stoub@microsoft.com>
Sat, 2 May 2020 20:42:39 +0000 (16:42 -0400)
committerGitHub <noreply@github.com>
Sat, 2 May 2020 20:42:39 +0000 (16:42 -0400)
* Delete dead code from Task.InternalCancel

The bCancelNonExecutingOnly argument was always false.

* Rename StandardTaskContinuation to ContinueWithTaskContinuation

* Remove unused return value from InternalCancel

* Combine two overloads

There's no point in these being separate, with one being small and only and always called from the other.

* Streamline ContinueWith completions for NotOn*

We can avoid unnecessarily instantiating the contingent properties bag, as well as a few interlocked operations.

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs

index 229de2d..61f6e4a 100644 (file)
@@ -609,7 +609,7 @@ namespace System.Threading.Tasks
                     if (cancellationToken.IsCancellationRequested)
                     {
                         // Fast path for an already-canceled cancellationToken
-                        this.InternalCancel(false);
+                        InternalCancel();
                     }
                     else
                     {
@@ -618,7 +618,7 @@ namespace System.Threading.Tasks
                         if (antecedent == null)
                         {
                             // if no antecedent was specified, use this task's reference as the cancellation state object
-                            ctr = cancellationToken.UnsafeRegister(t => ((Task)t!).InternalCancel(false), this);
+                            ctr = cancellationToken.UnsafeRegister(t => ((Task)t!).InternalCancel(), this);
                         }
                         else
                         {
@@ -635,7 +635,7 @@ namespace System.Threading.Tasks
                                 Task antecedentTask = tuple.Item2;
 
                                 antecedentTask.RemoveContinuation(tuple.Item3);
-                                targetTask.InternalCancel(false);
+                                targetTask.InternalCancel();
                             },
                             new Tuple<Task, Task, TaskContinuation>(this, antecedent, continuation));
                         }
@@ -2896,98 +2896,86 @@ namespace System.Threading.Tasks
         /// <summary>
         /// Cancels the <see cref="Task"/>.
         /// </summary>
-        /// <param name="bCancelNonExecutingOnly">
-        /// Indicates whether we should only cancel non-invoked tasks.
-        /// For the default scheduler this option will only be serviced through TryDequeue.
-        /// For custom schedulers we also attempt an atomic state transition.
-        /// </param>
-        /// <returns>true if the task was successfully canceled; otherwise, false.</returns>
-        internal bool InternalCancel(bool bCancelNonExecutingOnly)
+        internal void InternalCancel()
         {
             Debug.Assert((Options & (TaskCreationOptions)InternalTaskOptions.PromiseTask) == 0, "Task.InternalCancel() did not expect promise-style task");
 
-            bool bPopSucceeded = false;
-            bool mustCleanup = false;
-
             TaskSchedulerException? tse = null;
+            bool popped = false;
 
             // If started, and running in a task context, we can try to pop the chore.
             if ((m_stateFlags & TASK_STATE_STARTED) != 0)
             {
                 TaskScheduler? ts = m_taskScheduler;
-
                 try
                 {
-                    bPopSucceeded = (ts != null) && ts.TryDequeue(this);
+                    popped = (ts != null) && ts.TryDequeue(this);
                 }
                 catch (Exception e)
                 {
                     // TryDequeue threw. We don't know whether the task was properly dequeued or not. So we must let the rest of
                     // the cancellation logic run its course (record the request, attempt atomic state transition and do cleanup where appropriate)
                     // Here we will only record a TaskSchedulerException, which will later be thrown at function exit.
-
                     tse = new TaskSchedulerException(e);
                 }
+            }
 
-                bool bRequiresAtomicStartTransition = ts != null && ts.RequiresAtomicStartTransition;
+            // Record the cancellation request.
+            RecordInternalCancellationRequest();
 
-                if (!bPopSucceeded && bCancelNonExecutingOnly && bRequiresAtomicStartTransition)
-                {
-                    // The caller requested cancellation of non-invoked tasks only, and TryDequeue was one way of doing it...
-                    // Since that seems to have failed, we should now try an atomic state transition (from non-invoked state to canceled)
-                    // An atomic transition here is only safe if we know we're on a custom task scheduler, which also forces a CAS on ExecuteEntry
-
-                    // Even though this task can't have any children, we should be ready for handling any continuations that
-                    // may be attached to it (although currently
-                    // So we need to remeber whether we actually did the flip, so we can do clean up (finish continuations etc)
-                    mustCleanup = AtomicStateUpdate(TASK_STATE_CANCELED, TASK_STATE_DELEGATE_INVOKED | TASK_STATE_CANCELED);
-
-                    // PS: This is slightly different from the regular cancellation codepath
-                    // since we record the cancellation request *after* doing the state transition.
-                    // However that shouldn't matter too much because the task was never invoked, thus can't have children
-                }
+            // Determine whether we need to clean up
+            // This will be the case
+            //     1) if we were able to pop, and we are able to update task state to TASK_STATE_CANCELED
+            //     2) if the task seems to be yet unstarted, and we can transition to
+            //        TASK_STATE_CANCELED before anyone else can transition into _STARTED or _CANCELED or
+            //        _RAN_TO_COMPLETION or _FAULTED
+            // Note that we do not check for TASK_STATE_COMPLETION_RESERVED.  That only applies to promise-style
+            // tasks, and a promise-style task should not enter into this codepath.
+            bool mustCleanup = false;
+            if (popped)
+            {
+                // Include TASK_STATE_DELEGATE_INVOKED in "illegal" bits to protect against the situation where
+                // TS.TryDequeue() returns true but the task is still left on the queue.
+                mustCleanup = AtomicStateUpdate(TASK_STATE_CANCELED, TASK_STATE_CANCELED | TASK_STATE_DELEGATE_INVOKED);
             }
-
-            if (!bCancelNonExecutingOnly || bPopSucceeded || mustCleanup)
+            else if ((m_stateFlags & TASK_STATE_STARTED) == 0)
             {
-                // Record the cancellation request.
-                RecordInternalCancellationRequest();
-
-                // Determine whether we need to clean up
-                // This will be the case
-                //     1) if we were able to pop, and we are able to update task state to TASK_STATE_CANCELED
-                //     2) if the task seems to be yet unstarted, and we can transition to
-                //        TASK_STATE_CANCELED before anyone else can transition into _STARTED or _CANCELED or
-                //        _RAN_TO_COMPLETION or _FAULTED
-                // Note that we do not check for TASK_STATE_COMPLETION_RESERVED.  That only applies to promise-style
-                // tasks, and a promise-style task should not enter into this codepath.
-                if (bPopSucceeded)
-                {
-                    // hitting this would mean something wrong with the AtomicStateUpdate above
-                    Debug.Assert(!mustCleanup, "Possibly an invalid state transition call was made in InternalCancel()");
-
-                    // Include TASK_STATE_DELEGATE_INVOKED in "illegal" bits to protect against the situation where
-                    // TS.TryDequeue() returns true but the task is still left on the queue.
-                    mustCleanup = AtomicStateUpdate(TASK_STATE_CANCELED, TASK_STATE_CANCELED | TASK_STATE_DELEGATE_INVOKED);
-                }
-                else if (!mustCleanup && (m_stateFlags & TASK_STATE_STARTED) == 0)
-                {
-                    mustCleanup = AtomicStateUpdate(TASK_STATE_CANCELED,
-                        TASK_STATE_CANCELED | TASK_STATE_STARTED | TASK_STATE_RAN_TO_COMPLETION |
-                        TASK_STATE_FAULTED | TASK_STATE_DELEGATE_INVOKED);
-                }
+                mustCleanup = AtomicStateUpdate(TASK_STATE_CANCELED,
+                    TASK_STATE_CANCELED | TASK_STATE_STARTED | TASK_STATE_RAN_TO_COMPLETION |
+                    TASK_STATE_FAULTED | TASK_STATE_DELEGATE_INVOKED);
+            }
 
-                // do the cleanup (i.e. set completion event and finish continuations)
-                if (mustCleanup)
-                {
-                    CancellationCleanupLogic();
-                }
+            // do the cleanup (i.e. set completion event and finish continuations)
+            if (mustCleanup)
+            {
+                CancellationCleanupLogic();
             }
 
             if (tse != null)
+            {
                 throw tse;
-            else
-                return mustCleanup;
+            }
+        }
+
+        /// <summary>
+        /// Cancels a ContinueWith task as part of determining to see whether the continuation should run.
+        /// This is only valid if the ContinueWith has a default token associated with it.
+        /// </summary>
+        internal void InternalCancelContinueWithInitialState()
+        {
+            // There isn't a cancellation token assigned to the task, which means the task is still going to be in its
+            // initial state.  The only way it could have transitioned is if:
+            // - it was Start'd, which isn't valid for a continuation
+            // - it was canceled, which won't have happened without a token
+            // - it was run as a continuation, which won't have happened because this method is only invoked once
+            // As a result, we can take an optimized path that avoids inflating contingent properties.
+            const int IllegalFlags = TASK_STATE_STARTED | TASK_STATE_COMPLETED_MASK | TASK_STATE_DELEGATE_INVOKED;
+            Debug.Assert((m_stateFlags & IllegalFlags) == 0, "The continuation was in an invalid state.");
+            Debug.Assert((m_stateFlags & TASK_STATE_WAITINGFORACTIVATION) != 0, "Expected continuation to be waiting for activation");
+            Debug.Assert(m_contingentProperties is null || m_contingentProperties.m_cancellationToken == default);
+
+            m_stateFlags |= TASK_STATE_CANCELED; // no synchronization necessary, per above comment
+            CancellationCleanupLogic();
         }
 
         // Breaks out logic for recording a cancellation request
@@ -3000,11 +2988,11 @@ namespace System.Threading.Tasks
         // Breaks out logic for recording a cancellation request
         // This overload should only be used for promise tasks where no cancellation token
         // was supplied when the task was created.
-        internal void RecordInternalCancellationRequest(CancellationToken tokenToRecord)
+        internal void RecordInternalCancellationRequest(CancellationToken tokenToRecord, object? cancellationException)
         {
-            RecordInternalCancellationRequest();
-
             Debug.Assert((Options & (TaskCreationOptions)InternalTaskOptions.PromiseTask) != 0, "Task.RecordInternalCancellationRequest(CancellationToken) only valid for promise-style task");
+
+            RecordInternalCancellationRequest();
             Debug.Assert(m_contingentProperties!.m_cancellationToken == default);
 
             // Store the supplied cancellation token as this task's token.
@@ -3013,14 +3001,6 @@ namespace System.Threading.Tasks
             {
                 m_contingentProperties.m_cancellationToken = tokenToRecord;
             }
-        }
-
-        // Breaks out logic for recording a cancellation request
-        // This overload should only be used for promise tasks where no cancellation token
-        // was supplied when the task was created.
-        internal void RecordInternalCancellationRequest(CancellationToken tokenToRecord, object? cancellationException)
-        {
-            RecordInternalCancellationRequest(tokenToRecord);
 
             // Store the supplied cancellation exception
             if (cancellationException != null)
@@ -3288,7 +3268,7 @@ namespace System.Threading.Tasks
                         // The continuation was unregistered and null'd out, so just skip it.
                         continue;
                     }
-                    else if (currentContinuation is StandardTaskContinuation stc)
+                    else if (currentContinuation is ContinueWithTaskContinuation stc)
                     {
                         if ((stc.m_options & TaskContinuationOptions.ExecuteSynchronously) == 0)
                         {
@@ -4226,7 +4206,7 @@ namespace System.Threading.Tasks
             Debug.Assert(!continuationTask.IsCompleted, "Did not expect continuationTask to be completed");
 
             // Create a TaskContinuation
-            TaskContinuation continuation = new StandardTaskContinuation(continuationTask, options, scheduler);
+            TaskContinuation continuation = new ContinueWithTaskContinuation(continuationTask, options, scheduler);
 
             // If cancellationToken is cancellable, then assign it.
             if (cancellationToken.CanBeCanceled)
index e2cf8a4..b4571cf 100644 (file)
@@ -257,10 +257,10 @@ namespace System.Threading.Tasks
     }
 
     /// <summary>Provides the standard implementation of a task continuation.</summary>
-    internal class StandardTaskContinuation : TaskContinuation
+    internal sealed class ContinueWithTaskContinuation : TaskContinuation
     {
         /// <summary>The unstarted continuation task.</summary>
-        internal readonly Task m_task;
+        internal Task? m_task;
         /// <summary>The options to use with the continuation task.</summary>
         internal readonly TaskContinuationOptions m_options;
         /// <summary>The task scheduler with which to run the continuation task.</summary>
@@ -270,7 +270,7 @@ namespace System.Threading.Tasks
         /// <param name="task">The task to be activated.</param>
         /// <param name="options">The continuation options.</param>
         /// <param name="scheduler">The scheduler to use for the continuation.</param>
-        internal StandardTaskContinuation(Task task, TaskContinuationOptions options, TaskScheduler scheduler)
+        internal ContinueWithTaskContinuation(Task task, TaskContinuationOptions options, TaskScheduler scheduler)
         {
             Debug.Assert(task != null, "TaskContinuation ctor: task is null");
             Debug.Assert(scheduler != null, "TaskContinuation ctor: scheduler is null");
@@ -292,6 +292,10 @@ namespace System.Threading.Tasks
             Debug.Assert(completedTask != null);
             Debug.Assert(completedTask.IsCompleted, "ContinuationTask.Run(): completedTask not completed");
 
+            Task? continuationTask = m_task;
+            Debug.Assert(continuationTask != null);
+            m_task = null;
+
             // Check if the completion status of the task works with the desired
             // activation criteria of the TaskContinuationOptions.
             TaskContinuationOptions options = m_options;
@@ -303,7 +307,6 @@ namespace System.Threading.Tasks
                         (options & TaskContinuationOptions.NotOnFaulted) == 0);
 
             // If the completion status is allowed, run the continuation.
-            Task continuationTask = m_task;
             if (isRightKind)
             {
                 // If the task was cancel before running (e.g a ContinueWhenAll with a cancelled caancelation token)
@@ -333,20 +336,29 @@ namespace System.Threading.Tasks
                     }
                 }
             }
-            // Otherwise, the final state of this task does not match the desired
-            // continuation activation criteria; cancel it to denote this.
-            else continuationTask.InternalCancel(false);
-        }
-
-        internal override Delegate[]? GetDelegateContinuationsForDebugger()
-        {
-            if (m_task.m_action == null)
+            else
             {
-                return m_task.GetDelegateContinuationsForDebugger();
+                // Otherwise, the final state of this task does not match the desired continuation activation criteria; cancel it to denote this.
+                Task.ContingentProperties? cp = continuationTask.m_contingentProperties; // no need to volatile read, as we only care about the token, which is only assignable at construction
+                if (cp is null || cp.m_cancellationToken == default)
+                {
+                    // With no cancellation token, use an optimized path that doesn't need to account for concurrent completion.
+                    // This is primarily valuable for continuations created with TaskContinuationOptions.NotOn* options, where
+                    // we'll cancel the continuation if it's not needed.
+                    continuationTask.InternalCancelContinueWithInitialState();
+                }
+                else
+                {
+                    // There's a non-default token.  Follow the normal internal cancellation path.
+                    continuationTask.InternalCancel();
+                }
             }
-
-            return new Delegate[] { m_task.m_action };
         }
+
+        internal override Delegate[]? GetDelegateContinuationsForDebugger() =>
+            m_task is null ? null :
+            m_task.m_action is null ? m_task.GetDelegateContinuationsForDebugger() :
+            new Delegate[] { m_task.m_action };
     }
 
     /// <summary>Task continuation for awaiting with a current synchronization context.</summary>