Async and Event-Driven Object Design
Learning Objectives
Use callbacks, futures/promises and async/await.
Handle cancellation, timeouts, ordering, back-pressure and error propagation.
Design async interfaces and async disposal.
Prerequisites: Modules 41, 60.
Intuition and Definition
Asynchronous code starts an operation and continues without blocking; the result arrives later via a callback, a future/promise (a placeholder for a value), or await (syntax that suspends a function until a promise completes without blocking the thread).
Key concerns: cancellation (stop work no longer needed), timeouts, ordering (results may complete out of order), back-pressure (don't accept work faster than you can process), error propagation (a rejected promise must reach someone), and async disposal (closing resources asynchronously: await using in C#, AsyncExitStack/async with in Python).
Why It Matters
I/O-bound programs — servers, UIs, telematics gateways — spend most of their time waiting. Async lets one thread serve thousands of waits, but object lifetimes and error handling become subtler.
Syntax and Language Notes
public async Task<Reading> ReadAsync(CancellationToken ct) { await Task.Delay(50, ct); return new Reading(); }std::future<int> f = std::async(std::launch::async, [] { return 42; }); // C++: futures; C++20 has coroutines
int v = f.get();Complete Example
A TypeScript client that queries several ECUs concurrently with a timeout per request and cancellation via AbortController (timers simulate network latency so the run is deterministic):
The same ideas in Python asyncio, including ordering and bounded concurrency with a semaphore:
const sleep = (ms: number, signal?: AbortSignal) =>
new Promise<void>((resolve, reject) => {
const t = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => { clearTimeout(t); reject(new Error("aborted")); });
});
async function readDtcs(ecu: string, latencyMs: number, signal: AbortSignal): Promise<string> {
await sleep(latencyMs, signal);
if (ecu === "ABS") throw new Error("ABS: negative response 0x22");
return `${ecu}: 0 DTCs`;
}
async function withTimeout<T>(work: (s: AbortSignal) => Promise<T>, ms: number): Promise<T> {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms);
try { return await work(ctrl.signal); }
finally { clearTimeout(timer); }
}
async function main() {
const ecus: [string, number][] = [["Engine", 30], ["ABS", 10], ["Cluster", 200], ["Gateway", 20]];
const results = await Promise.allSettled(
ecus.map(([name, ms]) => withTimeout((s) => readDtcs(name, ms, s), 100)),
);
results.forEach((r, i) =>
console.log(ecus[i][0].padEnd(8), r.status === "fulfilled" ? r.value : `FAILED (${(r.reason as Error).message})`),
);
const first = await Promise.race(ecus.filter(([n]) => n !== "AB
…Engine Engine: 0 DTCs
ABS FAILED (ABS: negative response 0x22)
Cluster FAILED (aborted)
Gateway Gateway: 0 DTCs
first answer: Gateway: 0 DTCsimport asyncio
async def read(ecu, delay, sem, log):
async with sem: # at most 2 requests in flight (back-pressure)
log.append(f"start {ecu}")
await asyncio.sleep(delay)
log.append(f"done {ecu}")
return f"{ecu} ok"
async def main():
sem, log = asyncio.Semaphore(2), []
tasks = [read("Engine", 0.03, sem, log), read("ABS", 0.01, sem, log), read("Cluster", 0.02, sem, log)]
results = await asyncio.gather(*tasks) # results in REQUEST order, completion order differs
print(results)
print("\n".join(log))
try:
await asyncio.wait_for(asyncio.sleep(1), timeout=0.05)
except asyncio.TimeoutError:
print("timed out after 50 ms (task cancelled)")
asyncio.run(main())['Engine ok', 'ABS ok', 'Cluster ok']
start Engine
start ABS
done ABS
done Engine
start Cluster
done Cluster
timed out after 50 ms (task cancelled)Step-by-Step Walkthrough
All four TypeScript requests start immediately; Promise.allSettled waits for all, collecting successes and failures without one failure cancelling the others.
The cluster takes 200 ms, more than the 100 ms timeout: the AbortController fires, sleep rejects with "aborted", and the timer is cleared in finally.
Promise.race returns the first to settle (Gateway at 20 ms).
In Python, gather returns results in the order requested even though ABS finishes first; the semaphore lets Cluster start only after a slot frees up.
wait_for cancels the slow task on timeout.
sequenceDiagram
participant C as Client
participant E as Engine
participant A as ABS
participant K as Cluster
C->>E: read (timeout 100 ms)
C->>A: read
C->>K: read
A-->>C: error 0x22
E-->>C: 0 DTCs
Note over C,K: 100 ms: abort ClusterCommon Mistakes
Fire-and-forget promises/tasks whose errors are never observed (unhandled rejections).
Blocking inside async code (.Result/.Wait() in C#, time.sleep in asyncio) — stalls the event loop or deadlocks.
No timeouts or cancellation on network calls.
Assuming completion order = start order. Unbounded concurrency (launching 10,000 requests at once).
Object lifetime: an object destroyed while its async operation still runs and later touches it (C++ callbacks capturing this).
Trade-offs and Alternatives
Async improves scalability for I/O-bound work but complicates control flow, debugging and lifetimes. Threads are simpler for CPU-bound work. Message queues and actors decouple even further. Observer is in-process and synchronous; queues add buffering and persistence.
Real-World Use
Web servers (Node.js, ASP.NET Core, FastAPI), mobile/UI apps, diagnostic testers polling many ECUs, IoT gateways, database drivers.
Practice
Q1. (Easy) What does Promise.allSettled do differently from Promise.all?
Answer: It waits for all promises and reports each result or error; Promise.all rejects as soon as one rejects.
Q2. (Medium) Why is .Result on a Task dangerous in some C# applications?
Answer: It blocks the thread; in contexts with a synchronization context (UI, older ASP.NET) the awaited continuation needs that same thread → deadlock. Use await all the way.
Q3. (Advanced) In C++, how do you stop a callback from using a destroyed object?
Answer: Capture a weak_ptr to the object (from shared_from_this) and lock() it in the callback, or cancel/unregister pending operations in the destructor and wait for in-flight callbacks to finish.
Interview Question
Q1. "How do you add a timeout and cancellation to an async operation that doesn't support them?"
Answer: Race it against a timer (Promise.race, Task.WhenAny, asyncio.wait_for) so the caller stops waiting; propagate a cancellation token/signal into every layer that can actually stop work (I/O, loops) so resources are freed; clean up timers in finally; and make the operation idempotent or compensable, because a timed-out operation may still have completed on the server side.
Quick Quiz
Q1. What object represents a value that will be available later?
Answer: A future/promise (or task).
Q2. Python primitive for limiting concurrent coroutines?
Answer: asyncio.Semaphore.
Q3. What is back-pressure in async systems?
Answer: Slowing or rejecting producers when consumers can't keep up.
Summary
Async = start now, result later; design for cancellation, timeouts, ordering, back-pressure and errors.
Never block inside async code; bound concurrency; observe every failure.
Related: Modules 41, 51, 60, 76.