Synchronous vs Asynchronous
By the end of this lesson
Explain what blocking costs and when asynchronous code helps.
Every line of code you have written so far has run on a thread. A thread is a worker: it picks up your instructions and carries them out one after another. Your program gets one to start with, and can be given more.
Most methods finish by doing work. Some methods finish by waiting. When you ask a database for a row, or an API for a price, or a disk for a file, your code sends the request and then does nothing at all until the answer comes back. The thread is still assigned to you. It is still consuming memory. It is producing nothing.
That is blocking, and it is what asynchronous code exists to avoid.
Two kinds of slow, and they need different treatment
Before you reach for anything, decide which kind of slow you have. Asynchronous code helps with one of them and does nothing at all for the other.
The distinction is about who is doing the work — you, or something outside your process.
| I/O-bound work | CPU-bound work | |
|---|---|---|
| What the work is | Waiting on something external: a database query, an HTTP call, reading a file, a message queue | Your own code calculating: parsing a large file, sorting in memory, resizing an image, hashing |
| What your thread is doing meanwhile | Nothing. It waits for a result someone else is producing | Running flat out. It is the thing producing the result |
| What async gives you | The thread is released for the duration of the wait | Nothing. There is no idle wait to release |
| What actually helps | async and await | Splitting the work across processor cores, or making the algorithm cheaper |
| Effect on how long one operation takes | Roughly unchanged | Can genuinely fall, up to the number of cores you have |
Two summary sentences worth keeping: asynchronous code is about not occupying a thread while something else works. Parallelism is about occupying several threads on purpose, because you have work for all of them.
They are different tools for different problems, and using one where the other belongs is the most common source of disappointment with async.
The same operation, written both ways
public class RateService
{
private readonly HttpClient _httpClient;
public RateService(HttpClient httpClient) => _httpClient = httpClient;
// Synchronous. The calling thread is held for the whole round trip.
public decimal GetRate(string currencyCode)
{
string body = _httpClient
.GetStringAsync($"rates/{currencyCode}")
.GetAwaiter()
.GetResult(); // blocks here
return decimal.Parse(body);
}
// Asynchronous. The thread is released while the network does its work.
public async Task<decimal> GetRateAsync(string currencyCode)
{
string body = await _httpClient.GetStringAsync($"rates/{currencyCode}");
return decimal.Parse(body);
}
}- Both methods do the same amount of network work and take about the same wall-clock time. Nothing here is faster.
- In GetRate, .GetAwaiter().GetResult() means "stand here until the answer arrives". The thread that called GetRate cannot do anything else until then. The next lesson but one explains why this form is dangerous as well as wasteful.
- In GetRateAsync, await hands the thread back. Your remaining code — the decimal.Parse line — is recorded as the thing to run once the response arrives.
- The return type changes from decimal to Task<decimal>, and the convention is to suffix the name with Async. Both of those signal to a caller that this method finishes later.
- Nothing in the asynchronous version creates a thread. That is worth saying out loud, because the mental picture of "async starts a background thread" is both common and wrong.
Where the payoff actually shows up
On a server, requests are served by threads from a shared pool. The pool is finite, and when it runs short it adds threads slowly rather than all at once.
Picture an API endpoint that spends 300 milliseconds waiting on a database. Written synchronously, each in-flight request holds a thread for that whole 300 milliseconds. Once every thread in the pool is parked in front of the database, new requests queue. Response times climb even though the database is barely working and the processor is close to idle.
Written asynchronously, each request gives its thread back during the wait. The same pool serves far more concurrent requests, because threads are only held while there is something to do. The word for what improved is throughput: requests handled per second, not the speed of any one request.
The same reasoning applies outside servers, with a different benefit:
- Desktop and mobile apps have one thread that draws the interface. Block it and the window freezes. Awaiting keeps it free to redraw and respond.
- A background worker processing a queue can have many messages in flight without one thread per message.
- A console tool doing a single HTTP call gains nothing measurable, because there is no other work waiting for the thread. It is still worth writing asynchronously so the code matches the libraries it calls.
Summary
- Blocking means holding a thread while something outside your process produces a result
- Asynchronous code releases the thread during that wait — it does not make the work faster
- I/O-bound work benefits from async; CPU-bound work needs parallelism or a better algorithm
- On a server the gain is throughput: more concurrent requests served by the same pool of threads
- In an app with a user interface the gain is responsiveness, because the drawing thread stays free
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Think about it
An API has an endpoint that calls a payment provider. The call takes about two seconds. The service handles 40 requests per second comfortably in testing, then falls over in production at around 60, with response times of 30 seconds and a processor sitting at 8% usage.
All the database work and the provider call are written synchronously. What is the resource that ran out, and why does low processor usage point at it rather than away from it?
Show solution
Threads ran out, not processor capacity. Each request holds a pool thread for the full two-second wait, so the number of requests the service can have in flight is capped at roughly the number of threads available.
Low processor usage is the evidence, not a contradiction. If the machine were short of computing power it would be busy. A nearly idle processor alongside long response times means the threads are present but parked, waiting on something external.
Making the provider call and the database work asynchronous releases those threads during the wait. The payment provider is no faster, so each individual request still takes about two seconds. What changes is how many requests can be waiting at once without queuing.
Try it yourself
Classify the work
For each of these, decide whether it is I/O-bound or CPU-bound, and therefore whether async is the right tool: (1) generating a PDF invoice from data already in memory, (2) uploading that PDF to blob storage, (3) recalculating tax across 200,000 order lines, (4) sending a confirmation email through an SMTP server, (5) checking whether a postcode matches a regular expression.
Show solution
I/O-bound, so async is appropriate: 2 and 4. Both are round trips to something outside your process, and in both cases your thread would otherwise sit idle.
CPU-bound, so async does nothing: 1, 3 and 5. Your own code is doing the work. Item 3 is the only one big enough to be worth splitting across cores; 1 and 5 are likely fast enough that any change is wasted effort.
The useful habit is to ask "who is producing the result?" If the answer is a network, a disk or another process, async helps. If the answer is this program, it does not.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.