Skip to content

Fibers and scheduler groups

Fibers are managed executions multiplexed over native carrier threads. A fiber can suspend while waiting and let another fiber run on that carrier. Native threads created by egcl-thread:make-thread remain dedicated OS threads.

Availability

The EGCL-FIBER Lisp package is loaded by the standard bootstrap. Stackful fibers are supported on Unix x86-64 and AArch64 (including Android), Linux ppc64le and s390x, and Windows x86-64. Unix backends save native register and stack state directly, so they also work without libc ucontext. Native Windows validation is separate from testing under Wine. See Platform support.

The lower-level Rust interface is documented in Runtime fiber API. Its consuming join and completion operations have different contracts from the Lisp wrappers.

Lisp lifecycle

Functions

(egcl-fiber:make-fiber function
  &key name arguments stack-size initial-bindings) -> fiber
(egcl-fiber:start-fibers fibers &key carrier-count idle-hook) -> group
(egcl-fiber:run-fibers fibers &key carrier-count idle-hook) -> results
(egcl-fiber:submit-fiber group fiber) -> unspecified
(egcl-fiber:finish-fibers group) -> results
(egcl-fiber:fiber-group-done-p group) -> boolean
(egcl-fiber:scheduler-group-carriers group) -> list-of-threads

The lifecycle separates creation from scheduling. make-fiber creates an unscheduled fiber; its first submission assigns it to a group. start-fibers starts carriers and submits the supplied fibers. run-fibers is the convenience operation that starts and finishes a group. carrier-count defaults to the available processor count and must be at least one.

finish-fibers closes submission, waits for every submitted fiber, shuts down and joins the carriers, and returns a list of primary results in submission order. Repeated calls return the cached results. If an entry signals an unhandled error, completion still drains the other fibers before signaling fiber-error; subsequent joins and completion calls signal that saved failure. A fiber cannot finish its own group.

arguments is the entry function's argument list. initial-bindings is an alist of (special-symbol . value) pairs. Standard streams and *package* are inherited from the creator; explicit bindings override them. Other special variables use their global values until bound by the fiber. Package definitions are shared.

stack-size reserves the native stack in bytes, defaults to 524288 (512 KiB), and accepts 512 KiB through 1 GiB. This is separate from the runtime's managed Lisp frame stack. Deep interpreted code can need a larger native reservation.

An idle-hook receives the group on an idle carrier. With several carriers, it can run concurrently; protect shared state as usual. Hook errors are saved and reported by finish-fibers after work drains. Hooks should return promptly.

(egcl-fiber:run-fibers
  (list (egcl-fiber:make-fiber
          (lambda () (egcl-fiber:fiber-yield) (values 41 :extra)))
        (egcl-fiber:make-fiber (lambda () 42)))
  :carrier-count 2)
;; => (41 42)

Lisp waiting

Functions

(egcl-fiber:fiber-yield)
(egcl-fiber:fiber-sleep seconds)
(egcl-fiber:fiber-park predicate &key timeout) -> predicate-won-p
(egcl-fiber:fiber-join fiber &key timeout) -> entry-values

These operations suspend an unpinned fiber without occupying its carrier. Sleep durations and timeouts are expressed in seconds. A yield is a scheduling opportunity, not a synchronization protocol. Shared state still requires a mutex, condition variable, or another explicit protocol.

Subprocess pipe operations and process waits park unpinned fibers. Ordinary file, socket-stream, and terminal I/O still use blocking OS calls and can occupy the carrier; the runtime readiness service is not yet connected to all stream paths. Foreign calls can also block a carrier. The pinned-blocking policy below applies to operations that participate in the fiber-aware blocking protocol.

fiber-join preserves all entry values and can be called repeatedly. A timeout returns two values, nil nil; use the state/result accessors when the entry's own values could be ambiguous. fiber-park reevaluates its predicate between cooperative waits and returns true when it succeeds, or false at the timeout.

Socket I/O

Established TCP streams keep the usual synchronous Lisp interface: read-byte, read-char, writes, and output flushing park an unpinned fiber when the socket would block. Another fiber can run on that carrier until the socket is ready. egcl::%socket-wait-for-input also parks; a zero timeout only checks readiness. Receive timeouts still apply, and EOF retains the normal stream behavior.

Linux/Android use the shared epoll service, BSD/macOS use kqueue, and Windows uses one shared Winsock polling thread. Waiting on a socket does not create a thread per fiber. Windows polls active socket sets in slices of up to 10 ms. Pinned waits follow *pinned-blocking-action* and may occupy the carrier.

Connection setup is not yet cooperative: hostname lookup, connect, and accept can block a carrier. Regular file and terminal I/O are also outside this socket support. Stream operations retain exclusive ownership while waiting, so reads and writes on the same stream are serialized; closing that stream from another fiber does not interrupt an in-progress read. An explicit readiness wait releases stream ownership and can be woken by close.

Lisp pinning

Functions, macro, and variable

(egcl-fiber:fiber-pin &optional fiber)
(egcl-fiber:fiber-unpin &optional fiber)
(egcl-fiber:fiber-can-yield-p &optional fiber) -> boolean
(egcl-fiber:with-fiber-pinned ((&optional fiber)) body*)
egcl-fiber:*pinned-blocking-action*

Pinning is a balanced counter that prevents carrier migration. The macro balances the counter across ordinary and nonlocal exits. Explicit yield while pinned signals program-error. The blocking policy is :warn by default, :error to reject blocking, or nil for silent native blocking. Bind egcl-fiber:*pinned-blocking-action* dynamically to configure the current execution. Rust callers retain the runtime policy/environment interface.

Lisp observations

Functions

Function Result
(egcl-fiber:current-fiber) Current fiber, or nil on a plain native thread
(egcl-fiber:list-all-fibers) Snapshot of non-reclaimed fibers
(egcl-fiber:fiber-state fiber) Lisp lifecycle state keyword
(egcl-fiber:fiber-name fiber) Name string or nil
(egcl-fiber:fiber-result fiber) Result values as a list after completion
(egcl-fiber:fiber-error-p fiber) Whether an unhandled condition ended execution
(egcl-fiber:fiber-alive-p fiber) Whether the fiber has not completed
(egcl-fiber:fiber-carrier-thread fiber) Current or last carrier, or nil
(egcl-fiber:print-fiber-backtrace fiber &key stream count) Print the saved stack

Observations are snapshots. States are :created, :runnable, :running, :suspended, and :dead. A carrier can change after a yield. Joining transfers the result into the Lisp object and removes the runtime registration; list-all-fibers lists registrations that have not yet been reclaimed this way. Created fibers must be submitted to run and become joinable.

print-fiber-backtrace defaults to *standard-output* and 20 frames. It snapshots an unmounted continuation; inspecting a running fiber signals fiber-still-running. Created fibers show their entry; completed fibers have no active call frames. fiber-error-fiber and fiber-error-cause expose a failed entry and its saved condition.

Runtime handles are not resumable through saved images. Using a fiber or group from a previous process signals an error rather than reusing a stale identity.

Design sources: Lisp API inventory and concurrency specification.