ZetCode

C# async/await

last modified October 5, 2026

This article explains how to use the async and await keywords for asynchronous programming in C#.

Asynchronous programming allows a program to start an operation and continue with other work while it is waiting for the operation to complete. It is especially useful for I/O-bound work, such as network requests, database queries, and file access. CPU-bound work can also be moved to a thread-pool thread when it is useful to keep the calling thread responsive.

The async modifier enables the use of await in a method. An async method runs synchronously until it reaches an incomplete await. At that point, it returns control to its caller while the awaited operation is in progress. The await operator then produces the operation's result when it completes. An async method commonly returns Task, Task<T>, or (in specialized scenarios) ValueTask.

A Task represents an asynchronous operation. Asynchronous does not necessarily mean that a new thread is created: I/O operations can complete without occupying a thread while they wait. Avoid blocking on tasks with Wait or Result; prefer awaiting them.

C# simple synchronous example

The following program calls three methods synchronously. Each method blocks the current thread for a different amount of time.

Program.cs
using System.Diagnostics;

var sw = Stopwatch.StartNew();

F1();
F2();
F3();

sw.Stop();
Console.WriteLine($"elapsed: {sw.ElapsedMilliseconds} ms");

static void F1()
{
    Console.WriteLine("f1 called");
    Thread.Sleep(4000);
}

static void F2()
{
    Console.WriteLine("f2 called");
    Thread.Sleep(7000);
}

static void F3()
{
    Console.WriteLine("f3 called");
    Thread.Sleep(2000);
}

Thread.Sleep simulates slow work, and Stopwatch measures the total elapsed time. The calls execute one after another, so the total is approximately 13 seconds.

$ dotnet run
f1 called
f2 called
f3 called
elapsed: 13000 ms

C# simple asynchronous example

The next version starts three asynchronous delays and awaits them together with Task.WhenAll. The delays overlap, so the total time is approximately the longest individual delay.

Program.cs
using System.Diagnostics;

var sw = Stopwatch.StartNew();

var tasks = new[] { F1Async(), F2Async(), F3Async() };
await Task.WhenAll(tasks);

sw.Stop();
Console.WriteLine($"elapsed: {sw.ElapsedMilliseconds} ms");

static async Task F1Async()
{
    await Task.Delay(4000);
    Console.WriteLine("f1 finished");
}

static async Task F2Async()
{
    await Task.Delay(7000);
    Console.WriteLine("f2 finished");
}

static async Task F3Async()
{
    await Task.Delay(2000);
    Console.WriteLine("f3 finished");
}

Each method returns a Task. The calls begin before Task.WhenAll is awaited, so the output normally appears in the order in which the delays finish.

$ dotnet run
f3 finished
f1 finished
f2 finished
elapsed: 7000 ms

C# async Main method

Top-level statements support await directly. In a traditional program, the entry point can instead be declared as static async Task Main. The following example uses the traditional form.

Program.cs
using System.Diagnostics;

class Program
{
    static async Task Main()
    {
        var sw = Stopwatch.StartNew();

        Console.WriteLine("task 1");
        Task task1 = DoWorkAsync();

        Console.WriteLine("task 2");
        Task task2 = DoWorkAsync();

        Console.WriteLine("task 3");
        Task task3 = DoWorkAsync();

        await Task.WhenAll(task1, task2, task3);

        sw.Stop();
        Console.WriteLine("Tasks finished");
        Console.WriteLine($"elapsed: {sw.ElapsedMilliseconds} ms");
    }

    static async Task DoWorkAsync()
    {
        await Task.Delay(1500);
    }
}

The three calls start the operations, and Task.WhenAll completes when all three delays have finished. An async entry point lets the program await the work without blocking the main thread.

$ dotnet run
task 1
task 2
task 3
Tasks finished
elapsed: 1500 ms

C# reading files asynchronously

The File.ReadAllTextAsync method opens a text file, reads its contents asynchronously, and closes the file. Starting all reads before awaiting their combined task allows independent file operations to overlap.

Program.cs
var paths = new[] { "data1.txt", "data2.txt", "data3.txt" };
var tasks = paths.Select(path => File.ReadAllTextAsync(path)).ToArray();

Console.WriteLine("doing some work");
var contents = await Task.WhenAll(tasks);

foreach (var content in contents)
{
    Console.WriteLine(content.TrimEnd());
}

Task.WhenAll returns a task that completes when all of the read operations finish. Its result is an array containing the text in the same order as the input tasks. The files must exist in the program's working directory.

var contents = await Task.WhenAll(tasks);

Awaiting the combined task asynchronously unwraps all of the results. If one or more reads fail, the awaited task reports the failure.

C# CPU-bound async tasks

CPU-bound work can be moved to a thread-pool thread with Task.Run. This can keep a UI thread or request thread responsive, but it does not make the calculation itself faster. For server code, use this technique deliberately; do not wrap ordinary asynchronous I/O in Task.Run.

Program.cs
using System.Text;

var tasks = new[]
{
    Task.Run(() => CountCharacters("abc", 100_000)),
    Task.Run(() => CountCharacters("xyz", 100_000))
};

var lengths = await Task.WhenAll(tasks);
Console.WriteLine($"first length: {lengths[0]}");
Console.WriteLine($"second length: {lengths[1]}");

static int CountCharacters(string value, int repetitions)
{
    var builder = new StringBuilder(value.Length * repetitions);

    for (int i = 0; i < repetitions; i++)
    {
        builder.Append(value);
    }

    return builder.Length;
}

Each Task.Run queues a CPU-bound delegate to the thread pool. Task.WhenAll awaits both calculations, and each result is returned in the same order as the tasks array.

$ dotnet run
first length: 300000
second length: 300000

C# multiple asynchronous requests

HttpClient sends HTTP requests and receives HTTP responses. The following example starts several requests, awaits them together, and reports the HTTP status code for each URL. A single HttpClient is reused for all requests.

Program.cs
var urls = new[]
{
    "https://example.com",
    "https://www.iana.org/domains/example",
    "https://httpbin.org/status/200"
};

using var client = new HttpClient();
var tasks = urls.Select(async url =>
{
    using var response = await client.GetAsync(url);
    return $"{url}: {(int) response.StatusCode} {response.StatusCode}";
});

var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
    Console.WriteLine(result);
}

The asynchronous GetAsync calls begin as the sequence is enumerated. Task.WhenAll waits for every request and preserves the order of the input URLs in its results. Network availability and server responses can vary, so the output is illustrative.

$ dotnet run
https://example.com: 200 OK
https://www.iana.org/domains/example: 200 OK
https://httpbin.org/status/200: 200 OK

Source

Asynchronous programming with async and await

Task.WhenAll method

File.ReadAllTextAsync method

In this article we have used the async and await keywords, tasks, and asynchronous APIs to write concurrent C# programs.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.