July 14, 2020·4 min read·engineeringcppexecutorsconcurrencystandards

An executor preference cannot establish a required guarantee

P2183R0 reviews how executor properties compose. Its examples distinguish a requested preference from a guarantee and expose the limits of generic blocking adaptation.

An execution property can describe something an algorithm needs for correctness or something it merely prefers. Treating those requests alike can produce code that compiles while lacking the behavior on which its caller relies. The July 14 review P2183R0 examines that distinction in the proposed executor design. The WG21 document index records the review in the July mailing.

The properties mechanism aims to configure execution characteristics without multiplying execution functions for every combination. That is an attractive way to keep an interface manageable. Its correctness depends on whether each operation communicates what the resulting executor actually guarantees.

I value abstractions that preserve an explicit contract at the call site. In concurrent code, a preference that looks like a guarantee can conceal a change to the lifetime and synchronization argument. The review's examples make that difference concrete.

A preference may leave behavior unchanged

The paper contrasts requiring a property with preferring it. An executor that cannot meet a required property does not become suitable merely because the caller names that property. A preference can instead leave the executor unchanged when the requested adaptation is unsupported.

That fallback is useful when the algorithm works correctly either way. The caller can express a performance or configuration preference without excluding an otherwise acceptable executor. Its correctness argument must cover the fallback.

The same operation is inappropriate when the algorithm relies on the property to make its memory accesses valid. If execution may continue after submission returns, references to local state need a lifetime argument that covers that continuation. Preferring a blocking property does not supply that argument.

This is a general interface distinction with a specific consequence here. A request says what the caller wants. A guarantee says what the returned object will do. An algorithm can use the request as a hint, but it needs the guarantee before relying on the behavior.

Query results have a scope

A query can give the caller information about the executor's behavior. The review describes a broad default for blocking behavior that remains valid even when an executor lacks a more specific query implementation. Such a result is informative precisely because it avoids promising more than the implementation establishes.

An algorithm can then select a path consistent with that information. If it knows execution blocks as required, it can reason about completion before proceeding. If the answer remains broad, the algorithm needs a path that does not assume the narrower behavior.

The distinction affects examples involving shared state. Two submitted operations may access the same object under different execution histories. The source order of submissions does not, by itself, give the application every synchronization relationship it might want.

A valid fallback therefore needs its own account of concurrent access and completion. Adding an atomic object can address an access issue without making the order of operations deterministic. The algorithm still has to decide which outcomes its contract permits.

Generic blocking adaptation needs context knowledge

The review also challenges an adapter that promises to turn arbitrary executors into blocking executors. Its objection is that a wrapper lacks enough information to choose synchronization that works for every execution context. A mechanism appropriate for an ordinary thread need not work for a fiber or a GPU context.

This exposes a limit to interface generality. The wrapper can have a uniform function signature while lacking a uniform implementation of the promised behavior. Satisfying a syntactic requirement does not establish that a portable synchronization strategy exists.

The execution context knows details that an outside wrapper may not know. Allowing it to provide an adaptation can therefore support a stronger guarantee than assuming an arbitrary wrapper can manufacture one. The relevant capability must come from somewhere in the implementation.

That is also why capability reporting matters before the operation begins. If the adapter discovers only after submission that it cannot wait correctly, the interface has admitted an operation it cannot fulfill. A useful property system needs to prevent that unsupported promise.

The property set describes a contract

A polymorphic executor wrapper adds another boundary. Its advertised property set determines what algorithms can ask of the object through that wrapper. Information available from a concrete executor may not remain available after that boundary.

The design therefore needs to distinguish required capabilities from preferences that can fall back. An algorithm accepting a wrapper should be able to understand its contract from the wrapper's public type and documented behavior. It should not need to recover the erased concrete implementation to justify ordinary use.

I find that the most useful criterion for evaluating the proposal. A property mechanism reduces interface complexity only when the resulting guarantees remain readable. P2183R0 shows where requiring, preferring, and querying behavior need distinct meanings so that an execution request carries enough information for the algorithm's lifetime and synchronization decisions.