Browse the docs

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.

  • make packages a lazy Effect that outlives its caller, for a later owner to run.

  • drive runs the first activation, and each later activation after a wake.

  • park in the body stops it until an external event calls wake.

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:

  1. Initial after make. notifyInitial can mark it ready before the first drive.

  2. drive runs the body. It calls onComplete with the result, or onSuspend with the dormant Execution if the body parked.

  3. In park, the body gives its one Wake to a registration callback, and stops.

  4. wake makes 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#

  • Execution is 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 onSuspend gets the Execution back from drive.

Examples#

Make and drive one Execution#

silk
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#

silk
pub struct Wake

The 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#

silk
pub struct TransferableWake

A 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#

silk
impl Transferable for TransferableWake

TransferableExecution#

silk
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>#

silk
impl<A: Transferable> Transferable for TransferableExecution<A>

Implementation TransferableExecution<A>#

silk
impl<A> TransferableExecution<A>

Associated function adopt#

silk
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#

silk
pub fn local(self: Self) -> Intrinsic.Execution<A>

Gives back the handle so the current thread can drive or drop it.

Method notifyInitial#

silk
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#

silk
pub struct Execution

The 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:

  1. Initial after make. notifyInitial can mark it ready before the first drive.

  2. drive runs the body. It calls onComplete with the result, or onSuspend with the dormant Execution if the body parked.

  3. In park, the body gives its one Wake to a registration callback, and stops.

  4. wake makes 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#

silk
impl Execution

Associated function make#

silk
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 Allocator

Allocates 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#

silk
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 Allocator

Allocates 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#

silk
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#

silk
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#

silk
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#

silk
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#

silk
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#

silk
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.

View the source of this page