A Task represents an ongoing operation — a unit of work that may run asynchronously. It is the foundation of modern .NET concurrency and sits at a higher abstraction level than raw Thread objects.
Task— represents an operation with no return value.Task<T>— represents an operation that produces a value of typeT.
Task.Run(...) submits the work to the ThreadPool, which reuses threads instead of creating new OS threads for every request.
Project: ReturningResultsFromTask
Split an array into segments, compute each segment's sum in parallel on a separate Task<int>, then aggregate the results.
int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int SumSegment(int start, int end)
{
int segmentSum = 0;
for (int i = start; i < end; i++)
{
Thread.Sleep(100); // simulate CPU work
segmentSum += array[i];
}
return segmentSum;
}
Task<int>[] tasks = new Task<int>[4];
tasks[0] = Task.Run(() => SumSegment(0, 2));
tasks[1] = Task.Run(() => SumSegment(2, 5));
tasks[2] = Task.Run(() => SumSegment(5, 7));
tasks[3] = Task.Run(() => SumSegment(7, 10));
Console.WriteLine($"Sum: {tasks.Sum(t => t.Result)}");.Result blocks the calling thread until the task completes and then returns its value. Avoid calling .Result on the UI thread or inside an async method — use await instead to avoid deadlocks.
- CPU-bound divide-and-conquer work.
- Parallel data processing where each chunk produces a result to be merged.
Project: TaskContinuation-ContinueWith
A continuation is a callback scheduled to run when a preceding task finishes. It receives the completed task as its argument, giving access to its result and status.
using var client = new HttpClient();
var task = client.GetStringAsync("https://pokeapi.co/api/v2/pokemon");
task.ContinueWith(t =>
{
var result = t.Result;
var doc = JsonDocument.Parse(result);
var firstPokemon = doc.RootElement
.GetProperty("results")[0];
Console.WriteLine($"Name: {firstPokemon.GetProperty("name")}");
Console.WriteLine($"URL: {firstPokemon.GetProperty("url")}");
});
Console.WriteLine("This line runs before the HTTP call finishes.");- The continuation runs on a ThreadPool thread, not the calling thread.
- The preceding task's result is available through
t.Resultinside the continuation. - Prefer
async/awaitfor new code — continuations are the lower-level building block thatawaitis built on.
- When you need fine-grained control over continuation scheduling (
TaskContinuationOptions). - When you cannot use
async/await(e.g., constructors, legacy code).
Project: TaskConinuation-Chain&Unwrap
Continuations can be chained: the output of one feeds the input of the next. When a continuation itself returns a Task, you get a Task<Task<T>>. .Unwrap() flattens it into a single Task<T>.
var taskPokemonListJson = client.GetStringAsync("https://pokeapi.co/api/v2/pokemon");
var taskGetFirstUrl = taskPokemonListJson.ContinueWith(t =>
{
// parse JSON and return the first Pokemon's URL
return firstPokemon.GetProperty("url").ToString();
});
var taskDetails = taskGetFirstUrl.ContinueWith(t =>
{
// t.Result is the URL string — start another HTTP call
return client.GetStringAsync(t.Result); // returns Task<Task<string>>
}).Unwrap(); // flatten to Task<string>
taskDetails.ContinueWith(t =>
{
var doc = JsonDocument.Parse(t.Result);
Console.WriteLine($"Name: {doc.RootElement.GetProperty("name")}");
Console.WriteLine($"Weight: {doc.RootElement.GetProperty("weight")}");
});ContinueWith(t => SomeAsyncMethod()) produces a Task<Task<TResult>> — an outer task that wraps an inner task. .Unwrap() gives you a single Task<TResult> that completes when the inner task completes.
- Chaining multiple asynchronous HTTP calls without
async/await. - Building pipeline patterns (fetch → parse → fetch details → display).
Project: TaskContinuation-Wait-WaitAll-Result
These methods block the calling thread until a task (or all tasks) finish. They are the synchronous bridge between a Task and the calling thread.
// Wait — block until task is done, no return value
var task = Task.Run(() => { sum += i; });
task.Wait();
Console.WriteLine(sum);
// .Result — block and get the return value
var task = Task.Run(() => ComputeSum());
Console.WriteLine(task.Result);
// Fetch synchronously — only for console apps or top-level scripts
var task = client.GetStringAsync("https://pokeapi.co/api/v2/pokemon");
Console.WriteLine(task.Result);- Never call
.Wait()or.Resultinside anasyncmethod or on a UI thread — it causes a deadlock. - These are appropriate only in console applications, top-level program entry points, or test infrastructure.
- Console apps where
async Mainis not available or desirable. - Integration tests that must be synchronous.
- Converting an async library to a sync API as a last resort.
Project: TaskContinuation-WhenAll
Task.WhenAll returns a new Task that completes only when all supplied tasks have completed. It does not block the calling thread — it is the non-blocking alternative to calling .Wait() on each task in a loop.
Task<int>[] tasks = new Task<int>[4];
tasks[0] = Task.Run(() => SumSegment(0, 2));
tasks[1] = Task.Run(() => SumSegment(2, 5));
tasks[2] = Task.Run(() => SumSegment(5, 7));
tasks[3] = Task.Run(() => SumSegment(7, 10));
Task.WhenAll(tasks).ContinueWith(t =>
{
// t.Result is int[] — the results of all four tasks
Console.WriteLine($"Total: {t.Result.Sum()}");
});Task.WhenAll |
Task.WaitAll |
|
|---|---|---|
| Blocks calling thread | No | Yes |
Returns a Task |
Yes | No (void) |
async-friendly |
Yes (await Task.WhenAll(...)) |
No |
- Fanning out multiple parallel operations and reacting when all complete.
- Downloading multiple files, querying multiple APIs, or processing multiple data segments.
- Always prefer this over
WaitAllin async contexts.
Project: TaskCancelation
CancellationTokenSource creates a token you pass into a task. The task periodically checks the token and stops cleanly when cancellation is requested.
using var cts = new CancellationTokenSource();
var token = cts.Token;
var task = Task.Run(Work, token);
var input = Console.ReadLine();
if (input == "c") cts.Cancel();
task.Wait();
Console.WriteLine($"Task status: {task.Status}");
void Work()
{
for (int i = 0; i < 100000; i++)
{
token.ThrowIfCancellationRequested(); // throws OperationCanceledException
Thread.SpinWait(30000000);
}
}using CancellationTokenSource source = new CancellationTokenSource();
source.CancelAfter(10); // cancel after 10 ms
var json = client.GetStringAsync("https://pokeapi.co/api/v2/pokemon", source.Token);
Console.WriteLine(json.Result);| Cancellation method | Task.Status |
|---|---|
break (exits loop, no throw) |
RanToCompletion |
throw new OperationCanceledException() |
Canceled |
token.ThrowIfCancellationRequested() |
Canceled |
- Any long-running task that the user should be able to cancel.
- HTTP requests with timeouts.
- Background jobs with a graceful shutdown requirement.
Project: ExceptionHandlingInTasks
Exceptions thrown inside a Task are silently swallowed unless you observe them. They are stored in the task's Exception property as an AggregateException.
Hidden exceptions (bad pattern)
var task = client.GetStringAsync("https://invalid-url.co");
task.ContinueWith(t => Console.WriteLine(t.Result));
// If the request fails, the exception is swallowed silentlyThread.Sleep(1000);
Console.WriteLine(task1.Status); // "Faulted"
Console.WriteLine(task2.Status); // "Faulted" (continuation also faulted)var tasks = new[]
{
Task.Run(() => throw new InvalidOperationException("invalid")),
Task.Run(() => throw new ArgumentNullException("null")),
Task.Run(() => throw new Exception("general"))
};
Task.WhenAll(tasks).ContinueWith(t =>
{
if (t.IsFaulted)
foreach (var ex in t.Exception!.InnerExceptions)
Console.WriteLine(ex.Message);
});Accessing .Result or calling .Wait() on a faulted task re-throws the AggregateException, which you can then catch normally:
try
{
Task.WhenAll(tasks).Wait();
}
catch (AggregateException ex)
{
foreach (var inner in ex.InnerExceptions)
Console.WriteLine(inner.Message);
}- Use
.ContinueWith+t.IsFaultedfor fire-and-forget tasks. - Use
await+try/catchin async methods (unwrapsAggregateExceptionautomatically). - Use
.Wait()/.Resultin synchronous contexts where you need to rethrow.
Project: TaskSynchronization
The same SemaphoreSlim throttling pattern from threads-synchronization.md applied to Task-based code. Replaces Thread objects with Task objects, making the code cleaner and pool-aware.
using SemaphoreSlim semaphore = new SemaphoreSlim(3, 3);
Task monitoringTask = new Task(MonitorQueue);
monitoringTask.Start();
void MonitorQueue()
{
while (true)
{
if (requestQueue.Count > 0)
{
var input = requestQueue.Dequeue();
semaphore.Wait();
Task processingTask = new Task(() => ProcessInput(input));
processingTask.Start();
}
}
}
void ProcessInput(string? input)
{
try { Thread.Sleep(2000); Console.WriteLine($"Processed: {input}"); }
finally { semaphore.Release(); }
}- Limiting how many tasks can run concurrently inside a
Task-based pipeline. - In
asynccode, prefersemaphore.WaitAsync()to avoid blocking the thread.
| API | Purpose |
|---|---|
Task.Run(...) |
Offload CPU work to ThreadPool |
Task<T> |
Return a value from a background operation |
.ContinueWith(...) |
Schedule work after a task completes |
.Unwrap() |
Flatten Task<Task<T>> into Task<T> |
.Wait() / .Result |
Block until done (sync contexts only) |
Task.WhenAll(...) |
Wait for all tasks, non-blocking |
Task.WhenAny(...) |
React to the first task that completes |
CancellationToken |
Cooperative, structured cancellation |
AggregateException |
Container for one or more task exceptions |