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.
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}";
}sum = 500500, done = True
user-7
cached done = True, value = 42Add static async Task LogAsync(string msg) that waits 10 ms and prints the message, and await it between the two calls.
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);sequential took at least 600 ms: True
concurrent was faster: TrueThree 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
Asyncand returnTask
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
CS0029: forgetting the await
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'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.
Put await in front of the call. The calling method must itself be async (top-level statements already are).
int count = await GetCountAsync();
Console.WriteLine(count);
static async Task<int> GetCountAsync()
{
await Task.Delay(100);
return 3;
}