Concurrency Comparison

Coarse: Very large stacks (often several MiB, but may be dropped to around 10 KiB if the usage is predictable enough). Uses a lot of kernel resources. Significant costs in switching between them.
  • Processes: Transferring data between them is expensive. Effective for specific, independent operations. Allows low-level failures or corruption (which leads to crashes) to be isolated.
  • Preemptive threads: Shared, mutable, non-atomic data structures. Independent operations can be done in a straightforward manner, but it's very hard to ensure they really are independent, and mistakes are very hard to detect. Somewhat effective for specific, independent operations, but tends to be used more generally.
  • Cooperative threads with deep stacks: Doesn't scale to multiple CPUs. Easier to program with than preemptive threads, but possible to trigger a switch while in a function that does not expect it. Not obvious to the programmer where switching may occur.

Fine: Very small stacks (could be a few hundred bytes, or a lot more if not optimized by the language). Uses no kernel resources. Cheap to switch between them.
  • Coroutines: Can be used to implement Fine cooperative threads (and a few other things). More of a low-level tool than a high-level tool.
  • Generator-based tasks: Doesn't scale to multiple CPUs. Easy to use and switching locations are obvious, but can't cooperate with existing libraries that are not already designed to be asynchronous.
  • Event-driven state machines: Doesn't scale to multiple CPUs. Confusing spaghetti code. Doesn't require any special language features.
  • Erlang-style "processes": Scales to multiple CPUs. Interactions are easy and predictable. As a predominantly functional language, it is unpopular with those who would prefer a more imperative style.

It should be fairly obvious no single style is optimal in all situations. I believe a selection of complementary approaches is ideal, which I've explained in my previous post, Ideal Threading Model.

No comments: