C#
Task
asynchronous programming
WaitAll
debugging

Adding an anonymous Task to ListTask does not execute it after calling .WaitAll C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When you create a Task using the new Task(...) constructor and add it to a List<Task>, calling Task.WaitAll() on the list hangs indefinitely. This happens because new Task(...) creates a task in the Created state — it has not been started. Task.WaitAll() waits for all tasks to complete, but a task that was never started will never complete. The fix is to use Task.Run() instead of new Task(), which creates and immediately schedules the task for execution.

The Problem

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5var tasks = new List<Task>();
6
7// WRONG: new Task() creates an unstarted task
8tasks.Add(new Task(() => Console.WriteLine("Task 1")));
9tasks.Add(new Task(() => Console.WriteLine("Task 2")));
10tasks.Add(new Task(() => Console.WriteLine("Task 3")));
11
12// This hangs forever — tasks are in Created state, never started
13Task.WaitAll(tasks.ToArray());
14Console.WriteLine("Done"); // Never reached

The new Task() constructor creates a "cold" task. It exists but is not scheduled on the thread pool. Task.WaitAll() waits for completion, creating a deadlock against tasks that will never run.

csharp
1var tasks = new List<Task>();
2
3// Task.Run creates and starts the task immediately
4tasks.Add(Task.Run(() => Console.WriteLine("Task 1")));
5tasks.Add(Task.Run(() => Console.WriteLine("Task 2")));
6tasks.Add(Task.Run(() => Console.WriteLine("Task 3")));
7
8Task.WaitAll(tasks.ToArray());
9Console.WriteLine("Done");
10// Task 1
11// Task 2
12// Task 3
13// Done

Task.Run() creates a "hot" task — it is immediately scheduled on the thread pool and begins execution.

Fix 2: Call Start() After Creating

csharp
1var tasks = new List<Task>();
2
3var task1 = new Task(() => Console.WriteLine("Task 1"));
4var task2 = new Task(() => Console.WriteLine("Task 2"));
5
6tasks.Add(task1);
7tasks.Add(task2);
8
9// Manually start each task
10foreach (var task in tasks)
11{
12    task.Start();
13}
14
15Task.WaitAll(tasks.ToArray());
16Console.WriteLine("Done");

Calling Start() transitions the task from Created to WaitingToRun. This works but is error-prone — if you forget to call Start() on any task, WaitAll hangs.

Task States Explained

csharp
1var coldTask = new Task(() => { });
2Console.WriteLine(coldTask.Status);  // Created
3
4coldTask.Start();
5Console.WriteLine(coldTask.Status);  // WaitingToRun (or Running)
6
7coldTask.Wait();
8Console.WriteLine(coldTask.Status);  // RanToCompletion
9
10var hotTask = Task.Run(() => { });
11Console.WriteLine(hotTask.Status);   // WaitingToRun or Running (never Created)
ConstructorInitial StateNeeds Start()?
new Task(action)CreatedYes
Task.Run(action)WaitingToRunNo
Task.Factory.StartNew(action)WaitingToRunNo

Async/Await Pattern (Modern Approach)

csharp
1// Modern C# uses async/await instead of WaitAll
2async Task ProcessAllAsync()
3{
4    var tasks = new List<Task>
5    {
6        DoWorkAsync("Task 1"),
7        DoWorkAsync("Task 2"),
8        DoWorkAsync("Task 3")
9    };
10
11    await Task.WhenAll(tasks);
12    Console.WriteLine("All done");
13}
14
15async Task DoWorkAsync(string name)
16{
17    await Task.Delay(100); // Simulate async work
18    Console.WriteLine(name);
19}

Task.WhenAll() is the async equivalent of Task.WaitAll(). It returns a Task you can await without blocking the calling thread.

Task.Run with Return Values

csharp
1var tasks = new List<Task<int>>();
2
3tasks.Add(Task.Run(() => { Thread.Sleep(100); return 1; }));
4tasks.Add(Task.Run(() => { Thread.Sleep(200); return 2; }));
5tasks.Add(Task.Run(() => { Thread.Sleep(300); return 3; }));
6
7Task.WaitAll(tasks.ToArray());
8
9foreach (var task in tasks)
10{
11    Console.WriteLine($"Result: {task.Result}");
12}
13// Result: 1
14// Result: 2
15// Result: 3

Exception Handling with WaitAll

csharp
1var tasks = new List<Task>
2{
3    Task.Run(() => throw new InvalidOperationException("Error in task 1")),
4    Task.Run(() => Console.WriteLine("Task 2 OK")),
5    Task.Run(() => throw new ArgumentException("Error in task 3"))
6};
7
8try
9{
10    Task.WaitAll(tasks.ToArray());
11}
12catch (AggregateException ex)
13{
14    foreach (var inner in ex.InnerExceptions)
15    {
16        Console.WriteLine($"Caught: {inner.GetType().Name} - {inner.Message}");
17    }
18}
19// Caught: InvalidOperationException - Error in task 1
20// Caught: ArgumentException - Error in task 3

Task.WaitAll() collects all exceptions into a single AggregateException. Each faulted task's exception appears in InnerExceptions.

Common Pitfalls

  • Using new Task() instead of Task.Run(): The new Task() constructor creates an unstarted task in the Created state. Task.WaitAll() or await on this task hangs forever because it never runs. Always use Task.Run() for immediate execution.
  • Calling Start() on an already-started task: Calling Start() on a task that was created with Task.Run() or Task.Factory.StartNew() throws InvalidOperationException. Only cold tasks (from new Task()) can be started manually.
  • Using Task.WaitAll() on the UI thread: WaitAll() blocks the calling thread synchronously. On a UI thread (WPF, WinForms), this freezes the application. Use await Task.WhenAll() instead to keep the UI responsive.
  • Forgetting to handle AggregateException: Task.WaitAll() wraps all task exceptions in an AggregateException. If you catch only Exception, you get the aggregate wrapper. Access InnerExceptions to handle each failure individually.
  • Adding async lambdas to Task.Run() without awaiting: Task.Run(async () => { await SomeMethodAsync(); }) returns a Task that wraps the async lambda. If you use new Task(async () => ...), the task completes immediately because the constructor does not understand async delegates — it sees the lambda as returning void.

Summary

  • new Task(action) creates a cold (unstarted) task — Task.WaitAll() hangs because the task never executes
  • Use Task.Run(action) to create a hot (immediately scheduled) task
  • Prefer await Task.WhenAll() over Task.WaitAll() to avoid blocking the calling thread
  • If you must use new Task(), call Start() on every task before waiting
  • Handle AggregateException from WaitAll() to catch exceptions from multiple tasks
  • Task.Factory.StartNew() also creates hot tasks but has more configuration options than Task.Run()

Course illustration
Course illustration

All Rights Reserved.