Problem and context
C++26 is standardizing the std::execution sender/receiver model. Given three stages—reading network chunks, parsing records, and writing batches to disk—compose a sender pipeline. The caller may request stop and may place stages on I/O or CPU schedulers. Explain the value, error, and stopped completion channels and resource cleanup.
What the interviewer evaluates
The key distinction is between a lazy sender description and an operation state created after connection: connect builds state, while start begins work. Explain how setvalue, seterror, and set_stopped move through the pipeline, whether scheduler changes affect thread affinity, and how cancellation prevents new side effects.
Clarifying questions to ask first
Work and backpressure
Ask about batch size, permitted concurrency, input ordering, and retry policy for failed writes. The answers determine whether to limit sender concurrency or bound a queue.
Scheduling and stop sources
Ask how I/O and CPU schedulers are exposed, whether stops come from a timeout or a user action, and how an already-submitted system call is completed. A stop request is not forced thread termination.
Resources and commit semantics
Clarify ownership of file handles, buffers, and temporary files, whether writes are idempotent, and whether partial batches can roll back. This defines cleanup after set_error.
30-second answer framework
“I describe read, parse, and write with sender adaptors, then use scheduler adaptors to move execution contexts. The pipeline stays lazy: connect creates an operation state and start begins it. Each stage sends success values downstream, errors through seterror, and cancellation through setstopped; one stop token reaches every stage. The stop path checks before side effects, while owners finish and close submitted I/O and clean temporary files.”
Detailed solution steps
Step 1: Define value and error boundaries
Define input and output types for each stage and turn recoverable failures into explicit error senders. Do not let exceptions cross scheduler boundaries; let the final receiver record success, failure, or stop.
Step 2: Compose a lazy pipeline
Use let_value, then, or equivalent adaptors to connect reading, parsing, and writing. Composition builds a description without allocating threads or performing I/O; operation state owns any shared state that must outlive a callback.
Step 3: Connect and start
Call connect(sender, receiver) to obtain an operation state, keep it in a live scope, then call start. The receiver must outlive the operation state; asynchronous callbacks cannot reference destroyed stack objects.
Step 4: Move between execution contexts
After I/O completes, use a scheduler sender to move parsing to a CPU pool, then return writing to a bounded I/O pool. Record queue capacity and fairness; do not put blocking writes in an unbounded general pool.
Step 5: Propagate stop and backpressure
Pass a stop token to every interruptible stage. After a stop, do not enqueue new batches; cancel or finish an in-flight system call according to its API. When the queue is full, a throttling sender pauses upstream work to bound memory.
Step 6: Handle errors and partial side effects
Write to a temporary file or record a batch sequence before atomically committing. set_error triggers downstream cleanup and handle closure. Retries need limits and an idempotency key so they cannot duplicate writes.
Step 7: Verify concurrency and lifetime
Test multiple schedulers, stop races, parse failures, short writes, and early receiver destruction. Use a thread analyzer for races and measure queue time, throughput, stop latency, and unreleased operation states.
High-quality sample answer
The read sender emits batches, the parse sender runs on a CPU scheduler, and the write sender runs on a bounded I/O scheduler. The pipeline only describes dependencies; connect creates operation state and start launches it. Every stage handles value, error, and stopped completion and shares a stop token. Stopping blocks new batches, lets submitted I/O close safely, and uses temporary files plus batch IDs for idempotent retries. Tests cover scheduler hops, backpressure, stop races, and receiver lifetime.
Common mistakes
- Mistake: Assuming a constructed sender is already running. → Why: A sender is lazy. → Fix: State the connect/start boundary.
- Mistake: Handling exceptions but not stopped completion. → Why: Stop is a separate completion channel. → Fix: Implement both seterror and setstopped.
- Mistake: Killing a thread on cancellation. → Why: It may own a file handle or partial write. → Fix: Propagate a stop token and unwind safely.
- Mistake: Enqueuing batches without a bound. → Why: Missing backpressure can exhaust memory. → Fix: Bound concurrency, queue capacity, and retries.
Follow-up questions and answers
Follow-up 1: Why not use futures directly?
Sender/receiver makes scheduling, cancellation, and three completion channels composable and gives connection-time lifetime control. Futures usually need extra conventions for stop and error propagation.
Follow-up 2: Can the sender be destroyed after start?
The temporary sender description can go away, but the operation state, receiver, and captured resources must remain valid until completion. A task object or scope should own them.
Follow-up 3: Does set_stopped roll back every side effect?
No. It reports stopped completion; submitted I/O may not roll back. Temporary files, idempotent commits, or compensation are needed for consistency.
Follow-up 4: How do you prove writes are not duplicated?
Assign a stable batch ID, check committed IDs before writing, and let retries write only missing IDs. Inject failures, restarts, and stop races, then compare the commit log with the final file.