From db38bd6f8844675133bc8a5a518d628ee9f57305 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 30 Mar 2020 10:51:09 -0400 Subject: [PATCH] Add Task.WhenAny(task, task) overload Currently internal and used as an implementation detail under Task.WhenAny(params Task[]) as well as from SemaphoreSlim. Once API reviewed, it can be made public. --- .../src/System/Threading/SemaphoreSlim.cs | 4 +- .../src/System/Threading/Tasks/Task.cs | 143 ++++++++++++++++-- 2 files changed, 135 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs index ae4494e2031ade..5459bee1f73d13 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @@ -718,7 +718,7 @@ private async Task WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int // cancel, and we chain the caller's supplied token into it. using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { - if (asyncWaiter == await TaskFactory.CommonCWAnyLogic(new Task[] { asyncWaiter, Task.Delay(millisecondsTimeout, cts.Token) }).ConfigureAwait(false)) + if (asyncWaiter == await Task.WhenAny(asyncWaiter, Task.Delay(millisecondsTimeout, cts.Token)).ConfigureAwait(false)) { cts.Cancel(); // ensure that the Task.Delay task is cleaned up return true; // successfully acquired @@ -731,7 +731,7 @@ private async Task WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int var cancellationTask = new Task(null, TaskCreationOptions.RunContinuationsAsynchronously, promiseStyle: true); using (cancellationToken.UnsafeRegister(s => ((Task)s!).TrySetResult(), cancellationTask)) { - if (asyncWaiter == await TaskFactory.CommonCWAnyLogic(new Task[] { asyncWaiter, cancellationTask }).ConfigureAwait(false)) + if (asyncWaiter == await Task.WhenAny(asyncWaiter, cancellationTask).ConfigureAwait(false)) { return true; // successfully acquired } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs index 59bd088ab4bc97..229de2dfafaaf4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @@ -4295,14 +4295,7 @@ internal void ContinueWithCore(Task continuationTask, // Adds a lightweight completion action to a task. This is similar to a continuation // task except that it is stored as an action, and thus does not require the allocation/ // execution resources of a continuation task. - // - // Used internally by ContinueWhenAll() and ContinueWhenAny(). - internal void AddCompletionAction(ITaskCompletionAction action) - { - AddCompletionAction(action, addBeforeOthers: false); - } - - internal void AddCompletionAction(ITaskCompletionAction action, bool addBeforeOthers) + internal void AddCompletionAction(ITaskCompletionAction action, bool addBeforeOthers = false) { if (!AddTaskContinuation(action, addBeforeOthers)) action.Invoke(this); // run the action directly if we failed to queue the continuation (i.e., the task completed) @@ -5956,7 +5949,16 @@ public void Invoke(Task ignored) /// public static Task WhenAny(params Task[] tasks) { - if (tasks == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.tasks); + if (tasks == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.tasks); + } + + if (tasks.Length == 2) + { + return WhenAny(tasks[0], tasks[1]); + } + if (tasks.Length == 0) { ThrowHelper.ThrowArgumentException(ExceptionResource.Task_MultiTaskContinuation_EmptyTaskList, ExceptionArgument.tasks); @@ -5977,6 +5979,104 @@ public static Task WhenAny(params Task[] tasks) return TaskFactory.CommonCWAnyLogic(tasksCopy); } + // TODO https://github.com/dotnet/runtime/issues/23021: Make this public. + /// Creates a task that will complete when either of the supplied tasks have completed. + /// The first task to wait on for completion. + /// The second task to wait on for completion. + /// A task that represents the completion of one of the supplied tasks. The return Task's Result is the task that completed. + /// + /// The returned task will complete when any of the supplied tasks has completed. The returned task will always end in the RanToCompletion state + /// with its Result set to the first task to complete. This is true even if the first task to complete ended in the Canceled or Faulted state. + /// + /// + /// The or argument was null. + /// + internal static Task WhenAny(Task task1, Task task2) => + (task1 is null) || (task2 is null) ? throw new ArgumentNullException(task1 is null ? nameof(task1) : nameof(task2)) : + task1.IsCompleted ? FromResult(task1) : + task2.IsCompleted ? FromResult(task2) : + new TwoTaskWhenAnyPromise(task1, task2); + + /// A promise type used by WhenAny to wait on exactly two tasks. + /// Specifies the type of the task. + /// + /// This has essentially the same logic as , but optimized + /// for two tasks rather than any number. Exactly two tasks has shown to be the most common use-case by far. + /// + private sealed class TwoTaskWhenAnyPromise : Task, ITaskCompletionAction where TTask : Task + { + private TTask? _task1, _task2; + + /// Instantiate the promise and register it with both tasks as a completion action. + public TwoTaskWhenAnyPromise(TTask task1, TTask task2) + { + Debug.Assert(task1 != null && task2 != null); + _task1 = task1; + _task2 = task2; + + if (AsyncCausalityTracer.LoggingOn) + { + AsyncCausalityTracer.TraceOperationCreation(this, "Task.WhenAny"); + } + + if (s_asyncDebuggingEnabled) + { + AddToActiveTasks(this); + } + + task1.AddCompletionAction(this); + + task2.AddCompletionAction(this); + if (task1.IsCompleted) + { + // If task1 has already completed, Invoke may have tried to remove the continuation from + // each task before task2 added the continuation, in which case it's now referencing the + // already completed continuation. To deal with that race condition, explicitly check + // and remove the continuation here. + task2.RemoveContinuation(this); + } + } + + /// Completes this task when one of the constituent tasks completes. + public void Invoke(Task completingTask) + { + Task? task1; + if ((task1 = Interlocked.Exchange(ref _task1, null)) != null) + { + Task? task2 = _task2; + _task2 = null; + + Debug.Assert(task1 != null && task2 != null); + Debug.Assert(task1.IsCompleted || task2.IsCompleted); + + if (AsyncCausalityTracer.LoggingOn) + { + AsyncCausalityTracer.TraceOperationRelation(this, CausalityRelation.Choice); + AsyncCausalityTracer.TraceOperationCompletion(this, AsyncCausalityStatus.Completed); + } + + if (s_asyncDebuggingEnabled) + { + RemoveFromActiveTasks(this); + } + + if (!task1.IsCompleted) + { + task1.RemoveContinuation(this); + } + else + { + task2.RemoveContinuation(this); + } + + bool success = TrySetResult((TTask)completingTask); + Debug.Assert(success, "Only one task should have gotten to this point, and thus this must be successful."); + } + } + + public bool InvokeMayRunArbitraryCode => true; + } + /// /// Creates a task that will complete when any of the supplied tasks have completed. /// @@ -6035,14 +6135,37 @@ public static Task> WhenAny(params Task[] tasks) // return (Task>) WhenAny( (Task[]) tasks); // but classes are not covariant to enable casting Task to Task>. + if (tasks != null && tasks.Length == 2) + { + return WhenAny(tasks[0], tasks[1]); + } + // Call WhenAny(Task[]) for basic functionality - Task intermediate = WhenAny((Task[])tasks); + Task intermediate = WhenAny((Task[])tasks!); // Return a continuation task with the correct result type return intermediate.ContinueWith(Task.TaskWhenAnyCast.Value, default, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default); } + // TODO https://github.com/dotnet/runtime/issues/23021: Make this public. + /// Creates a task that will complete when either of the supplied tasks have completed. + /// The first task to wait on for completion. + /// The second task to wait on for completion. + /// A task that represents the completion of one of the supplied tasks. The return Task's Result is the task that completed. + /// + /// The returned task will complete when any of the supplied tasks has completed. The returned task will always end in the RanToCompletion state + /// with its Result set to the first task to complete. This is true even if the first task to complete ended in the Canceled or Faulted state. + /// + /// + /// The or argument was null. + /// + internal static Task> WhenAny(Task task1, Task task2) => + (task1 is null) || (task2 is null) ? throw new ArgumentNullException(task1 is null ? nameof(task1) : nameof(task2)) : + task1.IsCompleted ? FromResult(task1) : + task2.IsCompleted ? FromResult(task2) : + new TwoTaskWhenAnyPromise>(task1, task2); + /// /// Creates a task that will complete when any of the supplied tasks have completed. ///