Search code examples
c#async-await

Task.WhenAll is not waiting


I have below code, which should wait 10seconds. Problem is that it completes immediately, the WhenAll method is not working - what am I doing wrong here?

public class WhenAllIsNotWorking
{
    public async Task myFunc()
    {
        await Task.Delay(10000);
    }

    public async void Init()
    {
        var tasks = new List<Task>();
        for (var i = 0; i < 10; i++)
        {
            tasks.Add(new Task(async () => { await myFunc();  }));
        }
        foreach (var task in tasks)
        {
            task.Start();
        }
        await Task.WhenAll(tasks);
    }
}

Edit, as I didn't mention this originally - above is oversimplified example of my real code - in reality I have a hierarchical tree of entities which I first traverse and register operations per entity (thus why I use new Task() with combination of task.Start()). Once I register all the operations, I then group them, and later do task.Start() on them which allow me to execute operations in ordered way per entity type. Of course that's what I would like to do, if it wasn't for the fact that WhenAll is not doing it's job here.

My solution, someone closed my question and I can't post answers anymore, anyhow, here is what I ended up doing - thanks for all your help!

public class WhenAllIsNotWorking
{
    public async Task myFunc()
    {
        await Task.Delay(10000);
    }

    public async Task Init()
    {
        var tasks = new List<Func<Task>>();
        for (var i = 0; i < 10; i++)
        {
            tasks.Add(async () => { await myFunc();  });
        }
        var waitList = new List<Task>();
        foreach (var task in tasks)
        {
            waitList.Add(Task.Run(task));
        }
        await Task.WhenAll(waitList);
    }
}

Solution

  • Here is how to do it:

    public async Task Init()
    {
        List<Task<Task>> tasks = new();
    
        for (int i = 0; i < 10; i++)
        {
            tasks.Add(new Task<Task>(() => myFunc()));
        }
    
        // Do some operations in-between, before actually starting the tasks.
    
        foreach (Task<Task> taskTask in tasks)
        {
            taskTask.Start(TaskScheduler.Default);
        }
    
        // Wait for all the tasks to complete
        await Task.WhenAll(tasks.Select(t => t.Unwrap()));
    }
    

    Key points:

    1. The non generic Task is not async-friendly. It doesn't know what to do with an async delegate. The delegate end-up being async void, which is something to avoid.
    2. You can assign an async delegate to a Task<Task>. This is a nested task the represent the launching of the async operation. It doesn't represent the completion of the async operation. The outer task will complete immediately after the launching of the async operation. The inner task will complete when the async operation completes.
    3. You can use the Unwrap method to create a new proxy Task that represent both the launching and the completion of the async operation.
    4. Microsoft recommends to configure always the scheduler argument, whenever you start a Task with the StartNew and ContinueWith APIs. The same recommendation applies to the Start method. Otherwise your task will be scheduled on the ambient TaskScheduler.Current, which makes your code dependent on the ambient environment (not a good idea in general).