Prompt and applicability
A service must compose reading, transformation, and aggregation into an asynchronous pipeline. The caller may cancel on timeout, disconnect, or resource pressure; each stage can fail while other work is already running. Use the C++26 std::execution sender/receiver model to explain scheduling, completion signals, lifetime, and fallback paths.
This tests the boundary of a concurrency abstraction, not memorization of one library's syntax. The standard execution-control library separates a sender's work graph from receiver completion handling and uses an operation state for connected asynchronous state. A strong answer makes cancellation, error semantics, and cleanup explicit contracts.
What the interviewer is testing
- Distinguishing a lazy sender, a connected operation state, and actual execution at
start. - Representing execution resources with schedulers instead of creating ad hoc threads in business code.
- Handling
set_value,set_error, andset_stoppedseparately rather than disguising cancellation as an exception. - Propagating stop requests through every stage and preventing new side effects after stop.
- Explaining backpressure, limits, exception safety, and aggregation for parallel work.
- Providing capability detection, a compatibility layer, and consistent tests when the standard library is unavailable.
Clarifications to ask first
- Is reading from a local file, network request, or database cursor? Are stages repeatable or externally side-effecting?
- Does cancellation mean stop promptly, stop after an uninterruptible call, or roll back committed results?
- What are the parallelism, memory, per-item timeout, and overall deadline limits?
- Must aggregation preserve input order, stable floating-point results, or visible partial results?
- Does the target compiler and library implement C++26 execution, or only an experimental implementation?
A 30-second answer
I would define a completion contract with value, error, and stopped channels. Cancellation prevents work that has not started and lets interruptible stages respond quickly. Each sender stays lazy; connecting creates an operation state, and start begins execution. Explicit schedulers own execution resources, while parallelism and queue limits protect memory. The aggregator defines ordering and partial-result rules. Capability detection selects the standard implementation, a compatibility library, or a synchronous scalar path, with shared tests for cancellation, errors, and results.
Step-by-step deep dive
1. Draw the lazy work graph
Connect a read sender to a transform sender and then to an aggregate sender. then passes produced values to the next node, let_value can create another asynchronous operation from a result, and when_all represents parallel branches. Composition builds a graph; it should not perform I/O during construction.
2. Define connection and lifetime
connect between a sender and receiver creates an operation state; execution is permitted only after start. The operation state's address must remain valid until the asynchronous operation completes, so it cannot live in a stack frame that is about to return. A request context or asynchronous scope should own it and release resources on value, error, and stopped paths.
3. Give resources to schedulers
A scheduler is a lightweight handle to an execution resource. Put reading on an I/O resource and CPU transformation on a bounded parallel resource; use on, starts_on, or continues_on to express stage boundaries. Do not create a thread per element. Bound parallelism, queue length, and batch size to control memory and context switching.
4. Propagate stopped, error, and value
Value completion enters the next stage, errors enter unified error handling, and stopped enters cancellation handling. A receiver environment's stop token is a cancellation observation point. Blocking system calls need an interruptible interface or bounded timeout; otherwise they can respond only after returning. Cancellation is not rollback: once an external write happened, use an idempotency key, compensation, or an explicit irreversible boundary.
5. A minimal composition sketch
The code shows the graph shape; the actual read and thread-pool senders are project-provided.
using namespace std::execution;
auto pipeline = read_sender()
| let_value([](Batch batch) {
return bulk_transform(batch, get_parallel_scheduler());
})
| then([](Transformed value) { return summarize(value); })
| upon_error([](std::exception_ptr error) { record_failure(error); })
| upon_stopped([] { record_cancellation(); });
auto state = connect(std::move(pipeline), receiver);
start(state);The receiver must be owned by a live request scope that provides a stop token in its environment. Production code should also record stage, batch, deadline, and cancellation reason instead of exposing only a generic failure.
6. Parallel aggregation and side-effect boundaries
Keep per-task local state during parallel transformation and merge in a defined order at aggregation. If unordered merging is allowed, state the differences caused by non-associative floating-point operations; if stable output is required, preserve an index or partition sequence. Check the stop token before an external write and record an idempotency key after commit. set_stopped does not mean the commit was undone.
7. Fallback, testing, and observability
Build a matrix from feature-test macros, compiler versions, and library capabilities. If standard execution is unavailable, a compatibility implementation can preserve the internal sender contract; otherwise use a bounded thread pool or synchronous path while keeping value, error, and stopped semantics consistent. Test empty input, partial batches, repeated cancellation, error-versus-stop races, resource exhaustion, early operation-state destruction, and repeated starts. Benchmark throughput, tail latency, queue length, cancellation response time, and unfinished tasks.
High-quality sample answer
I would model the pipeline as a lazy sender graph: read, parallel transform, and aggregate each expose completion signatures, then connect creates an operation state and start executes it. I/O and CPU use different schedulers, with parallelism, queue, and batch limits. The receiver handles value, error, and stopped separately; every interruptible point checks the stop token. External writes use idempotency and compensation boundaries, so cancellation never promises rollback.
The request scope owns the operation state through completion, and both error and stopped paths share cleanup. The toolchain detects C++26 execution and chooses the standard, a compatibility implementation, or synchronous fallback. All paths share behavioral tests for empty batches, races, cancellation response, resource exhaustion, and early destruction. In production I would watch tail latency, queue depth, cancellation response, and leaks to verify that parallelism improves the target metric.
Common mistakes
- Treating sender construction as starting asynchronous work.
- Letting an operation state die when the function returns.
- Using only an exception channel and treating cancellation as an ordinary error.
- Creating a thread per element without queue, parallelism, or memory limits.
- Claiming an external side effect was rolled back when a stop signal arrived.
- Leaving ordering, floating-point tolerance, or partial-result rules undefined for parallel aggregation.
- Implementing only one standard-library path without capability detection or fallback.
Follow-ups and responses
When does a sender actually run?
Composition describes a graph. Connecting creates an operation state, and start starts the asynchronous operation. Tests should cover construction, connection, and start separately.
Can a stop request forcibly terminate a system call?
Not in general. The call needs an interruptible interface, a timeout, or chunked checks. Otherwise it responds after returning, and the worst response time must be measured.
What if error and stop happen together?
Define priority and a one-completion rule so the receiver sees exactly one terminal signal. Preserve both the original error and stop reason for diagnosis.
What happens when one branch of when_all fails?
Specify whether other branches continue, receive a stop request, or finish cleanup. Shared resources need scoped ownership and cancellation propagation; the aggregator must not read an invalid branch result.
How do you make aggregation reproducible?
Keep partition sequence numbers and merge in a fixed order, or explicitly allow unordered results with an error bound. Parallel reduction cannot assume floating-point associativity.
What if the production library lacks C++26 execution?
Use a compiler capability matrix to select a compatibility implementation or synchronous path while preserving completion semantics and tests. Do not expose an experimental library's private types in the public interface.