Fibers and scheduling.
Silk runs concurrent work as cooperative fibers on one thread. This page covers forking and observing fibers, yielding, the Scheduler service, the LocalScheduler provider, timers, and the setup a program needs before any fiber can run. Fibers add no syntax: they are library operations from silk/concurrency/fiber, silk/concurrency/scheduler and silk/concurrency/local_scheduler, used with run and requirement rows.
Execution model#
All fibers of one scheduler run on one host thread. There is no parallelism and no preemption.
Native threads are separate from fibers.
NativeThread.spawninsilk/concurrency/thread_nativeruns aTransferableclosure on its own operating-system thread, andJoinHandle.joinwaits for its result.silk/concurrency/atomic,silk/concurrency/syncandsilk/concurrency/threadhold the matching atomics, locks and thread properties.silk/concurrency/work_queuehas queues that move owned values between threads, andsilk/concurrency/thread_poolrunsTransferablelazy tasks on a work-stealing pool of native threads. This page does not cover them.A task runs until it parks. It parks when it waits on an unfinished fiber, yields, waits on a timer, or forks a child.
When a task parks, the scheduler resumes another ready task.
LocalSchedulerkeeps ready tasks in FIFO order.A task that never parks keeps the thread until it finishes.
Parking and resumption rules, cancellation and
Effect.suspendare covered in Suspension and cancellation.
Fiber handles#
Fiber<A, E>fromsilk/concurrency/fiberis an affine handle to one task's result.Ais the success type,Ethe failure type.Observing a fiber consumes it. Using a handle after
joinorawaitis OWN0001.A fiber has at most one observer.
Dropping a fiber stops observation. It does not cancel the task.
Forking#
Fiber<A, E>.forkChild(child)starts a child task and returns itsFiber<A, E>.The type arguments must be written. Without them it is SEM0052.
childis aonce Effect<'static; A ! E ? &mut Scheduler | &mut MonotonicClock>. A child may require less, but nothing else.A child that requires another service does not fit the parameter (SEM0076).
A child that borrows from the parent is not
'static(SEM0212). The error points at the parent's borrow, such as&nfor a capturedview = &n, or at therunof the fork when the child Effect borrows a parameter. Pass owned values such asSharedhandles instead.forkChildrequires? &mut Scheduler. Forking without it in the row is SEM0071.forkChildcan fail withOutOfMemoryError(allocation or publication refused) orTaskIdExhaustedError(no task identity left).The child does not start at the fork. The scheduler resumes the parent first. The child body starts on a later activation, for example when the parent parks.
Each task receives its own
SchedulerandMonotonicClockproviders. A child may fork its own children.
Observing a fiber#
Fiber<A, E>.await(fiber)returnsOutcome<A, E>. Its fieldvalueisSuccess<A> | Failure<E> | Cancelled.Fiber<A, E>.join(fiber)returns the success value. It raises the task's failureEunchanged, orCancelledif the task was cancelled. Its failure row is! E | Cancelled.On a finished task, both return at once. On an unfinished task, the caller parks until the task completes.
Neither needs a requirement row. Type arguments are inferred from the fiber.
Structured lifetime#
When a task finishes,
LocalSchedulercancels its unfinished children. A child that was only dropped, never joined, does not outlive its parent.When the root finishes or fails, every unfinished task is cancelled before
executereturns.Awaiting a cancelled task gives
Cancelled. Joining one fails withCancelled.
Yielding#
Fiber.yieldNow()parks the current task and marks it ready again at once.Under FIFO order, every task that was already ready runs before the yielding task continues.
It needs no requirement row and allocates nothing.
With no other ready task, it returns immediately.
The Scheduler service#
Schedulerinsilk/concurrency/scheduleris a service with one operation,prepare. It prepares a child task without publishing it.Fiber.forkChildcallsScheduler.prepareand then publishes the child. Application code usesforkChild, notprepare.A custom scheduler implements
Schedulerand drives the published tasks itself.TaskIdvalues are unique within one scheduler run and are not reused.
LocalScheduler#
LocalSchedulerinsilk/concurrency/local_scheduleris the standard deterministic single-thread provider.LocalScheduler.make()creates one.LocalScheduler.execute(&mut scheduler, program)runsprogramas the root task (task zero) and returns its result.programhas the same type as a forked child:once Effect<'static; A ! E ? &mut Scheduler | &mut MonotonicClock>.executereturnsA ! E | OutOfMemoryError | StalledError ? &mut MonotonicClock. TheMonotonicClockrequirement is the parent clock, used for timer waits.Each call creates fresh task storage, a FIFO ready queue and a timer source. The call returns only after the root terminates.
A task may call
executeagain with a newLocalScheduler. The inner run uses that task's clock as its parent clock.
StalledError#
StalledErrorfromsilk/concurrency/local_scheduleris raised when the root is unfinished, no task is ready, and no timer is pending.This happens when a task parks and the
Wakethat would resume it is lost.executecancels and releases the whole task tree before it raisesStalledError.
Sleeping and timers#
Effect.sleep(duration)insilk/core/effectwaitsdurationnanoseconds. It requires? &mut MonotonicClock. Duration literals such as2msfit.MonotonicClock.waitFor(duration)andMonotonicClock.waitUntil(deadline)are the underlying operations.MonotonicClock.now()reads the clock.Inside a
LocalSchedulertask, a wait parks only that task. Other tasks keep running.Waiting tasks resume in deadline order, then in registration order.
When no task is ready, the driver waits on the parent clock until the earliest deadline.
Clock reads inside a task return one cached mark per driver turn.
The parent clock decides what a wait means.
OsMonotonicClock.make()fromsilk/system/monotonic_clock_nativeblocks the host thread. A provider whosewaitUntilonly records the deadline gives virtual time.Outside any scheduler,
Effect.sleepwithOsMonotonicClockblocks the whole program.
Runtime setup in main#
Process startup supplies no scheduler and no clock. See Services and providers for what
mainreceives.Code that may park cannot run directly under
main. Amainthat reachesFiber.yieldNowor another parking operation outside a scheduler passessilk checkbut fails to build with SEM0225 atmain: "Entry pointmainmay park, but the selected runtime starts it without a scheduler, so it must not park".To use fibers,
maincreates aLocalSchedulerand a parentMonotonicClockprovider, then runs the root throughLocalScheduler.execute.mainmust also handle or declare the failures ofexecute: the root'sE,OutOfMemoryErrorandStalledError.
Examples#
Fork two children, yield inside them, and join both from an effect main:
import silk.mem.allocator { OutOfMemoryError }
import silk.core.effect { Effect }
import silk.concurrency.fiber { Fiber, Cancelled }
import silk.concurrency.local_scheduler { LocalScheduler, StalledError }
import silk.system.monotonic_clock { MonotonicClock }
import silk.system.monotonic_clock_native { OsMonotonicClock }
import silk.concurrency.scheduler { Scheduler, TaskIdExhaustedError }
effect fn square(n: i32) -> i32 {
run Fiber.yieldNow()
return n * n
}
effect fn root() -> i32
! OutOfMemoryError | TaskIdExhaustedError | Cancelled
? &mut Scheduler | &mut MonotonicClock {
let a = run Fiber<i32, never>.forkChild(square(3))
let b = run Fiber<i32, never>.forkChild(square(4))
let x = run Fiber.join(move a)
let y = run Fiber.join(move b)
return x + y
}
pub effect fn main() -> i32
! OutOfMemoryError | TaskIdExhaustedError | Cancelled | StalledError {
let mut scheduler = LocalScheduler.make()
let mut clock = OsMonotonicClock.make()
let total = run LocalScheduler.execute(&mut scheduler, root())
|> Effect.provideMut<MonotonicClock>(&mut clock)
return total - 25
}Timers, a typed child failure, and await with Outcome:
import silk.core.effect { Effect }
import silk.concurrency.fiber { Fiber, Cancelled, Outcome, Success, Failure }
import silk.concurrency.local_scheduler { LocalScheduler, StalledError }
import silk.system.monotonic_clock { MonotonicClock }
import silk.system.monotonic_clock_native { OsMonotonicClock }
import silk.mem.allocator { OutOfMemoryError }
import silk.concurrency.scheduler { Scheduler, TaskIdExhaustedError }
struct Refused {
code: i32,
}
effect fn slow() -> i32
? &mut Scheduler | &mut MonotonicClock {
run Effect.sleep(2ms)
return 30
}
effect fn quick() -> i32 {
run Fiber<i32, never>.yieldNow()
return 10
}
effect fn refuse() -> i32
! Refused {
fail Refused { code: 2 }
}
effect fn root() -> i32
! OutOfMemoryError | TaskIdExhaustedError | Cancelled
? &mut Scheduler | &mut MonotonicClock {
let a = run Fiber<i32, never>.forkChild(slow())
let b = run Fiber<i32, never>.forkChild(quick())
let c = run Fiber<i32, Refused>.forkChild(refuse())
let first = run Fiber<i32, never>.join(move a)
let second = run Fiber<i32, never>.join(move b)
let total = first + second
let observed = run Fiber<i32, Refused>.await(move c)
let Outcome<i32, Refused> { value } = move observed
let extra = match move value {
Success<i32> { value: v } => v
Failure<Refused> { error } => error.code
Cancelled {} => 100
}
return total + extra
}
effect fn program() -> i32
! OutOfMemoryError | TaskIdExhaustedError | Cancelled | StalledError {
let mut scheduler = LocalScheduler.make()
let mut clock = OsMonotonicClock.make()
return run LocalScheduler.execute(&mut scheduler, root())
|> Effect.provideMut<MonotonicClock>(&mut clock)
}
effect fn recover(
error: OutOfMemoryError | TaskIdExhaustedError | Cancelled | StalledError
) -> i32 {
drop error
return 99
}
pub fn main() -> i32 {
let result = run Effect.catchAll(program(), recover)
return result - 42
}Fork order, dropped fibers, and cancellation when the root finishes. The child runs only after the parent yields. The sleeping child is cancelled when the root returns, so it never writes:
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.core.effect { Effect }
import silk.concurrency.fiber { Fiber, Cancelled }
import silk.concurrency.local_scheduler { LocalScheduler, StalledError }
import silk.system.monotonic_clock { MonotonicClock }
import silk.system.monotonic_clock_native { OsMonotonicClock }
import silk.concurrency.scheduler { Scheduler, TaskIdExhaustedError }
import silk.mem.shared { Shared }
struct Log {
value: i32,
}
fn bump(log: &mut Log) -> () {
log.value = log.value * 10 + 1
return ()
}
fn mark(log: &mut Log) -> () {
log.value = log.value * 10 + 2
return ()
}
fn read(log: &Log) -> i32 {
return log.value
}
effect fn child(log: Shared<Log>) -> () {
Shared<Log>.withMut<()>(&log, bump)
return ()
}
effect fn late(log: Shared<Log>) -> ()
? &mut Scheduler | &mut MonotonicClock {
run Effect.sleep(1ms)
Shared<Log>.withMut<()>(&log, bump)
return ()
}
effect fn root(log: Shared<Log>) -> i32
! OutOfMemoryError | TaskIdExhaustedError
? &mut Scheduler | &mut MonotonicClock {
let f = run Fiber<(), never>.forkChild(child(Shared<Log>.clone(&log)))
Shared<Log>.withMut<()>(&log, mark)
drop f
run Fiber<(), never>.yieldNow()
Shared<Log>.withMut<()>(&log, mark)
let g = run Fiber<(), never>.forkChild(late(Shared<Log>.clone(&log)))
drop g
return Shared<Log>.with<i32>(&log, read)
}
effect fn program() -> i32
! OutOfMemoryError | TaskIdExhaustedError | StalledError {
let mut allocator = Allocator.systemAllocatorProvider()
let log = run Shared<Log>.make(Log { value: 0 }) |> Effect.provideMut<Allocator>(&mut allocator)
let mut scheduler = LocalScheduler.make()
let mut clock = OsMonotonicClock.make()
let seen = run LocalScheduler.execute(&mut scheduler, root(Shared<Log>.clone(&log)))
|> Effect.provideMut<MonotonicClock>(&mut clock)
let after = Shared<Log>.with<i32>(&log, read)
if seen != 212 {
return 1
}
if after != 212 {
return 2
}
return 0
}
effect fn recover(error: OutOfMemoryError | TaskIdExhaustedError | StalledError) -> i32 {
drop error
return 99
}
pub fn main() -> i32 {
return run Effect.catchAll(program(), recover)
}Diagnostics#
| Code | Meaning |
|---|---|
| OWN0001 | Fiber used after join or await consumed it |
| SEM0052 | forkChild called without explicit type arguments |
| SEM0071 | forkChild run without ? &mut Scheduler in the row |
| SEM0076 | Child Effect does not fit forkChild: it requires another service |
| SEM0212 | Child Effect borrows from the parent, so it is not 'static |
| SEM0225 | Parking code reachable from main without a scheduler (reported at main, by silk build only) |
See also#
Suspension and cancellation · Effects and run · Services and providers · Typed failures · Allocation and shared ownership · Ownership and moves · Program entry and termination · silk/concurrency/fiber · silk/concurrency/scheduler · silk/concurrency/local_scheduler · silk/system/monotonic_clock · silk/system/monotonic_clock_native · silk/concurrency/execution
Open questions#
Cancellation of a finished task's unfinished children is confirmed for
LocalScheduler. Whether everySchedulerprovider must do this is not stated in theSchedulercontract.SEM0225 for parking code under
mainis reported only bysilk build, notsilk check. It points atmain, not at the parking call.