Fibers
All ordinary Crow code runs in a fiber.
A fiber is like a thread, but implemented by the Crow runtime instead of the operating system.
The term thread always refers to a native thread.
The fiber type is internal to the Crow runtime; you only deal with it indirectly.
Fibers let you:
- Do "blocking" operations, such as network requests, without using callbacks.
- Write separate code for procedures which at runtime will actually run interleaved.
- Run code in parallel to get work done in less time (by using more processor cores).
A fiber is a logical series of operations that don't necessarily
correspond to sequential machine instructions.
A fiber might delay for a while, or wait for an event like a keypress.
For example, 3.second delay does not literally freeze the thread for 3 seconds;
the thread does other work while the fiber is set aside for 3 seconds.
When a fiber is delayed for whatever reason, it is said to yield.
The fiber queue
The runtime maintains a queue of runnable fibers, called the fiber queue.
This contains immediately runnable fibers only; fibers that are blocked are handled in various ways.
(So, the only reason for the queue to remain non-empty
is if there are more runnable fibers than available threads.)
The core runtime loop for each thread is to take a fiber off the queue, run it until it yields,
do some bookkeeping, then repeat.
When the Crow runtime starts, it launches a thread for each processor,
and creates the first fiber that will run main.
Launching new fibers
The common way to launch a new fiber is to use with : parallel.
This returns a future for its result.
A future represents a value that may not be ready yet.
await is a function that yields the current fiber
(in this case, the one running main)
and adds it to a waiting list for the future.
(await does not cause the other fiber to run;
that was already started by with : parallel. It just observes the result.)
When the future is resolved (in this case, when the code inside with : parallel completes),
the awaiting fiber is added to the fibers queue, meaning it will resume as soon as a thread is available.
More parallel fibers
parallel has more useful functions for running code in parallel.
For example, it includes a parallel for loop.
This creates a fiber for each element of a collection.
Exclusion
A fiber can't share mutable state with other fibers running in parallel. This is the thread-safety problem.
So, with : parallel and similar functions take
shared lambdas as arguments.
That means they can only close over values that are safe to share with parallel fibers.
There is a way to have multiple fibers that share mutable state: give them the same exclusion.
An exclusion is just a number associated with each fiber.
The fibers queue will only dequeue a fiber if its exclusion isn't already in use,
meaning two fibers with the same exclusion will never run at the same time.
Like fiber, you won't reference the exclusion type directly.
with : later works like with : parallel but reuses the calling fiber's exclusion.
In this example there are 3 fibers: One for main,
and one for each later.
Since all 3 fibers share the same exclusion, the calls to ~=
do not overlap (which would be bad).
Both later and parallel create a new fiber,
but parallel uses a new exclusion (meaning, one never seen before)
while later shares the exclusion of its caller.
So, later takes a mut lambda
(meaning the closure can include anything),
while parallel needs a shared one
(meaning the closure can only include shared types).
Try changing later to parallel
above to see the compile error.
Share any lambda
If some library you are using expects a shared lambda, worry not;
you can make any lambda shared just by putting the
shared keyword before it.
shared wraps the lambda with code that changes its calling fiber's exclusion
to the exclusion of the fiber that created the lambda (yielding until it's available),
and changes it back after calling.
That means the above example is not really running in parallel since there is only one call to
f at a time.
However, they could happen in any order, so, it is possible that it might log "world, hello".
(This won't happen in the browser which is single-threaded.)
When writing code that returns a lambda,
you should usually make it shared to make it easier for the user.
Futures and exceptions
When a fiber throws an exception,
the future is completed with that exception and will be thrown again at await.