0

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); } } 
16
  • 5
    Don't use async void Commented Jun 17, 2024 at 13:38
  • This did not solve the problem - even if Init is async Task it still completes immediately. Commented Jun 17, 2024 at 13:45
  • 1
    How are you calling Init()? It should be await Init(); Commented Jun 17, 2024 at 13:55
  • 1
    The Task constructor is not async-friendly (this is not the best link, but I couldn't find a better one right now). Commented Jun 17, 2024 at 14:02
  • 1
    @TheodorZoulias I wanted to! But this question is already closed and I can't add an answer anymore... Commented Jun 17, 2024 at 14:35

2 Answers 2

1

If you want to wait for tasks asynchronously, then you should not use await in any of the intermediary steps, the following simplification should work:

public class WhenAllIsNotWorking { public async Task myFunc() { await Task.Delay(10000); } // return Task, not void public async Task Init() { var tasks = new List<Task>(); for (var i = 0; i < 10; i++) { //tasks.Add(new Task(async () => { await myFunc(); })); tasks.Add(myFunc()); } // Tasks should start immediately //foreach (var task in tasks) //{ // task.Start(); //} await Task.WhenAll(tasks); } } 

Update

If the issue is that you deliberately want to prevent the start of the task until after a certain point in time, then async/await was not specifically designed to solve this.

The better pattern for this is to have a queue of objects that have the concrete implementation or a reference to your action or delegate and then you can prepare your objects and only call a Start() method on the object when you are ready to execute it.

https://dotnetfiddle.net/V9J3Np

public class WhenAllIsNotWorking { // return Task, not void public async Task Init() { Console.WriteLine("Begin Init()"); var workers = new List<DoSomeWork>(); for (var i = 0; i < 10; i++) { workers.Add(new DoSomeWork(i)); } Console.WriteLine("Workers Created..."); foreach (var worker in workers) { worker.Prepare(); } Console.WriteLine("Workers Prepared..."); Console.WriteLine("Tasks Started..."); await Task.WhenAll(workers.Select(x => x.Start())); Console.WriteLine("Tasks Completed..."); } } public class DoSomeWork { public int Id { get;set; } public bool Prepared { get; private set; } public DoSomeWork(int id) { this.Id = id; } public void Prepare() { this.Prepared = true; } public async Task Start() { await Task.Delay(1000); Console.WriteLine("Completed Task: {0}", Id); } } 

Task.WhenAll() is absolutely doing what is expected here.

You could have also used a timer or cancellation token to delay the start of the workers, but conceptually the steps are the same.

Sign up to request clarification or add additional context in comments.

7 Comments

When will my tasks actually start? I need to control the start time of these tasks.
At the moment of adding - method call starts the task.
I just tested your code - myFunc() starts immediately when added to the task list, thank you for this answer but it won't solve my use case, in which task.Start() is needed as I have to do some operations in-between before actually starting the tasks.
Make enumerable of the tasks - and feed it to Task.WhenAll, it would be a point of starting of all the tasks, internally in method implementation. Or enumerate them manually whenever you want.
Why not do those operations first? Your example was too simplified in that case. When you said "not working" you didn't mention that you were expecting a delay to start the work, that is not the problem that async/await is designed to solve.
|
0

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).

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.