Why Async Exists

A thread waiting on the network does no work; async hands that thread back so a server can serve more requests at once.

Syntaxawait SomeOperationAsync(); // the thread is released while this waits

A program spends a lot of its life waiting. It asks a database for rows, or calls another service over the network, and then there is nothing to do until the answer comes back.

What a thread costs

A thread is one line of execution inside your program. The operating system schedules it onto a processor core, and it reserves memory for its stack, around a megabyte by default on Windows. If a thread is parked waiting for a network reply, it still holds that memory and still occupies a slot in the pool of threads .NET keeps available. It does no useful work while it waits.

What async changes

Asynchronous code gives the thread back while it waits. The operating system already knows how to signal your process when data arrives on a socket, so no thread needs to sit and watch. When the reply arrives, the runtime picks a free thread and continues the code from where it left off.

The payoff is throughput: how many requests a server completes per second. A server with a hundred threads can have thousands of requests in flight if almost all of them are waiting.

What trips people up

Async does not make one call faster. A request that takes 80 milliseconds on the wire still takes 80 milliseconds. Async also does not help with work that keeps a processor busy, such as resizing an image. That work needs a thread the whole time, and the tool for it is different.

Example

Example · csharp
// Sequential: the second wait starts only after the first one ends.
// This pair takes about 600 milliseconds.
string users = await LoadAsync("users", 300);
string orders = await LoadAsync("orders", 300);
Console.WriteLine($"sequential: {users}, {orders}");

// Overlapped: both waits run at the same time, so this pair takes
// about 300 milliseconds. Neither version pins a thread while waiting.
Task<string> usersTask = LoadAsync("users", 300);
Task<string> ordersTask = LoadAsync("orders", 300);
Console.WriteLine($"overlapped: {await usersTask}, {await ordersTask}");

// Each call stands in for a slow network request. Task.Delay waits
// without holding on to a thread for the duration of the wait.
static async Task<string> LoadAsync(string name, int milliseconds)
{
    await Task.Delay(milliseconds);
    return name;
}

// Output:
// sequential: users, orders
// overlapped: users, orders

When to use it

  • Deciding whether a slow endpoint needs async or more processor time.
  • Explaining why a service with 200 threads can hold 5,000 open connections.
  • Spotting Thread.Sleep in request-handling code during a review.

More examples

Blocking the thread, for comparison

The two pairs of lines look equivalent and print in the same order, which is exactly the point: the difference is invisible in the output and visible in the thread count. Thread.Sleep is the shape to recognise and avoid in server code; it is the thing that quietly caps how many requests a service can hold at once.

Example · csharp
// Thread.Sleep parks the current thread. For the whole second it holds
// its stack memory and its place in the thread pool, and does nothing.
Console.WriteLine("blocking start");
Thread.Sleep(1000);
Console.WriteLine("blocking end");

// Task.Delay schedules a timer and returns a Task. The thread goes back
// to the pool and can run other work during the second.
Console.WriteLine("async start");
await Task.Delay(1000);
Console.WriteLine("async end");

// Output:
// blocking start
// blocking end
// async start
// async end

Where async does not help

Marking this method async would change nothing, because it never waits for anything outside the process. There is no point at which a thread could be released. Work like this is the case for Task.Run or parallel loops, not for async, and knowing which kind of work you have is the first decision.

Example · csharp
// CPU-bound work: every millisecond of this needs a thread.
static long CountPrimes(int limit)
{
    long found = 0;

    for (int candidate = 2; candidate <= limit; candidate++)
    {
        bool isPrime = true;

        for (int divisor = 2; divisor * divisor <= candidate; divisor++)
        {
            if (candidate % divisor == 0)
            {
                isPrime = false;
                break;
            }
        }

        if (isPrime)
        {
            found++;
        }
    }

    return found;
}

Console.WriteLine($"primes below 10000: {CountPrimes(10000)}");

// Output:
// primes below 10000: 1229

Discussion

  • Be the first to comment on this lesson.