silk.concurrency.execution.
Lazy computations with their own storage, which an owner drives and resumes.
When to use#
This module is for schedulers and runtimes. Application code uses silk.concurrency.fiber or
silk.concurrency.thread_pool instead.
makepackages a lazyEffectthat outlives its caller, for a later owner to run.driveruns the first activation, and each later activation after a wake.parkin the body stops it until an external event callswake.
Details#
make gets one combined package from the selected Allocator. It does not start the
Effect. The body, the continuation and the fixed readiness endpoint stay private.
The life of an Execution:
Initialaftermake.notifyInitialcan mark it ready before the first drive.driveruns the body. It callsonCompletewith the result, oronSuspendwith the dormant Execution if the body parked.In
park, the body gives its oneWaketo a registration callback, and stops.wakemakes the Execution eligible and calls the readiness endpoint. The owner then drives it again.
A nested Effect.suspend transfers execution at once. Only park gives up the Execution
until a Wake.
The readiness phase and the park generation are in one control word of the package. Each transition is one compare-exchange of that word. Thus a Wake can come from another thread, and readiness is published before the endpoint runs (EXEC-009).
For an Execution that moves between threads, use makeTransferable. Its handle is a
TransferableExecution. Its body parks with parkTransferable, and any thread can signal
the TransferableWake (EXEC-010).
Errors#
make fails with OutOfMemoryError if the package cannot be allocated. Growth of the private
execution stack later is a fatal trap, not an error.
Gotchas#
Executionis affine. The readiness callback must be detached and non-parking.Dropping a dormant Execution cancels it.
A Wake that you keep also keeps the complete inert package alive, until the Wake is used or dropped.
Only
onSuspendgets the Execution back fromdrive.
Examples#
Make and drive one Execution#
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.core.effect { Effect }
import silk.concurrency.execution { Execution }
// What the owner sees after one drive.
struct Run {
done: bool,
result: i32,
}
// The readiness endpoint. A Wake calls it when a parked body can continue.
fn ready(state: &()) -> () {}
fn completed(state: &mut Run, value: i32) -> () {
state.done = true
state.result = value
}
fn suspended(state: &mut Run, execution: Intrinsic.Execution<i32>) -> () {
// A real owner keeps the dormant Execution, and drives it again after the Wake.
drop execution
}
effect fn body() -> i32 {
return 6 * 7
}
effect fn program() -> i32
! OutOfMemoryError {
let mut allocator = Allocator.systemAllocatorProvider()
// make allocates the package. The body does not start yet.
let execution = run Execution.make(body(), (), ready)
|> Effect.provideMut<Allocator>(&mut allocator)
let mut state = Run { done: false, result: 0 }
// drive runs the body. It calls exactly one of the two callbacks.
run Execution.drive(move execution, &mut state, completed, suspended)
if !state.done || state.result != 42 {
return 1
}
return 0
}
effect fn outOfMemory(error: OutOfMemoryError) -> i32 {
return 1
}
pub fn main() -> i32 {
return run Effect.catchAll(program(), outOfMemory)
}Import as Execution with import silk.concurrency.execution { Execution }.
Public declarations: 4.
Wake#
pub struct WakeThe affine readiness authority for one parked generation of one Execution.
Details#
Execution.park passes the generation's only Wake to its registration callback.
Execution.wake consumes it and makes that generation eligible at most once.
Gotchas#
Dropping a Wake publishes nothing. A retained Wake keeps the inert package allocation alive after the Execution is dropped, until the Wake is consumed or dropped.
TransferableWake#
pub struct TransferableWakeA Wake that can move to another thread and be signaled there.
Details#
Execution.parkTransferable passes the only TransferableWake of a generation to its
registration callback. Execution.wakeTransferable consumes it on any thread.
Gotchas#
Dropping it publishes nothing. A kept one keeps the inert package allocation alive after the Execution is dropped, until it is consumed or dropped.
Implementation Transferable for TransferableWake#
impl Transferable for TransferableWakeTransferableExecution#
pub struct TransferableExecution<A>The owner of an Execution that Execution.makeTransferable made. It can move to another
thread.
Details#
local gives back the Intrinsic.Execution<A> handle that Execution.drive takes.
adopt wraps a handle again, such as the dormant one that onSuspend gets. The wrapper is
Transferable when A is.
Gotchas#
Adopting a handle that Execution.make made is a fatal trap.
Implementation Transferable for TransferableExecution<A>#
impl<A: Transferable> Transferable for TransferableExecution<A>Implementation TransferableExecution<A>#
impl<A> TransferableExecution<A>Associated function adopt#
pub fn adopt(execution: Intrinsic.Execution<A>) -> TransferableExecution<A>Wraps a handle to a package made by Execution.makeTransferable.
Gotchas#
A handle made by Execution.make is a fatal state trap.
Method local#
pub fn local(self: Self) -> Intrinsic.Execution<A>Gives back the handle so the current thread can drive or drop it.
Method notifyInitial#
pub fn notifyInitial(self: &mut Self) -> ()Notifies the fixed readiness endpoint for one Initial Execution exactly once.
Details#
It changes the package to InitialReady before it invokes the endpoint. It is synchronous,
does not park, and does not run the body. A later Execution.drive starts the body.
Gotchas#
The Execution must still be Initial. Calling this operation again, or calling it after the
Execution has been driven, parked, completed, or destroyed, is a fatal state trap.
Execution#
pub struct ExecutionThe owner of the execution lifecycle operations: lazy computations with their own storage, which an owner drives and resumes.
When to use#
This type is for schedulers and runtimes. Application code uses silk.concurrency.fiber or
silk.concurrency.thread_pool instead. Execution.make packages a lazy Effect for a later owner to
run. Execution.drive runs the first activation, and each later activation after a wake.
Execution.park in the body stops it until an external event calls Execution.wake. For
an Execution that moves between threads, use Execution.makeTransferable.
Details#
The life of an Execution:
Initialaftermake.notifyInitialcan mark it ready before the first drive.driveruns the body. It callsonCompletewith the result, oronSuspendwith the dormant Execution if the body parked.In
park, the body gives its oneWaketo a registration callback, and stops.wakemakes the Execution eligible and calls the readiness endpoint. The owner then drives it again.
A nested Effect.suspend transfers execution at once. Only park gives up the Execution until
a Wake.
This struct carries no data and is never constructed by the library. Every operation is an
inherent member declared in impl Execution, so import silk.concurrency.execution { Execution } is the
one import that reaches Execution.make(...) and the rest. It is unrelated to the builtin
Intrinsic.Execution<A> type.
Implementation Execution#
impl ExecutionAssociated function make#
pub effect<'static> fn make<
A,
F: once Effect<'static; A> + Intrinsic.Detached,
O: Intrinsic.Detached,
R: fn<'static>(&O) -> () + Intrinsic.Detached + Intrinsic.NonParking
>(body: F, readyState: O, onReady: R) -> Intrinsic.Execution<A>
! OutOfMemoryError
? &mut AllocatorAllocates one combined package and transfers the lazy body and fixed endpoint into it.
Details#
The package comes from the selected Allocator. The body, the continuation and the fixed
readiness endpoint stay private.
The returned Execution is Initial. The body does not start during this call. Allocation
refusal produces no Execution and leaves every input under ordinary Effect cleanup. Later
private stack growth is a fatal trap and is not a OutOfMemoryError.
Gotchas#
The Execution is affine. The readiness callback must be detached and non-parking. Dropping a dormant Execution cancels it.
Associated function makeTransferable#
pub effect<'static> fn makeTransferable<
A: Transferable,
F: once Effect<'static; A> + Intrinsic.Detached + Transferable,
O: Intrinsic.Detached + Transferable + Concurrent,
R: fn<'static>(&O) -> () + Intrinsic.Detached + Intrinsic.NonParking + Transferable + Concurrent
>(body: F, readyState: O, onReady: R) -> TransferableExecution<A>
! OutOfMemoryError
? &mut AllocatorAllocates one package whose handle and Wakes can move to other threads.
Details#
The returned Execution is Initial, the body does not start during this call, and allocation
refusal produces no Execution. The body and
the result must be Transferable, because
the body can resume on any thread that drives it. The endpoint and its state must be
Transferable and Concurrent, because each TransferableWake runs the endpoint on the
thread that signals it. The body parks with parkTransferable.
Gotchas#
Values that the body makes itself need no bound. Silk has no thread-local or global storage, so nothing outside the body can reach them while it moves between threads.
Associated function drive#
pub effect<'env> fn drive<
'env,
A: 'env,
D: 'env,
C: once fn<'env>(D, A) -> () + Intrinsic.NonParking,
S: once fn<'env>(D, Intrinsic.Execution<A>) -> () + Intrinsic.NonParking
>(
execution: Intrinsic.Execution<A>,
branchState: D,
onComplete: C,
onSuspend: S
) -> ()Drives one Initial, InitialReady or Eligible activation, and gives branchState to one
outcome callback.
Details#
Completion calls onComplete. If the body gives up the Execution, onSuspend gets the
dormant Execution. Each activation calls exactly one callback. A nested transfer can complete
in the same drive. An external park gives up ownership until the readiness endpoint reports
that the Execution is eligible.
The body starts its own diagnostic invocation. To keep an observer across parks, install it inside the body. The outcome callbacks run in the diagnostic context of the caller.
Gotchas#
Driving a dormant, notifying, completed or destroyed Execution is a fatal state trap. Only
onSuspend gets the Execution back.
Associated function notifyInitial#
pub fn notifyInitial<A>(execution: &mut Intrinsic.Execution<A>) -> ()Notifies the fixed readiness endpoint for one Initial Execution exactly once.
Details#
This operation changes the package to InitialReady before it invokes the endpoint. It is
synchronous, does not park, and does not run the body. A later drive starts the body.
Gotchas#
The Execution must still be Initial. Calling this operation again, or calling it after the
Execution has been driven, parked, completed, or destroyed, is a fatal state trap.
Associated function park#
pub effect fn park<
'env,
G,
F: once fn<'env>(Wake) -> G + Intrinsic.NonParking
>(register: F) -> ()Relinquishes the currently Running Execution until one external readiness signal arrives.
Details#
The registration callback receives the generation's sole affine Wake. Its returned guard
is retained while dormant and dropped immediately before source continues after this call.
A Wake signaled during registration is latched until drive returns this Execution to its
owner, which then notifies. The Wake invokes the fixed readiness endpoint at most one time.
Gotchas#
A caller that keeps a cancelled Wake also keeps the complete inert Execution package alive. Consuming or dropping that Wake releases the final package authority.
Associated function parkTransferable#
pub effect fn parkTransferable<
'env,
G,
F: once fn<'env>(TransferableWake) -> G + Intrinsic.NonParking
>(register: F) -> ()Gives up the running Execution and passes a TransferableWake to register.
Details#
Use it in a body that makeTransferable made. The registration callback receives the
generation's only Wake, which can go to another thread and be signaled there with
wakeTransferable. A Wake signaled during registration is latched until drive returns
this Execution to its owner. The Wake invokes the fixed readiness endpoint at most one time.
Gotchas#
A call from a body that make made is a fatal state trap, because that endpoint cannot run
on another thread.
Associated function wakeTransferable#
pub fn wakeTransferable(wake: TransferableWake) -> ()Consumes one TransferableWake on any thread, and makes its parked generation eligible.
Details#
A Wake signaled while its generation is still registering is latched until the owner receives
the Execution back from drive. A Wake signaled while its generation is dormant makes the
Execution eligible and invokes its fixed readiness endpoint on the calling thread. Signaling
never drives the Execution. A Wake for a dropped or completed Execution, or for an earlier
generation, publishes nothing.
Associated function wake#
pub fn wake(wake: Wake) -> ()Consumes one Wake and makes its parked generation eligible.
Details#
A Wake signaled while its generation is still registering is latched until the owner receives
the Execution back from drive. A Wake signaled while its generation is dormant makes the
Execution eligible and synchronously invokes its fixed readiness endpoint. Signaling never
drives the Execution.
Gotchas#
A Wake for a dropped or completed Execution, or for an earlier generation, publishes nothing and only releases its package authority.
Category: Concurrency.
Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-gnu-no-libc, x86_64-unknown-linux-gnu, x86_64-unknown-linux-gnu-no-libc, wasm32-unknown-unknown, wasm32-wasip1, wasm32-wasip2.