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.
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.
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.
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.
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.
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.
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
In this article we have used the async and await
keywords, tasks, and asynchronous APIs to write concurrent C# programs.
Author
List all C# tutorials.