Thanks! On the execution-context SPI, the main issue was that a custom multi-threaded context needs several pieces that are currently :nodoc:: context registration, thread-pool checkout/checkin, scheduler enumeration for syscall monitoring, and the event-loop lifecycle. The event-loop API was the only compatibility issue I hit directly; fork_join has a small compile-time branch for the change from the Fiber::List form in 1.21 to the block-based form in 1.22-dev.
A reusable base for multi-threaded execution contexts might help here. It could take care of registration, thread attachment, syscall-monitor participation, and event-loop setup, while leaving queueing, stealing, parking, and wakeup policy to the implementation. Short of that, documenting and stabilizing the small set of hooks required by a custom scheduler would already make experimentation easier.
About the context itself, I’m wondering what specific use cases benefit from such a design. It’s clear that this context is meant for algorithms that produce additional work items recursively. The critical point is that these work items are represented as individual fibers.
A different approach would represent work items as plain job descriptions, and a couple of fixed fibers for executing them (which may include a similar, depths-first scheduling logic). That would be my first choice, because it avoids the overhead of potentially very many fiber stack allocations.
Do you have any insights on the pros and cons of using fibers as work items?
The use case I have in mind is nested Crystal workloads where fibers discover and spawn more work as they run. A recursive filesystem processor is one example: walking directories produces more directory and file work, while individual branches may read, parse, hash, or send results through channels. The tree is irregular, so keeping new work local and allowing idle workers to steal from busy ones helps it spread naturally.
Java’s fork/join framework and Rayon use the same scheduling idea for recursive sorting, tree traversal, search, and parallel collection processing. Those systems generally represent work as lightweight tasks, so I agree that plain job descriptions executed by fixed fibers are a better choice for tiny, CPU-only operations.
The reason fork_join uses fibers is that it is an execution context for existing Crystal code. Spawned work remains an ordinary fiber and can use channels, timers, I/O, or other suspension points without introducing a separate task API. The cost is one fiber per work item, so the work needs to be coarse enough to justify that cost, with recursive algorithms stopping at a sensible cutoff.
The current benchmark compares Parallel and fork_join using the same recursive fiber workload. It measures the scheduling-policy difference, not fibers versus lightweight jobs. That would be a useful separate comparison.