advanced24 min studyLesson 161 in Engineering

Blocking, readiness, and completion are different contracts

Trace when an I/O operation starts, when a thread may wait, and which observation actually supplies a result.

On this page

What you’ll understand

  • Distinguish a readiness notification from an operation result.
  • Trace nonblocking retry state without losing offsets.
  • Bound useful overlap using explicit resource assumptions.

A server receives a notification that a connection is readable. Has the requested message arrived, has a read already happened, or is the application merely permitted to try? These interpretations lead to different state machines. Asynchronous programming becomes much easier to reason about when every notification is matched to the promise it actually makes, and every resource retains an owner until that promise is discharged.

Locate the waiting boundary

A blocking interface may keep the calling thread inside an operation while progress depends on another component. The thread can be asleep rather than consuming CPU, but it cannot simultaneously execute your next handler. A nonblocking interface instead reports that the operation cannot presently make progress. That response is ordinary control flow. It is neither an empty message nor proof that the peer has failed. The application must save its current offset and arrange another opportunity to try.

An asynchronous completion interface accepts an operation whose result arrives later. Submission and completion are separate observations: acceptance alone does not provide bytes or establish success. Meanwhile, a readiness interface reports a condition associated with a resource; the application still performs the operation. Both can support overlapping connections, and both require error handling. Ask which exact API produces the byte count before deciding which component owns the transition from waiting to usable data.

Tserial=∑i=1n(wi+ci)T_{\mathrm{serial}}=\sum_{i=1}^{n}(w_i+c_i)
For strictly sequential independent jobs, w_i is external waiting time in seconds and c_i is application CPU time in seconds. The sum assumes no overlap and excludes dispatch overhead.↔ Scroll wide equations horizontally.

Readiness is an invitation to attempt progress

Linux epoll reports readiness rather than performing a read for the application. Use nonblocking descriptors so a stale observation cannot trap an event-loop thread inside a blocking call. Another consumer or a change in state can alter what an attempted operation observes. Inspect its actual return value, including positive progress, end of stream, interruption, or the would-block condition. Protocol framing remains a separate responsibility: readable bytes do not automatically constitute a complete application message.

Level-triggered and edge-triggered notification policies change how the application revisits resources. In an edge-triggered design, preserve knowledge that work remains until nonblocking I/O reaches its stopping condition. If a fairness budget interrupts draining, place the resource on an application-ready list rather than blindly waiting for another edge. This combines progress with scheduling discipline. The resource state must say both where byte processing stopped and whether the dispatcher still owes it attention.

Toverlap≥max⁡ ⁣(∑ici, max⁡iwi)T_{\mathrm{overlap}}\geq\max\!\left(\sum_i c_i,\ \max_i w_i\right)
This deliberately weak lower bound assumes one application CPU, all independent waits start together at time zero, and enough external capacity. It is not a guarantee of an achievable schedule; dependent waits and overhead raise elapsed time.↔ Scroll wide equations horizontally.

Overlap waits without inventing capacity

A thousand outstanding requests do not create a thousand processors or unlimited downstream bandwidth. Overlap can hide independent waiting, while CPU work, connection limits, buffer memory, and dependency capacity still constrain throughput. Bound outstanding operations and record how many are waiting, runnable, and terminal. An increase in concurrency is useful only if completed useful work improves within latency and memory budgets. A submitted operation that will eventually be abandoned still consumes some resources now.

Use the queue lab as a capacity analogy, not an epoll or completion simulator. Press Reset, set arrival rate to 2 and service rate to 3, predict whether the initial four-job fluid backlog drains, then press Play. For an independent comparison, press Reset again, explicitly set arrival rate to 4 and service rate to 3, predict growth, then press Play. These are fresh runs from four jobs, not a live rate change applied to an old backlog. The model cannot distinguish readiness from completion or expose notification races. Build a separate paper trace with operation identifier, attempted byte count, returned byte count, and next state to test that distinction.

Separate waiting from application work

Four independent fetches each wait exactly 20 milliseconds externally and then need 2 milliseconds on one application CPU. Assume all waits can start together, finish together, and incur no dispatch or transfer overhead. Compare two explicit schedules.

  1. A strictly sequential schedule takes four times 22 milliseconds, giving 88 milliseconds. Each wait begins only after the previous computation finishes.

  2. Start all four independent waits at time zero. They finish at 20 milliseconds; the single CPU then processes four results for 8 milliseconds, finishing at 28 milliseconds.

  3. The constructed speedup is 88/28, about 3.14. It depends on the stated simultaneous-start and external-capacity assumptions; a saturated dependency or sequential authentication would change the schedule.

The result

The asynchronous interface permits a better overlap schedule here. It does not eliminate the eight milliseconds of application CPU work.

Explore the model

Interactive lab

Interactive lab · queue

Outstanding work still needs capacity

Press Reset, set arrival rate to 2 and service rate to 3, predict the slope, then press Play. Repeat from Reset with both rates explicitly set to 4 and 3. Each run starts with four jobs; this illustrates capacity balance, not readiness, completion queues, or individual request timelines.

Outstanding work still needs capacityFluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Current readings: Fluid backlog: 4; Net rate · λ − μ: −1 /s; M/M/1 mean residence: 1 s. All parameters are adjustable with the labeled controls below.02.557.51001.534.56Elapsed time · sFluid backlogλ < μ
Fluid backlogCurrent time
Fluid backlog
4
Net rate · λ − μ
−1 /s
M/M/1 mean residence
1 s
t = 0 s

Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.

Look forOverlapping waits helps only within the capacities and resource limits that still apply.

Check your understanding

After a readiness notification, a nonblocking read reports that it would block. What should a correct handler do?

Try it yourself

Two operations each wait 30 milliseconds and then require 5 milliseconds on one CPU. Under simultaneous independent waits and negligible overhead, construct an overlapped schedule and compare it with a sequential one.

Take a moment to make a prediction or work it out on paper. Open a hint when you need one.

Give me a hint

Draw external waits first; serialize only the CPU portions after both waits finish.

Compare with the solution

The sequential schedule takes 2 × (30 + 5) = 70 milliseconds. Starting both waits together permits CPU work from 30 to 35 and 35 to 40 milliseconds, giving 40 milliseconds. This is a constructed ideal schedule, not a promise from a language keyword. Shared external limits, dependencies, and dispatch costs can increase it.

Take it with you

  • Submission, readiness, and completion answer different questions.
  • Nonblocking handlers trust operation results and preserve partial state.
  • Concurrency overlaps waits while capacity and memory remain bounded.

Read further

Original sources and deeper explanations to follow at your own pace.

Optional progress, saved in this browser when available. Revisit any lesson at any time.

Lesson not marked complete.