Home > Software design >  Why Task.WhenAll requires a manually created Task to be started when the same doesn't require f
Why Task.WhenAll requires a manually created Task to be started when the same doesn't require f

Time:10-13

Ex, the following code manually instantiates a Task and passes to a Task.WhenAll in a List<T>

public async Task Do3()
{
    var task1 = new Task(async () => { await Task.Delay(2000); Console.WriteLine("########## task1"); });

    var taskList = new List<Task>() { task1};

    taskList[0].Start();

    var taskDone = Task.WhenAll(taskList);
    await taskDone;
}

without starting the Task it doesn't work, it hangs forever calling from a console app, but the below works just fine without starting it

public async Task Do3()
{
    //var task1 = new Task(async () => { await Task.Delay(2000); Console.WriteLine("########## task1"); });

    var taskList = new List<Task>() { SubDo1() };

    //taskList[0].Start();

    var taskDone = Task.WhenAll(taskList);
    await taskDone;
}

public async Task SubDo1()
{
    await Task.Delay(2000);
    Console.WriteLine("########## task1");
}

CodePudding user response:

Task is used in two completely different ways here; when you call an async method: you are starting it yourself; at this point, two things can happen:

  1. it can run to completion (eventually) without ever reaching a truly asynchronous state, and return a completed (or faulted) task to the caller
  2. it can reach an incomplete awaitable (in this case await Task.Delay), at which point it creates a state machine that represents the current position, schedules a completion operation on that incomplete awaitable (to do whatever comes next), and then returns an incomplete task to the caller

It is not "not started"; to return anything to the caller: we have started it. However, unlike Task.Start(), we start that work on our current thread - not an external worker thread - with other threads only getting involved based on how that incomplete awaitable schedules the completion callbacks that the compiler gives it.

This is very different to the new Task(...) scenario, where nothing is initially started. That's why they behave differently. Note also the Remarks section of the Task constructor here - it is a very niche API, and honestly: not hugely recommended.

Additionally: when you don't immediately await an async method, you're essentially going into concurrent territory (assuming the awaitable won't always complete synchronously). In some cases, this matters, and may cause threading problems re race-conditions. It shouldn't matter much in this case, though.

  • Related