graphitemaster / fibers

The fiber sourcebook
22 stars 4 forks source link

Coroutines and Fibers aren't the same. #10

Open johnrs opened 1 year ago

johnrs commented 1 year ago

Coroutines are one way to implement concurrency, but not the only way. Same for Async. Fibers are much more powerful than either of these models. They can implement both of these models, and lots of others.

Most of my experience with fibers comes from Go's goroutines (built into the language). Rust's Tokio library provides similar features, from what I read. I have also seen what look like true fiber implementations in a few other languages.

Here are just a few of the problems I found in your article. I'll be using Go's fibers (goroutines) as my reference. Some of the others I mentioned are similar. One-trick ponies like Coroutines and Async are not.

Scheduling: Go's fibers are not cooperatively scheduled - unless you write additional code to make it so. When blocked they are automatically rescheduled. Then can also be preempted at function calls by the Go runtime so they don't hog a OS thread. Finally, mainly to make garbage collection more efficient (2 sync points), they can also be preempted, regardless. Note: This requires a runtime. Without one, you don't have fibers. Also, fibers generally don't have a "join" command - although the programmer can achieve it without too much trouble.

Blocking: Go fibers are often blocked. This is good. No point in spin-locking when another fiber could be running. The runtime takes care of the blocking system calls, including i/o and sleeps. for example. No extra code is needed.

Runtime: The Go runtime takes care not only of blocking and scheduling, context switching, automatic stack sizing, multi-CPU/core (SMP), load balancing between multiple OS threads, and OS Signals. The fiber context switch is approximately 50 times faster than an OS thread switch - because there is less to do. A fiber's stack size starts small, 2KB, rather than the Linux OS thread default of about 10MB. Should a fiber need more, or less, space the runtime will adjust it as needed.

So fibers are more efficient (less switching overhead and less wasted stack space). This is why you can have a system with 1M fibers instead of of only 1K threads. This is nice for situations like using a process (fiber or thread) per user.

Fibers maintain state - both stack and local variables. No "volatile" needed. This is part of the context which gets switched by the runtime.

The runtime, as Go's does, generally provides various synchronization techniques (locks, semaphores, channels, etc) so that you don't have to use the OS methods. The runtime's are generally more efficient and are portable across platforms.

Simple blocking code is easier to write, read, and reason about. As long as you avoid contortions like async, that is. You have to realize that blocking is good - not bad. CPU time is NOT being wasted while you are blocked. The runtime takes care of this.

Troubleshooting SMP code is easier with a set of tools designed to work in this environment. Tool quality varies quite a bit depending on the language. While 3rd party can be good, built-in tools are generally the first choice.

Real Time: Everyone has a different threshold. For human interface, quite often 1ms is good enough. That can be achieved with Fibers in most cases. But I wouldn't count on 1us. When comparing to OS threads, please remember that fibers context switch faster, and some runtimes allow setting a CPU affinity.

Summary: I think that you need to separate the discussion of fibers from async and coroutines. Fibers are a much more all-encompassing concurrency solution than either async or coroutines. They are also harder to implement in the language or library. But they result in a better programmer experience.