Reputation: 4806
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
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 currentTaskScheduler
.
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
Reputation: 239646
Task
s 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 Task
s 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