Site

Async — C# async/await and Task

Tutorial S8  •  C# / Learn

S8.0 What This Teaches

This tutorial covers asynchronous programming in C#:

S8.1 Task and async/await

An async method returns a Task or Task<T>. await suspends the method until the task completes, releasing the thread to do other work:
static async Task<string> FetchGreeting(string name)
{
    await Task.Delay(100);   // simulate async I/O (non-blocking)
    return $"Hello, {name}!";
}

// Top-level await (C# 9+)
string msg = await FetchGreeting("Alice");
Console.WriteLine(msg);

// Void-returning async entry point
async Task Main()
{
    string result = await FetchGreeting("Bob");
    Console.WriteLine(result);
}
The method pauses at each await without blocking a thread. When the awaited task completes, execution resumes from that point.

S8.2 Running Tasks Concurrently

static async Task<int> ComputeAsync(int n)
{
    await Task.Delay(n * 100);  // simulate variable-length work
    return n * n;
}

// Sequential - each awaits before next starts (slow)
int a = await ComputeAsync(1);
int b = await ComputeAsync(2);
int c = await ComputeAsync(3);

// Concurrent - all start immediately, wait for all to finish
int[] results = await Task.WhenAll(
    ComputeAsync(1),
    ComputeAsync(2),
    ComputeAsync(3)
);
Console.WriteLine(results.Sum());   // 1 + 4 + 9 = 14

// WhenAny - continue as soon as the first finishes
Task<int> winner = await Task.WhenAny(ComputeAsync(3), ComputeAsync(1), ComputeAsync(2));
Console.WriteLine(await winner);    // 1 (finished first)

S8.3 CancellationToken

using var cts = new CancellationTokenSource(timeout: TimeSpan.FromSeconds(2));

try
{
    await DoLongWorkAsync(cts.Token);
    Console.WriteLine("Done");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled after timeout");
}

static async Task DoLongWorkAsync(CancellationToken ct)
{
    for (int i = 0; i < 10; i++)
    {
        ct.ThrowIfCancellationRequested();   // cooperative cancellation
        await Task.Delay(500, ct);
        Console.Write($"{i} ");
    }
}

S8.4 CPU-Bound Work with Task.Run

async/await shines for I/O-bound operations. For CPU-bound work (computation), use Task.Run to move it off the main thread:
static int ExpensiveCompute(int n)
{
    // Simulate heavy CPU work
    return Enumerable.Range(1, n).Sum();
}

// Run on thread pool, keep UI/main thread free
int result = await Task.Run(() => ExpensiveCompute(10_000_000));
Console.WriteLine(result);
Don't use Task.Run inside library code - let callers decide whether to offload. It is mainly for top-level application code keeping a UI responsive.

S8.5 Example - All Together

// Async - parallel HTTP-like fetches with timeout and cancellation.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var ct = cts.Token;

var tasks = new[] { "Alice", "Bob", "Carol" }
    .Select(name => FetchAsync(name, ct))
    .ToList();

try
{
    string[] results = await Task.WhenAll(tasks);
    foreach (string r in results)
        Console.WriteLine(r);
}
catch (OperationCanceledException)
{
    Console.WriteLine("One or more fetches cancelled");
}

static async Task<string> FetchAsync(string name, CancellationToken ct)
{
    int delay = name.Length * 200;   // simulate different latencies
    await Task.Delay(delay, ct);
    return $"Data for {name} (took {delay}ms)";
}

S8.6 Exercise

Exercise
  • Write an async method DownloadAsync(string url) that simulates downloading data with a random delay. Run three downloads concurrently with Task.WhenAll and print the total time.
  • Add a CancellationToken with a 1-second timeout. Handle OperationCanceledException and print which downloads completed.
  • Use Task.Run to compute the sum of squares of 1-1,000,000 without blocking, then await the result.

S8.7 Common Mistakes

async void - fire-and-forget

async void DoWork() { ... }   // exceptions are unobserved - crash the process!
async Task DoWork() { ... }   // correct - exceptions propagate via Task
Only use async void for event handlers where the signature is forced. All other async methods should return Task or Task<T>.

.Result or .Wait() on a task (deadlock risk)

string data = GetDataAsync().Result;    // blocks; can deadlock in ASP.NET
string data = await GetDataAsync();     // non-blocking - correct

Awaiting in a loop instead of concurrently

// Sequential - slow; each waits for the previous to finish
foreach (var url in urls)
    await FetchAsync(url);

// Concurrent - fast; all run at the same time
await Task.WhenAll(urls.Select(FetchAsync));

S8.8 Key Terms

TermMeaning
TaskRepresents an ongoing or completed operation; a promise
Task<T>Task that eventually produces a value of type T
asyncMarks a method as asynchronous; enables use of await inside
awaitSuspends the method until the awaited task completes; releases the thread
Task.WhenAllReturns a task that completes when all supplied tasks complete
Task.WhenAnyReturns a task that completes when the first supplied task completes
CancellationTokenSignals cooperative cancellation to async methods
Task.RunSchedules CPU-bound work on the thread pool