I'm confused in how this article defines "fiber." At the beginning of the article, fibers are described as a lightweight userspace thread, similar to what is provided in the runtimes of Haskell and Go, and has been recently revived in Java as "virtual threads."
However, later in the article (after a long and apparently irrelevant digression about stack management), they describe Windows API functions that are required for using fibers and seem to suggest that fibers are OS-level, and not application-managed.
The Windows fiber library is just that, a library. It's not an OS component. It's provided because stack switching in Windows is a somewhat fraught [1] and not officially documented procedure which requires modifying the TIB [2].
Doing it correctly isn't hard, and has been done many times in bullet-proof libs (notably boost::context), but MS would maybe rather you not.
It is an OS component, it's provided in kernel32.dll and accesses internal OS data structures that are not publicly defined or stable between versions. You cannot safely implement fibers in Windows without the fibers API, period. Boost.Context's direct Windows context switching code hacks undocumented fields in the TIB:
https://github.com/boostorg/context/blob/6fa6d5c50d120e69b2d...
Microsoft continually adds and changes fields in the TIB with each new release of Windows. Attempting to implement fibers manually is a ticking time bomb that should never be used in production.
That bug was caused by the TLS slots pointer being uninitialized in the newly created Boost fiber, not a change in Windows API. TLS pre-dates boost::context, the field had existed since the very first commit.
I already said "MS would rather you not", obviously, otherwise they would have documented the TIB.
The fact remains that tons of production systems rely on officially-unofficial elements of NT's architecture and this is one such element that is heavily relied on by everyone who uses boost stackful coroutines. Hyrum's Law in action.
The bug was caused by Boost.Context manipulating OS data structure fields that it has no business changing.
And yes, those fields do now have to be maintained for backwards compatibility -- because libraries like Boost.Context hardcoded this into applications, unbeknownst to the users of that library.
"Fibers", "green threads", "stack switching", "cooperative multitasking" are essentially all the same thing, they all rely on being able to switch execution context by switching to a different stack within the same OS thread. As such they can be implemented either in (CPU/OS-specific) user code or in "OS APIs" (like the Windows Fiber functions).
Only downside of the technique is that it cannot be implemented in WASM (and maybe some other esoteric runtime environments), because WASM has separate data- and call-stacks and the call stack is not accessible from within the WASM virtual machine (while 'async-await' which relies on code transformation done by the compiler can be implemented in WASM just fine).
There is a 'stack-switching proposal' for WASM though, but I don't know what's the state of that:
I've always been curious why WASM has the program flow restrictions it has. Most "ASM" just lets you set whatever register.
There isn't much sandboxing value (WASM shouldn't be able to touch addresses outside the sandbox anyways). Was the reason ease of compilation/optimization for arbitrary architectures? Easier to run inside an interpreter?
The default idea for WASM feels like it should have been more like RISC-V (but with enough wiggle-room to allow easy JITing to x86/ARM).
Or are most of WASM's quirks just because it has ASM.js in its lineage?
Having a native stack switch for WASM would be nice but it's not necessary. Map all memory segments as shared into several instances of a single module -- each instance is a fiber.
Implementation is left as an exercise for the reader.
WebAssembly stores stack data - parameters, return addresses, local variables - in a "shadow stack" that does not live in the native heap your application has access to. There's no way for you to switch it.
Each instantiated module has its own complete function call stack. A call out of the module, to a bit of control code in JS, and then into a different instance of the same module, with the same memory mappings, would look (to the program itself) like a stack switch, no?
I guess it raises the philosophical question of what is a stack switch, anyway? If you end up cooperatively running multiple "green threads" within a single "OS thread" does it matter that what you actually switched was the entire execution environment?
The devil's in the implementation details though. The operating system may provide both fully-preemptive threads on which to schedule user space cooperative threads (scheduling on system call). At that point green threads (e.g. threading implemented by a virtual machine on top of the operating system) do not provide the same value. This is actually why Green threads were effectively pulled out of Java for a few decades.
Fibers are distinct in that they have no scheduling and are code being run on a thread - if the thread is preempted, it will resume on that same thread. Unlike cooperative threading, they must explicitly yield.
Coroutines have such varying implementations that you would need to define requirements to know if they count as fibers or not - for instance, whether you mandate a C-compatible stack.
There's one thing missing here, whereas fibers are cooperative, they have a user space scheduler deciding which fiber to switch to on a thread when you yield, coroutines specify who they're yielding to.
You'll notice the function's documentation is "Schedules a fiber. The function must be called on a fiber." so while it may not be a literal difference (after all they're both semi-user space), they are difference systems to use, but they're both roughly at the same level of "power".
If you're not using a scheduler with fibers then I'd argue you shouldn't be calling it a fibers system (and rather coroutines), since otherwise it's a distinction without a difference.
Windows has its own fibers facility which does some things for you but still leaves the scheduling to the application so it's still fibers. It's a little confusing in the article but they look at the Windows API as a kind of API template - what do you need to implement to have fibers. Then they describe their own implementation.
My general understanding is that a fiber is a lightweight thread, and conceptually can be implemented inside any program that wants to schedule its own fibers.
They’re distinct from CPU threads.
When I last dived into fibers I discovered windows was trying to make them a thing quite a while ago which is why we have things like [0].
Fibers being virtual threads, are recursive. In the same way you can run a VM inside a VM, you could build fibers on top of fibers (which sounds like a recipe for unpredictable performance).
On Windows, many things exist in user-space that are managed by the OS. This is in contrast to linux where the Linux kernel is its own thing with its own stable API.
Fibers are an OS-level concept that exists in user-space.
Personally if I had a choice of just one I’d pick the threads over fibers if the threads facility is really high performance like the one in Java.
Today you have quad cores even on a cheap phone and 16 on a desktop and 60+ on a server and OS and some runtimes make it pretty easy to get large speed ups on many tasks even when subtasks are closely coordinated. (One thing I like about Java is that it has low-level thread primitives that really scale as opposed to many systems have have a small set of low-level primitives that in theory let you do everything but not scale.)
Contrast that to fibers and similar things (JS and Python async) that do a great job of keeping a CPU busy when you are waiting for network activity but are limited to one CPU.
However, later in the article (after a long and apparently irrelevant digression about stack management), they describe Windows API functions that are required for using fibers and seem to suggest that fibers are OS-level, and not application-managed.
Am I missing something?
[edit: typo]