Search code examples
tasktask-parallel-librarycontinuations

My continuation task appears to be complete before it finishes running


I have a non-intuitive issue with a continuation task that I would think task.Wait would wait for the continuation task but it yields RanToCompletion before the task even finishes running? Here is the short source code. The output is below:

private static void TestChildTasks()
{
    Task t = Task.Run(() => RunParentTask());
    Task t2 = t.ContinueWith(task => Task.Run(() => RunChildTask()));
    //Task t2 = Task.Run(() => RunChildTask());
    Console.WriteLine("Waiting on t1");
    t.Wait();
    Console.WriteLine("Done waiting on t1");

    Console.WriteLine($"Waiting on t2, status of {t2.Status}");
    t2.Wait();

    Console.WriteLine($"Finished; child task is {t2.Status}");
}

private static void RunParentTask()
{
    Console.WriteLine("Parent Task is running");
    Thread.Sleep(2000);
    Console.WriteLine("Parent Task is done");
}

private static void RunChildTask()
{
    Console.WriteLine("Child task is running");
    Thread.Sleep(3000);
    Console.WriteLine("Child Task is done");
}

Here is the output:

Waiting on t1

Parent Task is running

Parent Task is done

Done waiting on t1

Waiting on t2, status of Running

Finished; child task is RanToCompletion

press enter to exit

Child task is running

Child Task is done

Why does the child task continue to run after it returns a status of RanToCompletion?


Solution

  • Lets look at the following line:

    Task t2 = t.ContinueWith(task => Task.Run(() => RunChildTask()));
    

    Even though you have declared t2 as Task it is really a Task<Task>:

    Task<Task> t2 = t.ContinueWith(task => Task.Run(() => RunChildTask()));
    

    Why? Because Task.Run creates a new Task.

    You have several options to fix this:

    Option #1 - Unwrap

    To make a Task from a Task<Task> you need to call the Unwrap method.

    Task t2 = t.ContinueWith(task => Task.Run(() => RunChildTask())).Unwrap();
    

    With this modification the output will look like this:

    Waiting on t1
    Parent Task is running
    Parent Task is done
    Done waiting on t1
    Child task is running
    Waiting on t2, status of WaitingForActivation
    Child Task is done
    Finished; child task is RanToCompletion
    

    Option #2 - Avoid new task creation

    Actually you can call the RunChildTask without the Task.Run:

    Task t2 = t.ContinueWith(_ => RunChildTask());
    

    With this modification the output will look like this:

    Waiting on t1
    Parent Task is running
    Parent Task is done
    Done waiting on t1
    Child task is running
    Waiting on t2, status of Running
    Child Task is done
    Finished; child task is RanToCompletion
    

    Option #3 - AttachToParent

    If you want to you can attach a Task not just with the ContinueWith, but also with the Task.Factory.StartNew:

    Task t2 = null;
    Task t = Task.Factory.StartNew(() =>
    {
        RunParentTask();
        t2 = Task.Factory.StartNew(RunChildTask, TaskCreationOptions.AttachedToParent);
    });
    

    With this modification the output will look like this:

    Waiting on t1
    Parent Task is running
    Parent Task is done
    Child task is running
    Child Task is done
    Done waiting on t1
    Waiting on t2, status of RanToCompletion
    Finished; child task is RanToCompletion
    

    There are several other ways as well.