core/fibre
skip to the library navigationShared: available on every enabled target.
Fibres: tasks a routine starts as a group and waits for before it goes on.
| Call | What it does |
|---|---|
fibre.group_over | Cut a backing the caller lends into equal stacks, run by any fibre.scheduler, and return the group from both. |
fibre.turns_over | The same over a fibre.turns, for tasks that must take turns. |
fibre.admit | Wait for a free stack, copy a state onto it and start a task that calls a routine with the copy. |
fibre.join | Wait until every task has ended, and fail with the first failure any had. |
fibre.yield | Let the provider's other fibres run, through turns evidence. |
A task is an ordinary routine, (inout state: T) -> none ! failure.reason, and its state a value copied onto its own stack, so one group holds tasks of different state types. The state is kept until the task ends, escaping into the group: what it refers to must outlive the group, as the routine's own parameters and the locals declared before the group do.
Two concepts say what runs the tasks:
| Concept | What it promises | Providers |
|---|---|---|
scheduler | start, finished and wait; it may run each task to its end as it is admitted | fibre.whole, the blocking host, and every provider of turns |
turns | a scheduler with yield: a task that yields lets every other unfinished one run first | stacks.round |
- A program written against
schedulermust be right when every task runs to its end. One that needs turns asksturns_over, and is refused there for a provider ofscheduleralone. - Fibres are cooperative: a fibre gives the core away only inside a call to its provider, and the fibres of one provider are one execution context.
- The first failure is kept and the rest are dropped; nothing cancels a fibre, so
joinreturns only once every task has ended. - A group must be joined before its backing goes. Nothing but the program's
defer fibre.join(group) else nonemakes sure it is. - Each stack is at least 512 bytes after its record; a smaller backing fails
mem.out_of_memory.
Items
Functions 5
Also on this page
public scheduler: type = concept (provider: type)
Start work on record and stack, or run it to its end.
start: (self: provider, escaping record: ptr mut space,
escaping stack: []mut u8, escaping work: any task)
-> none ! mem.out_of_memory
Whether the task last started on record has ended.
finished: (self: provider, record: space) -> (yes: bool)
Wait until one of the provider's fibres has finished.
wait: (self: provider) -> none
end scheduler
What runs fibres. A scheduler may run a task to its end inside start, before returning, and a program written against it must be right when every task does. start fails mem.out_of_memory when stack cannot hold the task's first frame. finished says whether the task last started on record has ended. wait returns once one of the provider's fibres has finished, or at once when none is running, which is how a group waits for a free stack. The provider runs at most one of its fibres at a time, and its fibres are one execution context (D446).
public task: type = concept (task_type: type)
Run the task to its end.
run: (self: ptr mut task_type) -> none
end task
What a provider starts: one entry, no error set, since a fibre's first frame has no caller to fail to. An application does not write one; a group makes it from a routine and a state.
A scheduler whose fibres take turns. yield gives the core to another fibre of the provider that can run, and its contract is the promise: a fibre that yields lets every other unfinished fibre of the provider run before it resumes. A provider that runs every task to its end has nothing to yield to, and so does not provide turns; a program that needs turns asks for them with turns_over, and is refused there.
public group: type = group_value
Fibres a routine starts and waits for: count equal stacks cut from a backing the caller lends, each with its record at its base, and the provider that runs them. Made by group_over or turns_over, given tasks by admit, and waited for by join, which the routine making the group defers as it would a region's release: a group must be joined before its backing goes, and nothing else makes sure it is.
public space: type = struct
saved: usize
status: u8
end space
The caller's record of one fibre: where its stack pointer was saved, and whether a task runs on it. A group keeps these at the base of the stacks it cuts, so an application never declares one. status is zero when no task runs on the record, before the first start and after a task has ended; a provider makes it nonzero while one does. Where the stack ends is in the frames the switch saves, not here (D472).
public whole: type = struct
reserved: u32
end whole
A provider that has no second stack to give: it runs every task to its end, on the caller's stack, before start returns, and ignores the stack it is given. The blocking host, system.host, is another; a device that wants no switching is a third. Make one zeroed.
public admit: (state_type: type, inout g: group,
work: (inout state: state_type) -> none ! failure.reason,
escaping into g state: state_type)
-> none ! mem.out_of_memory
Start a task: wait, through the provider, until one of the group's stacks is free; copy state onto the top of that stack, as a call passes an argument; and start a fibre whose first frame calls work with the copy. Because each task's state lives on its own stack, one group holds tasks of different state types.
work's failure is kept by the group, the first one only, and join fails with it. A task with atoms of its own maps them to failure.reason before returning. state is kept in the group until the task ends, so what it refers to must outlive the group (D445): a parameter of the routine making the group does, and so does a local declared before the group. It fails mem.out_of_memory when the state leaves the stack less than its least, 512 bytes, for its own calls.
public group_over: (runs: any scheduler, backing: []mut u8, count: usize)
-> (g: group from runs, backing) ! mem.out_of_memory
A group over a scheduler, which may run each task to its end as it is admitted. It cuts backing into count equal stacks and fails mem.out_of_memory when they would be smaller than a stack's least, 512 bytes after its record. The group borrows the provider and the backing for as long as it is used [0790].
public join: (inout g: group) -> none ! failure.reason
Wait until every task admitted to g has ended. The first failure any of them had is join's, handed on once; the rest ran to their own ends regardless, since nothing cancels a fibre (D449). A group may be joined again, and given more tasks after it is.
public turns_over: (runs: any turns, backing: []mut u8, count: usize)
-> (g: group from runs, backing) ! mem.out_of_memory
A group over a provider of turns, whose tasks take turns: a task that yields, or waits in a call to the provider, lets the others run. A provider that cannot promise that is refused here, where the group is made, rather than when a task waits for ever.
public yield: (provider: type is turns, host: provider) -> none
Give the core to the provider's other fibres, through turns evidence, for a provider the program holds as itself.
Example program
This complete program is one of the compiler's runtime tests. View source.
-- D444, prototype 5's two requests in miniature: one task waits on a -- clock and the other answers at once, from one source. Over the -- blocking host and over fibre.whole each task runs to its end as it is -- admitted, so `a` is answered first; over a round of turns, whose clock -- takes turns while it waits, `b` is answered while `a` waits. Each task -- records its answer through a pointer to the caller's parameter, which -- the group keeps through `escaping into` (D445). import core/failure import core/fibre import core/fibre/stacks import core/io import core/time import hosted/system answers: type = struct order: [2]u8 count: usize end answers asking: type = struct clock: any time.clock wait: u64 tag: u8 heard: ptr mut answers end asking -- Nothing in it knows it runs on a fibre. ask: (inout self: asking) -> none ! failure.reason = if self.wait > 0 then self.clock.sleep(time.milliseconds(self.wait)) end if self.heard.val.order[self.heard.val.count] = self.tag self.heard.val.count += 1 end ask ask_both: (runs: any fibre.scheduler, clock: any time.clock, inout heard: answers) -> none ! failure.reason = -- A task here takes about 4.4 KiB of its stack on RV64, whose -- frames keep every scalar in a home of their own, at every profile. mut stacks: [2 * 8192]u8 = zeroed mut asks := try fibre.group_over(runs, stacks, 2) defer fibre.join(asks) else none a: asking = (clock: clock, wait: 20, tag: u8('a'), heard: addr heard) try fibre.admit(asks, ask, a) b: asking = (clock: clock, wait: 0, tag: u8('b'), heard: addr heard) try fibre.admit(asks, ask, b) try fibre.join(asks) end ask_both -- A clock whose sleep returns at once, having moved its reading on. still: type = struct reading: ptr mut u64 end still still_now: (self: still) -> (at: time.instant) = at = time.instant(self.reading.val) end still_now still_sleep: (self: still, span: time.duration) -> none = self.reading.val += u64(span) end still_sleep still is time.clock (now: still_now, sleep: still_sleep) -- A clock that takes a turn for each millisecond it sleeps: its reading -- moves on only while its sleeper waits, and the round's other fibres -- run meanwhile. turning: type = struct reading: ptr mut u64 runs: any fibre.turns end turning turning_now: (self: turning) -> (at: time.instant) = at = time.instant(self.reading.val) end turning_now turning_sleep: (self: turning, span: time.duration) -> none = deadline: u64 = self.reading.val + u64(span) while self.reading.val < deadline do self.runs.yield() self.reading.val += u64(time.milliseconds(1)) end while end turning_sleep turning is time.clock (now: turning_now, sleep: turning_sleep) show: (provider: type is io.writer, inout out: io.buffered_writer(provider), name: []u8, heard: answers) -> none ! io.io_failed = try io.put(out, name) try io.put(out, ": ") try io.put(out, heard.order[0..<heard.count]) try io.put(out, "\n") end show -- The same `ask_both` over three providers. run: (host: system.host) -> none ! failure.reason = mut storage: [64]u8 = zeroed mut out := io.buffered_over(host, io.out(host), storage) defer io.flush(out) else none mut over_host: answers = zeroed try ask_both(any(addr host), any(addr host), over_host) try show(out, "host", over_host) runner: fibre.whole = zeroed mut reading: u64 = 0 stopped: still = (reading: addr reading) mut over_whole: answers = zeroed try ask_both(any(addr runner), any(addr stopped), over_whole) try show(out, "whole", over_whole) mut state: stacks.round_space = zeroed mut entries: [3]usize = zeroed idler: stacks.spin = zeroed round := try stacks.round_over(state, entries, any(addr idler)) taking: turning = (reading: addr reading, runs: any(addr round)) mut over_round: answers = zeroed try ask_both(any(addr round), any(addr taking), over_round) try show(out, "round", over_round) end run public main: () -> (code: i32) = code = 1 host := system.new() run(host) else (problem) _ = problem return end code = 0 end main
$ refine compile main.ldn -o fibre-same-source-any-provider && ./fibre-same-source-any-provider host: ab whole: ab round: ba