Thread-pool coroutines¶
The standard module supplies asynchronous result ownership and a worker pool for
coroutines, recursive calls, and synchronous functions launched with GO.
IMPORT std;
::increment FUNCTION(@value I32): I32 { RETURN value + 1; }
::sum COROUTINE(@depth I32): I32
{
IF (depth == 0) { RETURN 1; }
VAR independent := GO increment(@value depth);
VAR recursive I32 := sum(@depth depth - 1);
RETURN recursive + AWAIT(independent);
}
::example FUNCTION(): I32
{
VAR runtime std::thread_pool_cort_runtime:(@workers 2);
VAR result := ON(runtime) RUN sum(@depth 4);
RETURN result.get();
}
Launch and controller protocol¶
ON(runtime) RUN operation(...) selects a coroutine through
runtime.CO_RUN(@COROUTINE binding, @ARGUMENTS arguments) and returns
std::future#T, where T is the coroutine's declared return type.
Within a scheduled coroutine, GO operation(...) invokes the controller's
CO_GO hook and returns its result directly. The standard controller returns
std::future#T for both synchronous functions and coroutines. A VOID operation
returns std::future#VOID. GO evaluates and captures arguments before returning;
it does not implicitly await completion.
The hooks receive the complete callable binding before overload selection. The standard pool policy selects coroutine overloads when the binding contains a coroutine. A function-only binding is selected through ordinary function overload resolution. Explicit specialization selects the declaration before launch.
std::thread_pool_cort_controller#T connects an invocation to its pool, result
producer, and notification target. Its CO_COROUTINE_CALL hook returns an eager
std::task#U for the called coroutine's return type U. The ordinary coroutine
call protocol awaits that task. Each recursive invocation has independent
allocated storage; the caller suspends while its child is pending. One worker can
therefore execute recursive calls without blocking on its own queued work.
CO_CALL_CTOR and CO_CALL_DTOR use the same scheduled invocation protocol.
CO_RETURN stages a value, and CO_EXCEPTION stages an exception. Publication
occurs after the frame reports terminal completion, including local and argument
cleanup. A cleanup exception replaces a previously staged return value.
AWAIT(std::cort_yield()) queues another activation of the current invocation.
The pool returns to its ready queue before that activation starts.
Futures, promises, and tasks¶
| Type | Ownership and operations |
|---|---|
std::future#T |
Move-only ownership of one consumer. valid(), is_ready(), wait(), get(), and the native awaiter protocol are available. |
std::promise#T |
Move-only ownership of one producer. get_future() acquires the consumer; set_value(@value value) or set_exception(@error exception) publishes its result. |
std::promise#VOID |
set_value() publishes successful completion without a value. |
std::task#T |
Move-only ownership of an eagerly scheduled result, with the same readiness, wait, retrieval, and await operations as its future. |
std::broken_promise |
Exception delivered when an incomplete producer is destroyed or replaced through move assignment. |
A future is consumed once through get() or AWAIT. Consumption leaves it
invalid, including when the producer's exception is rethrown. Moving transfers
ownership and leaves the source invalid. A default-constructed future is invalid.
Readiness, waiting, and retrieval require a valid future. get_future() is called
once per promise, and a producer publishes at most one result. The promise and
its consumer can operate on different threads. Operations on the same future or
the same promise object require external synchronization.
get() and wait() can block the calling thread. A pool coroutine uses AWAIT
for pending results so its worker can execute other invocations. The awaiter
checks readiness and registers its continuation under the result-state mutex.
Producer completion either observes that registration or causes registration to
decline suspension, preventing a lost notification.
Destroying a future or task releases consumer ownership without waiting for the producer. A scheduled invocation remains owned by its pool until execution and cleanup finish. The pool does not expose cancellation of submitted work.
Pool and notification lifetime¶
std::thread_pool_cort_runtime(@workers count) starts count workers. The default
is four; the count must be positive. post(@operation callable) owns and queues a
callable whose zero-argument call operator completes synchronously without
throwing. drain() waits for every submitted invocation and its cleanup.
Destruction drains work, stops idle workers, and joins all worker threads.
drain() and pool destruction run outside that pool's workers. External
submission cannot overlap destruction. Borrowed arguments and receivers remain
alive until their invocations finish. Passing a temporary to a reference
parameter does not extend its lifetime. A coroutine waiting on an external promise
can keep drain() pending until that producer completes or is abandoned.
std::async_continuation contains a MUT->VOID context and a notification
procedure that completes without throwing. The controller's continuation()
returns its stable continuation.
An awaiter retains that target while registered and delivers one notification
when the awaited operation completes. The pool records a notification received
during an active drive and queues the next activation only after that drive
returns. An invocation never has two simultaneous drivers.
The pool and synchronization operations require native threading support. They
are not constant-evaluation operations. Coroutine control objects and function
captures use typed VOID pointer round trips; the library does not recover an
invocation from a contained object's address.
Callable capture operations¶
BIND_CALL function(arguments...) selects a synchronous function and returns a
composite with CALLEE and ARGUMENTS fields. CALLEE identifies the selected
specialization and retains a bound reference receiver, including its virtual dispatch
behavior. ARGUMENTS owns adapted value parameters and retains reference
parameters. Argument conversions and default expressions run during capture.
The selected function body runs when the stored call is invoked:
VAR bound := BIND_CALL increment(@value 41);
VAR result I32 := FORWARD PACK(COMPOSITE_TO_PACK(COMPOSITE_FIELD_GET(MOVE(bound), "ARGUMENTS")))
TO COMPOSITE_FIELD_GET(bound, "CALLEE");
A captured call that moves owned arguments is consumed once. Borrowed arguments
must outlive invocation. Coroutine targets and construction or destruction
destinations are rejected. The selected function's RETURN_TYPE metadata names
its return type. This metadata is available for both function and coroutine
specializations.
HAS_COROUTINE_OVERLOAD(binding) is a constant BOOL indicating whether the
callable binding contains a coroutine declaration. It does not select an overload
or prove that any overload accepts a particular argument list. IS_FUNCTUM_CALLABLE_WITH(binding, @name Type, % [Type, ...]) tests invocation
viability using explicit argument types. The argument list supports
COMPOSITE_UNPACK(CompositeType) and can be omitted for a zero-argument call.
Overload resolution applies argument adaptation, default parameters, and declaration
constraints. A missing viable overload produces FALSE; ambiguous overloads and
malformed type arguments produce compilation errors. The query does not execute
the function or evaluate default argument values.
The pool controller uses IS_COROUTINE_CALLABLE_WITH with @CONTROLLER AUTO to test coroutine viability for
the actual argument types. A viable coroutine overload takes precedence. If no
coroutine overload is viable, the controller captures a synchronous call.