Search code examples
c#multithreadingtask

Why is RunContinuationsAsynchronously an invalid continuation option for TaskFactory


I tried to create a TaskFactory instance with TaskContinuationOptions.RunContinuationsAsynchronously, so that I am sure that tasks created will have their continuations run asynchronously. However, this doesn't seem to be possible as I get an ArgumentOutOfRangeException.

TaskFactory tf = new TaskFactory(TaskCreationOptions.None, 
    TaskContinuationOptions.RunContinuationsAsynchronously);
//ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'continuationOptions')

Now I think what I wanted to achieve is done by using TaskCreationOptions.RunContinuationsAsynchronously and not TaskContinuationOptions.RunContinuationsAsynchronously. This aside. I couldn't find list of invalid options or why they are invalid in the docs.

So, why is TaskContinuationOptions.RunContinuationsAsynchronously an invalid option for TaskFactory?


Solution

  • It wasn't as obvious as I expected.

    First important thing to know is that the enum TaskCreationOptions is unofficially (not documented besides in code, hard to imagine changing) a subset of TaskContinuationOptions per this comment in the source:

    /// <summary>
    /// Specifies flags that control optional behavior for the creation and execution of tasks.
    /// </summary>
    // NOTE: These options are a subset of TaskContinuationsOptions, thus before adding a flag check it is
    // not already in use.
    

    My strong hypothesis is that to maintain this bit-compatibility the RunContinuationsAsynchronously is made part of TaskContinuationsOptions although it's really used only as TaskCreationOptions.

    For example when we use Task.ContinueWith we are passing TaskContinuationsOptions that are used simultaneously for two purposes:

    1. The subset that isTaskCreationOptions will be extracted and used in the construction of the ContinuationTaskFromTask
    2. The rest of the options are passed to constructing ContinueWithTaskContinuation which encapsulates them together with the ContinuationTaskFromTask

    This is compacted version of the source without the exception checking:

    private Task ContinueWith(Action<Task> continuationAction, TaskScheduler scheduler,
        CancellationToken cancellationToken, TaskContinuationOptions continuationOptions) {
    
        // OP Comment: 1 converison to TaskCreationOptions    
        CreationOptionsFromContinuationOptions(continuationOptions, out TaskCreationOptions creationOptions, out InternalTaskOptions internalOptions);
    
        Task continuationTask = new ContinuationTaskFromTask(
            this, continuationAction, null,
            creationOptions, internalOptions
        );
    
        // OP Comment: 2. creating the continuation which makes use of the ContinuationOptions that are just relevant for continuation
        ContinueWithCore(continuationTask, scheduler, cancellationToken, continuationOptions);
        // OP COMMENT: this calls inside new ContinueWithTaskContinuation(continuationTask, options, scheduler);
    
        return continuationTask;
    }
    

    The exception that I encountered was in TaskFactory's constructor that allows for BOTH TaskCreationOptions and TaskContinuationOptions in all of its overloads, so it treats the latter argument as "pure" TaskContinuationOptions since the user can pass TaskCreationOptions separately.

    For the why it doesn't "make sense" (per the comments in the code that throws the exception from the question) we need to look at how continuations are typically invoked.

    Task has RunContinuations method in which the original task object determines whether to prohibit synchronous execution of its continuations. (m_stateflags includes the TaskCreationOptions bits used to create the task)

    bool canInlineContinuations =
               (m_stateFlags & (int)TaskCreationOptions.RunContinuationsAsynchronously) == 0 &&
               RuntimeHelpers.TryEnsureSufficientExecutionStack();
    

    Then calls TaskContinuation.Run

    void Run(Task completedTask, bool canInlineContinuationTask)
    

    The TaskContinuationOptions of the task continuation object become relevant here, but the role of RunContinuationsAsynchronously is already done (as TaskCreationOptions, not TaskContinuationOptions) in helping to determine the value of the argument canInlineContinuationTask.

    Also per Panagiotis Kanavos multiple comments, the value of TaskContinuationOptions.None already did officially define the asynchronous case for TaskContinuationOptions:

    The continuation runs asynchronously when the antecedent task completes, regardless of the antecedent's final Status property value

    So why add a redundant option if not to maintain that TaskContinuationOptions is unofficially a superset of TaskCreationOptions and can serve as both in method signatures such as shown above for ContinueWith?!

    As a final detail asynchronous/synchronous don't mean map 1 to 1 with "on a different thread", "same thread". As Panagiotis Kanavos commented, we are at the mercy of the specific TaskScheduler (the task is associated with) in whether the continuation will be on the same thread or not (in case of single-threaded TaskScheduler implementation e.g.)

    So asynchronous means - queued, synchronous - ran inline. This is clearly visible in the names used in the ContinueWithTaskContinuation.Run method which was last described.

    // Either run directly or just queue it up for execution, depending
    // on whether synchronous or asynchronous execution is wanted.
    if (canInlineContinuationTask && // inlining is allowed by the caller
        (options & TaskContinuationOptions.ExecuteSynchronously) != 0) // synchronous execution was requested by the continuation's creator
    {
        InlineIfPossibleOrElseQueue(continuationTask, needsProtection: true);
    } else {
        try { continuationTask.ScheduleAndStart(needsProtection: true); } catch (TaskSchedulerException) {
            // No further action is necessary -- ScheduleAndStart() already transitioned the
            // task to faulted.  But we want to make sure that no exception is thrown from here.
        }
    }