David Rutten
David Rutten

Reputation: 4806

Does or does Task.Wait not start the task if it isn't already running?

According to the Microsoft TPL documentation I read (link) calling the Task.Wait() method will block the current thread until that task finishes (or cancels, or faults). But it also said that if the task in question hadn't started yet, the Wait method will attempt to run it on its own thread by asking the scheduler to reassign it, thus reducing the amount of wastage due to blocking.

I've got a system in which tasks (once running) begin by collecting data by starting other tasks and waiting on their results. These other tasks in turn begin by collecting data from yet other tasks and-so-on-and-so-fort, potentially a few hundred layers deep. I really don't want umpteen tasks blocking and waiting for the one task at the end to finally finish.

However when I tried this out in a test console app, Task.Wait() doesn't seem to start anything at all.

What are the correct incantations for building a sequence of tasks that must all wait on each other with a minimum of wasted cycles? It's sort of like ContinueWith, except starting with the last task in the series...

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
  class Program
  {
    static void Main(string[] args)
    {
      var source = new CancellationTokenSource();
      var token = source.Token;

      // Create a non-running task.
      var task = new Task<string[]>(() => InternalCompute(token), token);

      // Isn't this supposed to start the task?
      task.Wait(CancellationToken.None);

      // I realise this code now won't run until the task finishes,
      // it's here for when I use task.Start() instead of task.Wait().
      Console.WriteLine("Press any key to cancel the process.");
      Console.ReadKey(true);
      source.Cancel();
      Console.WriteLine("Source cancelled...");

      Console.WriteLine("Press any key to quit.");
      Console.ReadKey(true);
    }

    private static string[] InternalCompute(CancellationToken token)
    {
      string[] data;
      try
      {
        data = Compute(token);
      }
      catch (TaskCanceledException ex)
      {
        return null;
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
        return new[] { ex.Message };
      }

      Console.WriteLine("Post-processor starting.");
      for (int i = 0; i < data.Length; i++)
        if (data[i] is null)
          Console.WriteLine($"Null data at {i}.");
        else
          Console.WriteLine($"Valid data at {i}.");
      Console.WriteLine("Post-processor completed.");
      return data;
    }

    /// <summary>
    /// This method stands in for an abstract one to be implemented by plug-in developers.
    /// </summary>
    private static string[] Compute(CancellationToken token)
    {
      var data = new string[10];
      for (int i = 0; i < data.Length; i++)
      {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(250);
        data[i] = i.ToString();
        Console.WriteLine($"Computing item {i + 1}...");
      }
      return data;
    }
  }
}

Upvotes: 3

Views: 3505

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43409

This is the part of the article that caused the confusion (emphasis added).

If the Task being Wait’d on has already started execution, Wait has to block. However, if it hasn’t started executing, Wait may be able to pull the target task out of the scheduler to which it was queued and execute it inline on the current thread.

This is the description of the Task.Start method:

Starts the Task, scheduling it for execution to the current TaskScheduler.

And these are the eight different stages of a task's lifecycle:

public enum TaskStatus
{
    Created = 0, // The task has been initialized but has not yet been scheduled.
    WaitingForActivation = 1, // The task is waiting to be activated and scheduled internally by the .NET Framework infrastructure.
    WaitingToRun = 2, // The task has been scheduled for execution but has not yet begun executing.
    Running = 3, // The task is running but has not yet completed.
    WaitingForChildrenToComplete = 4, // The task has finished executing and is implicitly waiting for attached child tasks to complete.
    RanToCompletion = 5, // The task completed execution successfully.
    Canceled = 6, // The task acknowledged cancellation by throwing an OperationCanceledException with its own CancellationToken while the token was in signaled state, or the task's CancellationToken was already signaled before the task started executing.
    Faulted = 7 // The task completed due to an unhandled exception.
}

The article is talking implicitly about hot tasks that are either in the WaitingForActivation or in the WaitingToRun stage, and explains under which conditions they could be progressed internally to the Running stage when their Wait method is called. It's not talking about cold tasks in the Created stage. A task in this stage cannot be progressed without either its Start or RunSynchronously method called. In other words the .NET infrastructure never changes the temperature of a task from cold to hot automatically. This responsibility belongs 100% to the application code that created the task using the Task constructor.

As a side note, it's worth quoting this sentence from the remarks:

This constructor should only be used in advanced scenarios where it is required that the creation and starting of the task is separated.

Upvotes: 0

Damien_The_Unbeliever
Damien_The_Unbeliever

Reputation: 239646

Tasks are generally split into two groups - "cold" tasks and "hot" tasks. "cold" tasks are tasks that have not yet been started and are not meant to run yet. "hot" tasks are tasks that may or may not be currently running but, importantly, if they're not running yet, they may do so at any time. They're meant to be running but haven't yet been assigned the resource (a thread) they need to do so.

What this post is talking about is executing a "hot" task that hasn't otherwise had an opportunity to run. "hot" tasks are created by calling e.g. Task.Run(). They're also e.g. the type of Tasks you'll receive from async methods. new Task(...), on the other hand gives you "cold" tasks. Unless or until you call Start or moral equivalent methods on that task, it remains "cold". It's calling one of those methods explicitly that makes it "hot" instead of "cold".

Generally, you don't want to be working with "cold" tasks at all these days, which is why directly calling a Task constructor is frowned upon. They were really a bad experiment from before they worked out how scheduling should really work. Most modern code doesn't expect to be working with "cold" tasks at all.

The key quote from the above post is this one:

However, if it hasn’t started executing, Wait may be able to pull the target task out of the scheduler to which it was queued and execute it inline on the current thread.

If you've not called Start on the task, it hasn't been queued with a scheduler - so obviously we cannot do what the above says.

Upvotes: 7

Related Questions