awesome-everything RU
↑ Back to the climb

Base CS from zero

Blocking vs non-blocking

Crux A blocking call does not return until the work is done — its stack frame is stuck waiting. A non-blocking call returns immediately and delivers the result later through a callback: a function handed over to be run when the answer is ready.
◷ 20 min

The previous lesson said async means the CPU does other useful work while a slow device answers. That is the goal. But it leaves a sharp question unanswered: how can a program even keep going past a line that reads a file, when the file is not there yet?

In the model from Unit 08, a program runs one line, then the next, then the next. If line 3 reads a disk file and line 4 uses that file, then line 4 plainly cannot run until line 3 has the data. The program seems forced to wait.

The escape is in how the read on line 3 is written. There are two kinds of call a program can make to a slow device, and they behave completely differently at exactly this point. One forces the wait; the other does not. This lesson contrasts the two — blocking and non-blocking — on the very same task.

Goal

After this lesson you can define a blocking call as one that does not return until its work is done, define a non-blocking call as one that returns immediately, explain what a callback is and how it delivers a deferred result, and trace what the call stack does in each case.

1

A blocking call: it does not return until the work is done. Recall from Unit 08 that calling a function pushes a stack frame, and the function returns when its work is finished — at which point its frame is popped and the caller continues at the next line.

A blocking call is a call that follows that rule even when the work is slow. The call to read a disk file does not return until the file’s bytes are actually in hand. The function’s stack frame stays on the stack the entire time the disk is working — ten milliseconds, ten million cycles — doing nothing, just occupying the stack.

This is “blocking” in the literal sense: the call blocks the line below it from running. Line 4 cannot start because line 3 has not returned. The whole program is frozen at that frame until the device answers. This is exactly the synchronous, sit-idle behaviour from lesson 01 — now seen at the level of a single function call.

2

A non-blocking call: it returns immediately. A non-blocking call is a call to a slow device that does not wait for the work to finish. It does only the quick part — hand the request to the device — and then returns right away, in a handful of cycles, long before the device has produced any answer.

So the function’s stack frame is pushed and almost immediately popped. The call returns, the caller continues to the next line, and the program keeps running. The disk read is now happening in the background, handled by the device itself, while the CPU moves on.

But this creates an obvious gap. The call returned before the data existed — so the call could not have given back the data the way an ordinary function returns a value. The result will exist only later, after the device finishes. The program needs some way to receive that result whenever it eventually shows up. That mechanism is the callback.

3

A callback: a function handed over to be run later. Recall from Unit 08 that a function is a named, reusable block of instructions, and that a function can be passed around as a value — given to other code by name.

A callback is exactly that: a function that your program writes and then hands to the non-blocking call, with the instruction “run this for me when the result is ready.” The program does not call the callback itself. It gives the callback away and walks on.

When the slow device finally finishes — milliseconds later — the result is delivered by calling the callback, passing the result in as an argument (Unit 08: arguments are the values handed to a function when it is called). The callback’s body then runs, with the data finally available. So the structure is split in two: the non-blocking call starts the work and returns; the callback contains the code that uses the result, and runs separately, later. The line “use the file” no longer sits right below “read the file” — it lives inside the callback instead.

Why this works

Why hand over a function — why not just return the data? Because the data does not exist yet when the call returns. An ordinary return hands back a value that is ready now. A non-blocking call has nothing ready now — only a promise of something later. You cannot return a value that does not exist. So instead of returning data, the program supplies code: the callback. It is the program saying, in advance, “here is what to do once the data does exist.” Handing over code is the only way to bridge a result that arrives in the future.

4

The same task, the two ways. Take one concrete task: read a user’s name from disk, then greet them.

Blocking. Line 1: name = readFileBlocking("user.txt"). This call blocks — its frame sits on the stack for the full 10 ms. Only when the bytes arrive does it return, putting them in name. Line 2: print("Hello, " + name) runs next. Simple to read, but the CPU was frozen for 10 ms between the two lines.

Non-blocking. Line 1: readFileNonBlocking("user.txt", greet). This call hands the request to the disk and hands over greet as the callback, then returns immediately. Line 2 — whatever it is — runs right away; the CPU is not frozen. About 10 ms later the disk finishes, and greet is called with the name as its argument; greet’s body does the print.

Same task, same disk, same 10 ms of device time. The difference is whether the CPU spent those 10 ms frozen on a stuck frame (blocking) or running other lines while a callback waited its turn (non-blocking).

call
STUCK
STUCK
return
next line
Blocking: the call's frame is STUCK on the stack until the device answers, and the next line cannot run. Non-blocking removes the STUCK cells — the call returns at once and the next line runs immediately, with the callback delivering the result later.
Worked example

Tracing the call stack for both versions.

Assume the disk read takes 10 ms. The CPU runs 1,000,000 cycles per ms.

Blocking version — name = readFileBlocking("user.txt"):

  1. The call pushes a stack frame for readFileBlocking.
  2. The frame sends the request to the disk — then stays on the stack, waiting.
  3. For the next 10 ms (10,000,000 cycles) that frame just sits there. The call has not returned, so nothing below it runs. The CPU is frozen.
  4. The disk finishes. readFileBlocking finally returns the bytes; its frame is popped.
  5. Now the next line runs. Total CPU work done during the wait: 0 instructions.

Non-blocking version — readFileNonBlocking("user.txt", greet):

  1. The call pushes a stack frame for readFileNonBlocking.
  2. The frame hands the request to the disk and stores greet as the callback to run when the disk is done. That is all quick work.
  3. The call returns immediately — its frame is popped after only a handful of cycles.
  4. The next line runs right away. The CPU keeps working for the whole 10 ms.
  5. About 10 ms later the disk finishes; greet is called with the name. Total CPU work done during the wait: up to 10,000,000 instructions of other code.

The disk took 10 ms in both. Only the non-blocking version kept the CPU busy — and it did so by splitting the task across a non-blocking call and a callback.

Common mistake

A common mistake is to think a non-blocking call has already produced the result by the time it returns. It has not. When readFileNonBlocking returns, the disk has barely started — the bytes will not exist for another 10 ms. Anything that needs the result must go inside the callback, not on the line right after the call. Code placed right after a non-blocking call runs while the result is still missing.

Practice 0 / 5

A blocking disk-read call is made. The disk takes 10 ms. For how many milliseconds does that call's stack frame stay on the stack before the call returns?

A non-blocking disk-read call is made. The quick part (handing the request to the disk) takes a handful of cycles. Roughly how many milliseconds until the call returns? Type 0 for 'effectively immediately'.

A program makes a non-blocking call and hands over a callback. Who calls that callback when the device finishes — type 1 for the program's own next line, 2 for the mechanism that delivers the result?

The disk read itself takes 10 ms. Using a non-blocking call instead of a blocking one, how many milliseconds does the disk's own work take?

In the non-blocking version, the code that uses the file's contents lives where — type 1 for the line right after the call, 2 for inside the callback?

Check yourself
Quiz

What is the difference between a blocking and a non-blocking call, and what is a callback for?

Recap

A blocking call does not return until its work is finished. When that work is a slow device, the call’s stack frame stays stuck on the stack for the whole wait, and the line below it cannot run — the program is frozen. A non-blocking call does the opposite: it hands the request to the device and returns immediately, so the program keeps running while the device works in the background. Because the call returns before the result exists, the result cannot be returned the ordinary way. Instead the program supplies a callback — a function (Unit 08) handed to the non-blocking call with the instruction “run this when the result is ready.” When the device finishes, the result is delivered by calling that callback with the result as an argument. Blocking keeps all the code in one straight line but freezes the CPU; non-blocking splits the task across a call and a callback but keeps the CPU free. The next lesson shows the mechanism that actually runs those callbacks.

Continue the climb ↑The event loop
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.