Free Handbook · Every example compiled & verified

Async & Await

Task, async and await from the ground up: what really runs when, WhenAll and WhenAny, exceptions, cancellation, async streams and the pitfalls that hang apps.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 10 · what you'll be able to do

  • Explain the difference between I/O-bound and CPU-bound work and pick async/await, Task.Run or Parallel for each
  • Predict the exact order an async method and its caller run in, and where an await suspends
  • Run several operations at once with Task.WhenAll and race them against a timeout with WhenAny or WaitAsync
  • Handle exceptions and cancellation in async code with try/catch, CancellationToken and OperationCanceledException
  • Stream results with IAsyncEnumerable and avoid async void, .Result deadlocks and other classic mistakes
01

Why async, and what a Task is

Most of the time a backend program is not calculating anything — it is waiting: for a database row, an HTTP response, a file to be read. That is I/O-bound work. If a thread sits blocked for those 200 ms, it cannot serve anyone else, and a web server that runs out of threads stops answering. async/await lets the method give the thread back while it waits and pick up where it left off when the result arrives. CPU-bound work (resizing images, hashing, number crunching) is different: the CPU is genuinely busy, so the answer there is more cores — Task.Run or Parallel — not async.

A Task is an object that represents work that will finish later. Task finishes with no value; Task<T> finishes with a T. An async method returns one immediately, and await unwraps it: await on a Task<int> gives you an int. Top-level statements are allowed to await, which is why the examples below need no Main.

C#Program.cs
Task<int> sumTask = Task.Run(() => SumTo(1_000));   // CPU work on a pool thread
int total = await sumTask;
Console.WriteLine($"sum = {total}, done = {sumTask.IsCompletedSuccessfully}");

string name = await GetUserNameAsync(7);            // I/O-style work
Console.WriteLine(name);

Task<int> cached = Task.FromResult(42);             // an already-finished Task
Console.WriteLine($"cached done = {cached.IsCompleted}, value = {await cached}");

static int SumTo(int n)
{
    int s = 0;
    for (int i = 1; i <= n; i++) s += i;
    return s;
}

static async Task<string> GetUserNameAsync(int id)
{
    await Task.Delay(20);        // stands in for a database or HTTP call
    return $"user-{id}";
}
Outputcompiled & run with real C#
sum = 500500, done = True
user-7
cached done = True, value = 42
Your turn

Add static async Task LogAsync(string msg) that waits 10 ms and prints the message, and await it between the two calls.

C#Program.cs
using System.Diagnostics;

var sw = Stopwatch.StartNew();
await CallApiAsync("orders");      // one after another: 3 x 200 ms
await CallApiAsync("stock");
await CallApiAsync("prices");
long sequential = sw.ElapsedMilliseconds;

sw.Restart();
Task a = CallApiAsync("orders");   // all three start now...
Task b = CallApiAsync("stock");
Task c = CallApiAsync("prices");
await Task.WhenAll(a, b, c);       // ...and we wait for the slowest
long concurrent = sw.ElapsedMilliseconds;

Console.WriteLine($"sequential took at least 600 ms: {sequential >= 600}");
Console.WriteLine($"concurrent was faster: {concurrent < sequential}");

static Task CallApiAsync(string name) => Task.Delay(200);
Outputcompiled & run with real C#
sequential took at least 600 ms: True
concurrent was faster: True

Three waits that do not depend on each other can overlap. No extra threads were needed: nothing is running during a Task.Delay, just a timer.

I/O-bound: use async/await

  • Database queries, HTTP calls, file and network reads
  • The thread is released while waiting
  • Scales a web server to many requests with few threads
  • Methods end in Async and return Task

CPU-bound: use Task.Run / Parallel

  • Image processing, compression, heavy maths
  • A core is busy the whole time
  • Speeds up by using more cores, up to the number you have
  • In ASP.NET Core, rarely wrap CPU work in Task.Run — the request already runs on a pool thread
Error you will hit

CS0029: forgetting the await

C#
int count = GetCountAsync();
Console.WriteLine(count);

static async Task<int> GetCountAsync()
{
    await Task.Delay(100);
    return 3;
}
Program.cs(1,13): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task<int>' to 'int'
Why the compiler said that

Calling an async method gives you the promise of an int — a Task<int> — not the int. The value only exists once the task completes, and await is how you get it.

The fix

Put await in front of the call. The calling method must itself be async (top-level statements already are).

C#
int count = await GetCountAsync();
Console.WriteLine(count);

static async Task<int> GetCountAsync()
{
    await Task.Delay(100);
    return 3;
}
02

How await really flows

An async method runs synchronously, on the caller's thread, until it reaches an await on a task that has not finished yet. At that point it registers "the rest of me" as a continuation and returns an unfinished Task to the caller, who carries on. When the awaited task completes, the continuation runs. If the awaited task is already complete, await does not pause at all.

The example below uses a TaskCompletionSource — a Task you complete by hand — so we decide exactly when the awaited work "finishes". That makes the order of lines completely predictable.

C#Program.cs
var gate = new TaskCompletionSource();

Console.WriteLine("1 Main: calling DoWorkAsync");
Task work = DoWorkAsync(gate.Task);
Console.WriteLine($"3 Main: got a Task back, IsCompleted={work.IsCompleted}");

gate.SetResult();            // the thing DoWorkAsync waits for completes
await work;
Console.WriteLine($"5 Main: finished, IsCompleted={work.IsCompleted}");

static async Task DoWorkAsync(Task signal)
{
    Console.WriteLine("2 DoWork: started, now awaiting");
    await signal;            // not complete yet: suspend and return to Main
    Console.WriteLine("4 DoWork: resumed after await");
}
Outputcompiled & run with real C#
1 Main: calling DoWorkAsync
2 DoWork: started, now awaiting
3 Main: got a Task back, IsCompleted=False
4 DoWork: resumed after await
5 Main: finished, IsCompleted=True
Your turn

Move gate.SetResult(); above the call to DoWorkAsync. Predict the new order (hint: the await finds an already-completed task) and run it.

VisualizeStepping through the suspend and resumeStep 1 / 10
var gate = new TaskCompletionSource();
Console.WriteLine("1 Main: calling DoWorkAsync");
Task work = DoWorkAsync(gate.Task);
Console.WriteLine($"3 Main: got a Task back, IsCompleted={work.IsCompleted}");
gate.SetResult(); // the thing DoWorkAsync waits for completes
await work;
Console.WriteLine($"5 Main: finished, IsCompleted={work.IsCompleted}");
static async Task DoWorkAsync(Task signal)
{
Console.WriteLine("2 DoWork: started, now awaiting");
await signal; // not complete yet: suspend and return to Main
Console.WriteLine("4 DoWork: resumed after await");
}
Line 1

A hand-controlled task is created. It is not complete.

Variables now
gate.Taskpending
All 10 steps as a table
StepLineWhat happenedVariables now
11A hand-controlled task is created. It is not complete.gate.Task = pending
23Ordinary synchronous code.
34Calling an async method runs its body right away, on this same thread — no new thread is started.
413Still synchronous: DoWorkAsync prints before Main gets control back.
514signal is not complete, so the compiler-generated state machine saves its place, registers the rest of the method as a continuation, and returns an unfinished Task.work = pending
65Main continues with the Task in hand. The work is not done yet.
77Completing the gate triggers the saved continuation.gate.Task = completed
815DoWorkAsync resumes after its await and runs to the end, which completes work.work = completed
98work is already complete, so this await does not pause.
109Done.
await does not start anything
The work starts when you call the async method. await only waits for a task that is already running. That is why Task a = FooAsync(); Task b = BarAsync(); await a; await b; runs both at once, while await FooAsync(); await BarAsync(); runs them one after the other.
Error you will hit

CS4032: await in a method that is not async

C#
Console.WriteLine(GetCount());

static int GetCount()
{
    await Task.Delay(100);
    return 3;
}
Program.cs(5,5): error CS4032: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task<int>'.
Why the compiler said that

await needs the compiler to rewrite the method into a state machine that can pause and resume, and a paused method cannot hand back a plain int — the value does not exist yet. The compiler even tells you the return type it needs.

The fix

Mark the method async, return Task<int>, name it with the Async suffix by convention, and await it at the call site.

C#
Console.WriteLine(await GetCountAsync());

static async Task<int> GetCountAsync()
{
    await Task.Delay(100);
    return 3;
}
03

Task.WhenAll, WhenAny and WhenEach

Task.WhenAll gives you one task that completes when every task in the list has completed. For Task<T> it returns a T[] whose order matches the order of the tasks you passed in — not the order they finished — so results are always predictable. The usual pattern is: start all the tasks (often with LINQ Select), then await them together.

C#Program.cs
int[] ids = { 3, 1, 2 };

Task<string>[] tasks = ids.Select(id => FetchAsync(id)).ToArray();   // all start now
string[] results = await Task.WhenAll(tasks);

Console.WriteLine(string.Join(", ", results));

static async Task<string> FetchAsync(int id)
{
    await Task.Delay(id * 30);     // id 3 finishes last
    return $"item{id}";
}
Outputcompiled & run with real C#
item3, item1, item2

item3 finished last but comes first: WhenAll keeps the input order.

Your turn

Remove the .ToArray() and pass the lazy IEnumerable straight to WhenAll. Does the output change? (WhenAll enumerates it once, so no — but see Module 09 on deferred execution for why enumerating it twice would start the downloads twice.)

Task.WhenAny completes as soon as the first task completes and hands you that task. Its classic use is a timeout race. Since .NET 6 there is a shorter way: task.WaitAsync(timeout) throws TimeoutException if the task is not done in time. And .NET 9 added Task.WhenEach, an async stream that yields tasks in the order they finish.

C#Program.cs
Task<string> slow = SlowAsync();
Task timeout = Task.Delay(100);
Task first = await Task.WhenAny(slow, timeout);
Console.WriteLine(first == timeout ? "timed out, using cached value" : await slow);

try
{
    await SlowAsync().WaitAsync(TimeSpan.FromMilliseconds(100));
}
catch (TimeoutException)
{
    Console.WriteLine("WaitAsync threw TimeoutException");
}

var jobs = new[] { Job("c", 300), Job("a", 100), Job("b", 200) };
await foreach (Task<string> done in Task.WhenEach(jobs))
    Console.WriteLine($"finished: {await done}");

static async Task<string> SlowAsync()
{
    await Task.Delay(5000);
    return "fresh value";
}

static async Task<string> Job(string name, int ms)
{
    await Task.Delay(ms);
    return name;
}
Outputcompiled & run with real C#
timed out, using cached value
WaitAsync threw TimeoutException
finished: a
finished: b
finished: c

A timeout stops you waiting; it does not stop the slow task. To actually abandon the work, pass it a CancellationToken (later in this module).

MethodCompletes whenGives you
await Task.WhenAll(ts)every task is doneT[] in input order; throws if any failed
await Task.WhenAny(ts)the first task is donethe finished Task (await it again for its value)
await foreach (… in Task.WhenEach(ts))each task, one at a timetasks in completion order (.NET 9+)
await t.WaitAsync(timeout)t is done or time runs outthe result, or TimeoutException
04

Exceptions in async code

An exception thrown inside an async method does not fly out of the call — it is stored in the returned Task, which becomes Faulted. It is re-thrown when someone waits for the task. How it comes out depends on how you wait: await re-throws the original exception with its original type, while the blocking .Wait() and .Result wrap it in an AggregateException. Exception basics (try, catch, finally) are covered in Module 07.

C#Program.cs
try { await FailAsync("A"); }
catch (InvalidOperationException ex)
{
    Console.WriteLine($"await gave: {ex.GetType().Name}: {ex.Message}");
}

try { FailAsync("B").Wait(); }
catch (AggregateException ex)
{
    Console.WriteLine($"Wait gave: {ex.GetType().Name} wrapping \"{ex.InnerException!.Message}\"");
}

Task all = Task.WhenAll(FailAsync("C"), FailAsync("D", 60));   // D fails later
try { await all; }
catch (Exception ex)
{
    Console.WriteLine($"await WhenAll threw only: {ex.Message}");
}
var inner = all.Exception!.InnerExceptions.Select(e => e.Message);
Console.WriteLine($"all.Exception holds: {string.Join(", ", inner)}");

static async Task FailAsync(string name, int ms = 10)
{
    await Task.Delay(ms);
    throw new InvalidOperationException($"{name} failed");
}
Outputcompiled & run with real C#
await gave: InvalidOperationException: A failed
Wait gave: AggregateException wrapping "B failed"
await WhenAll threw only: C failed
all.Exception holds: C failed, D failed
Your turn

Make only "D" fail (have FailAsync return normally for C) and predict what await all throws.

await WhenAll shows you one failure
When several tasks fail, await Task.WhenAll(…) re-throws only the first exception. The others are not lost — they are in the WhenAll task's Exception.InnerExceptions. Keep a reference to that task (as above) when you need to log every failure.
Error you will hit

AggregateException: blocking on .Result

C#
Task<int> task = LoadAsync();
Console.WriteLine(task.Result);

static async Task<int> LoadAsync()
{
    await Task.Delay(10);
    throw new InvalidOperationException("database is down");
}
Unhandled exception. System.AggregateException: One or more errors occurred. (database is down)
 ---> System.InvalidOperationException: database is down
   at Program.<<Main>$>g__LoadAsync|0_0() in Program.cs:line 7
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Program.<Main>$(String[] args) in Program.cs:line 2
Why the compiler said that

.Result blocks the thread until the task finishes and, because a task can hold several exceptions, wraps whatever it holds in an AggregateException. The real error is the inner one after --->. A catch (InvalidOperationException) around line 2 would not catch it.

The fix

Use await. It unwraps the exception, keeps the thread free, and cannot deadlock (see the pitfalls lesson).

C#
try
{
    Console.WriteLine(await LoadAsync());
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"could not load: {ex.Message}");
}

static async Task<int> LoadAsync()
{
    await Task.Delay(10);
    throw new InvalidOperationException("database is down");
}
05

CancellationToken and timeouts

Cancellation in .NET is cooperative. A CancellationTokenSource owns the "cancel" button; it hands out CancellationTokens, which methods check. Nothing is killed from outside: the method either passes the token to something that honours it (Task.Delay, HttpClient, EF Core queries) or calls token.ThrowIfCancellationRequested() itself. Either way the result is an OperationCanceledException, which you catch to stop cleanly.

C#Program.cs
using var cts = new CancellationTokenSource();
string[] files = { "a.csv", "b.csv", "c.csv", "d.csv" };

try
{
    // simulate the user pressing Cancel after two files
    await ProcessAsync(files, cts.Token, done => { if (done == 2) cts.Cancel(); });
}
catch (OperationCanceledException)
{
    Console.WriteLine("stopped cleanly");
}

static async Task ProcessAsync(string[] files, CancellationToken token, Action<int> onProgress)
{
    for (int i = 0; i < files.Length; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(10, token);
        Console.WriteLine($"processed {files[i]}");
        onProgress(i + 1);
    }
}
Outputcompiled & run with real C#
processed a.csv
processed b.csv
stopped cleanly
Your turn

Change the loop to if (token.IsCancellationRequested) { Console.WriteLine("partial result saved"); return; } instead of throwing. When is returning early better than throwing?

C#Program.cs
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));   // auto-cancel

try
{
    await Task.Delay(5000, cts.Token);
    Console.WriteLine("finished");
}
catch (OperationCanceledException ex)
{
    Console.WriteLine($"caught {ex.GetType().Name}");
    Console.WriteLine($"is an OperationCanceledException: {ex is OperationCanceledException}");
    Console.WriteLine($"token cancelled: {cts.IsCancellationRequested}");
}
Outputcompiled & run with real C#
caught TaskCanceledException
is an OperationCanceledException: True
token cancelled: True

Task-based APIs throw TaskCanceledException, a subclass of OperationCanceledException. Catch the base class and you handle both.

C#ReportsController.cs
// ASP.NET Core passes a token that is cancelled when the client disconnects.
app.MapGet("/report", async (AppDb db, CancellationToken ct) =>
{
    // a linked source: cancelled by EITHER the client OR our own 10-second limit
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    cts.CancelAfter(TimeSpan.FromSeconds(10));

    var rows = await db.Orders.Where(o => o.Total > 100).ToListAsync(cts.Token);
    return Results.Ok(rows.Count);
});

Pass the token all the way down. A token that stops at the top layer cancels nothing.

Error you will hit

TaskCanceledException: a timeout nobody catches

C#
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
Console.WriteLine("downloading...");
await Task.Delay(5000, cts.Token);
Console.WriteLine("done");
downloading...
Unhandled exception. System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at Program.<Main>$(String[] args) in Program.cs:line 3
   at Program.<Main>(String[] args)
Why the compiler said that

The token was cancelled after 50 ms, so Task.Delay (standing in for an HTTP call) ended by throwing. Cancellation is reported as an exception on purpose — code after the await must not run as if it had a result.

The fix

Catch OperationCanceledException where cancelling is an expected outcome (a timeout, a user pressing Cancel) and decide what to do instead.

C#
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
Console.WriteLine("downloading...");
try
{
    await Task.Delay(5000, cts.Token);
    Console.WriteLine("done");
}
catch (OperationCanceledException)
{
    Console.WriteLine("timed out, try again later");
}
06

Async streams: IAsyncEnumerable and await foreach

A Task<List<T>> makes you wait for all the items. An async stream, IAsyncEnumerable<T>, hands items over one by one as they arrive — page by page from an API, row by row from a database. You write one with async plus yield return (the async twin of the iterators in Module 08) and consume it with await foreach. Since .NET 10 the LINQ operators (Where, Select, ToListAsync, CountAsync…) work on async streams out of the box.

C#Program.cs
await foreach (Page page in FetchPagesAsync(3))
    Console.WriteLine($"page {page.Number}: {string.Join(",", page.Items)}");

int bigPages = await FetchPagesAsync(3).Where(p => p.Items.Length > 2).CountAsync();
Console.WriteLine($"pages with more than 2 items: {bigPages}");

List<int> firstItems = await FetchPagesAsync(3).Select(p => p.Items[0]).ToListAsync();
Console.WriteLine(string.Join(" ", firstItems));

static async IAsyncEnumerable<Page> FetchPagesAsync(int count)
{
    for (int n = 1; n <= count; n++)
    {
        await Task.Delay(10);                                  // one "HTTP call" per page
        yield return new Page(n, Enumerable.Range(n * 10, n + 1).ToArray());
    }
}

record Page(int Number, int[] Items);
Outputcompiled & run with real C#
page 1: 10,11
page 2: 20,21,22
page 3: 30,31,32,33
pages with more than 2 items: 2
10 20 30
Your turn

Add a break inside the await foreach after page 2. The generator stops too — page 3 is never fetched. Add a Console.WriteLine in the generator to prove it.

Cancelling an async stream
Give the generator a parameter marked [EnumeratorCancellation] CancellationToken token = default, then consumers can write await foreach (var x in source.WithCancellation(ct)). The token flows into the generator even when the stream was created somewhere else.
07

Pitfalls: async void, .Result deadlocks and ConfigureAwait

Most async bugs come from a handful of mistakes. async void is the worst: a void method returns no Task, so nobody can await it, nobody learns when it finishes, and an exception inside it cannot be caught by the caller — it is thrown on the thread pool and takes the whole process down. Use async Task everywhere; the only legitimate async void is a UI event handler (button.Click), whose signature is fixed.

Error you will hit

Unhandled exception from an async void method

C#
try
{
    SaveAsync();
}
catch (IOException)
{
    Console.WriteLine("never reached");
}
await Task.Delay(200);
Console.WriteLine("never printed");

static async void SaveAsync()
{
    await Task.Delay(10);
    throw new IOException("disk full");
}
Unhandled exception. System.IO.IOException: disk full
   at Program.<<Main>$>g__SaveAsync|0_0() in Program.cs:line 15
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__124_1(Object state)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
   at System.Threading.Thread.StartCallback()
Why the compiler said that

The try/catch finished long before the exception happened: SaveAsync() returned at its first await. With no Task to store the exception in, the runtime throws it on a thread-pool thread, where it is unhandled and ends the process. Note there is no Main in that stack trace at all.

The fix

Return Task and await it. The exception then travels through the Task back to the caller's catch.

C#
try
{
    await SaveAsync();
}
catch (IOException ex)
{
    Console.WriteLine($"save failed: {ex.Message}");
}

static async Task SaveAsync()
{
    await Task.Delay(10);
    throw new IOException("disk full");
}

The .Result deadlock

Some environments have a synchronization context that says "continuations must run on this one thread": the UI thread in WPF/WinForms/MAUI, and the request context in classic ASP.NET (.NET Framework). Now picture this code on the UI thread:

C#MainWindow.xaml.cs
private void LoadButton_Click(object sender, RoutedEventArgs e)
{
    // blocks the UI thread until the download finishes...
    string html = DownloadAsync().Result;
    Output.Text = html;
}

private async Task<string> DownloadAsync()
{
    string html = await http.GetStringAsync("https://example.com");
    // ...but this line must run on the UI thread, which is blocked above.
    return html;
}

Each side waits for the other forever. The window freezes and no exception is ever thrown.

  1. 1
    1. The click handler calls DownloadAsync()

    It starts the HTTP request, hits await, and returns an unfinished Task.

  2. 2
    2. .Result blocks the UI thread

    The UI thread now sits waiting for that Task to complete.

  3. 3
    3. The response arrives

    The continuation (return html;) is posted to the synchronization context — the UI thread.

  4. 4
    4. Deadlock

    The UI thread is blocked in step 2, so it never runs the continuation, so the Task never completes, so step 2 never ends.

The fix is async all the way: make the handler async void (an event handler, the one allowed case) and await instead of .Result. Library code can also add .ConfigureAwait(false) to its awaits, which means "I do not need to come back to the original context", so its continuations run on the thread pool and cannot be caught in this trap. Console apps and ASP.NET Core have no synchronization context, which is why the .Result error card above threw instead of hanging — but the same code will hang the day it is called from a UI.

RuleWhy
Never block on async code with .Result, .Wait() or .GetAwaiter().GetResult()Deadlocks under a synchronization context, and wastes a thread everywhere else
No async void except event handlersExceptions crash the process; callers cannot await it
Library code: await x.ConfigureAwait(false)The library does not know who calls it; do not resume on their UI thread
App code in ASP.NET Core: plain awaitThere is no context to escape, so ConfigureAwait adds only noise
Pass CancellationToken through every layerA token that is not passed on cancels nothing
Suffix async methods with AsyncCallers can see at a glance that they must await
Error you will hit

CS4033: await inside a void method

C#
Greet();

static void Greet()
{
    await Task.Delay(100);
    Console.WriteLine("hi");
}
Program.cs(5,5): error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
Why the compiler said that

Same rule as CS4032, for a method that returns nothing. Notice the compiler suggests Task, not async void — it steers you away from the pitfall above.

The fix

Make it static async Task GreetAsync() and await GreetAsync(); at the call site.

C#
await GreetAsync();

static async Task GreetAsync()
{
    await Task.Delay(100);
    Console.WriteLine("hi");
}
async without await
An async method that never awaits still pays for the state machine and runs fully synchronously. Older C# compilers warned about this with CS1998; the .NET 10 SDK compiler used by this handbook does not, so watch for it in code review. If a method has nothing to await, drop async and return Task.FromResult(value) or Task.CompletedTask. For very hot paths that usually complete synchronously, ValueTask<T> avoids allocating a Task — but it may be awaited only once, so keep Task as your default.
08

CPU-bound work: Parallel.For and PLINQ

When the work really is computation, spread it over cores. Parallel.For and Parallel.ForEach split a loop across thread-pool threads and block until all iterations finish; Parallel.ForEachAsync does the same for async bodies with a limit on how many run at once. PLINQ adds .AsParallel() to a LINQ query. The order in which iterations run is not defined, so write each result to its own slot, use AsOrdered(), or combine with a thread-safe operation such as Interlocked.Increment.

C#Program.cs
long[] squares = new long[10];
Parallel.For(0, squares.Length, i => squares[i] = (long)i * i);   // each i owns its slot
Console.WriteLine(string.Join(" ", squares));

long total = Enumerable.Range(1, 1_000_000).AsParallel().Sum(n => (long)n);
Console.WriteLine(total);

var picked = Enumerable.Range(1, 20).AsParallel().AsOrdered()
    .Where(n => n % 4 == 0)
    .Select(n => n * 10);
Console.WriteLine(string.Join(",", picked));

int counter = 0;
Parallel.For(0, 1000, _ => Interlocked.Increment(ref counter));   // counter++ would lose updates
Console.WriteLine(counter);
Outputcompiled & run with real C#
0 1 4 9 16 25 36 49 64 81
500000500000
40,80,120,160,200
1000
Your turn

Replace Interlocked.Increment(ref counter) with counter++ and run it a few times. Why is the total sometimes below 1000?

SituationReach for
Waiting on I/O (database, HTTP, files)async/await, Task.WhenAll
Many I/O calls, but at most N at a timeParallel.ForEachAsync with MaxDegreeOfParallelism
One heavy computation off the UI threadawait Task.Run(() => …)
A big loop of independent computationsParallel.For / Parallel.ForEach
A LINQ query over a large in-memory collectionPLINQ .AsParallel() — measure first, it is not free
Task / Task<T>
An object representing work that completes later, with no value or with a value of type T.
async
A modifier that lets a method use await; the compiler rewrites it into a state machine.
await
Waits for a task without blocking the thread; the rest of the method becomes a continuation.
Continuation
The code after an await, run when the awaited task completes.
I/O-bound
Work that mostly waits on disks, networks or databases — the case async/await is built for.
CPU-bound
Work that keeps a core busy; speed it up with Task.Run, Parallel or PLINQ.
CancellationToken
A cooperative cancel signal passed down through async calls; honoured by throwing OperationCanceledException.
AggregateException
The wrapper .Wait() and .Result throw around a faulted task's exceptions; await unwraps it.
IAsyncEnumerable<T>
An async stream, produced with async + yield return and consumed with await foreach.
Synchronization context
A rule about which thread continuations run on (e.g. the UI thread); the root cause of .Result deadlocks.
Quick check

An async method prints "A", then awaits a task that is not yet complete, then prints "B". The caller prints "C" right after calling it (without awaiting yet). What is printed first, and what is printed second?

Quick check

Which way of waiting for a faulted Task<int> throws the original InvalidOperationException rather than an AggregateException?

Frequently asked questions

Does async/await create a new thread in C#?
No. An async method runs on the caller's thread until it awaits something unfinished, then releases that thread. While waiting on I/O no thread is used at all; the continuation later runs on a thread-pool thread (or on the UI thread if there is a synchronization context). Only Task.Run and Parallel explicitly use extra threads.
What is the difference between Task.WhenAll and awaiting tasks one by one?
If the tasks were all started first, both wait for the same work, but WhenAll waits for all of them even if one fails early and exposes every exception. Awaiting a method call directly, one after another, starts each operation only after the previous one finishes, so the total time is the sum instead of the longest one.
Why should I avoid .Result and .Wait() on a Task?
They block a thread while waiting, wrap exceptions in AggregateException, and deadlock in environments with a synchronization context such as WPF, WinForms or classic ASP.NET. Make the calling method async and use await instead.

Finish the C# handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.