Async & Concurrency Models — Cross-Language Comparison
This note compares the async + concurrency models a programmer actually picks between — OS threads vs green threads vs async/await coroutines vs actors vs CSP channels vs STM vs reactive streams vs event loop vs structured concurrency vs virtual threads vs fiber — across all 53 deep language notes in the library. It’s a more operational view than _compare_concurrency (which covers the broader threads/STM/actors taxonomy); here the lens is “what async primitive do I reach for to handle 10⁴+ concurrent I/O operations, and what does it cost?” The closing decision tree maps latency + throughput + scale target onto the right primitive.
See also
- _compare_concurrency — the broader concurrency model survey (this note’s parent)
- _compare_memory_models — how concurrency affects GC + allocation
- _compare_error-handling — structured concurrency error propagation
- _compare_type_systems — effect typing for async (Effect-TS, ZIO, OCaml 5 effects)
1. The seven families
| Family | What you write | Scheduler | Stack | Languages |
|---|---|---|---|---|
| OS threads (1:1) | std::thread, pthread_create, Thread::new | kernel | per-thread (typ 1–8 MB) | C, C++, Java (until v21), Rust std::thread, C# native, Swift legacy, MATLAB |
| Green threads / M:N | go fn(), spawn fn, Erlang spawn | runtime | small stackful (KB; growable) | Go, Erlang, Elixir, Gleam, Haskell (forkIO), Pony |
| Async/await coroutines (stackless) | async fn, await, futures | event loop | tiny (state-machine struct) | Python asyncio, Rust Tokio, JS event loop, C# Task, Kotlin coroutines, Swift, Dart, F#, Nim, Zig (proposed) |
| Actors | actor, Akka Actor, gen_server | runtime mailbox dispatcher | per-actor heap | Erlang, Elixir, Akka (Scala/Java), Pony, Orleans (.NET), Swift actor |
| CSP channels | go ch <- v, core.async chan, crossbeam::channel | scheduler + channel queue | mixed | Go, Clojure core.async, Rust crossbeam, Crystal, Ocaml domainslib |
| STM | atomically { ... }; refs / transactions | runtime + log | normal | Haskell (canonical), Clojure, Scala (Akka STM legacy), Pony |
| Reactive streams | Observable.map().filter().subscribe() | reactive scheduler | varies | RxJava, RxSwift, Combine, Reactor, Mutiny, RxJS, Akka Streams |
Cross-cutting primitives below also matter (event loop, thread pool + work-stealing, structured concurrency, virtual threads, fiber).
2. OS threads — the kernel-backed primitive
A 1:1 thread of execution managed by the kernel. Preemptive scheduling, kernel-allocated TLB + stack, syscall-cheap context switch (~1 μs) but heavy creation (~100 μs) and large stack (default 1–8 MB so 1,000 threads = several GB virtual memory).
| Language | API | Scheduler | Default stack | Notes | Linked note |
|---|---|---|---|---|---|
| C | pthread_create, Win32 CreateThread | kernel | 8 MB (Linux) | manual lifecycle; signal interactions tricky | c |
| C++ | std::thread, std::jthread (C++20) | kernel | platform default | jthread joins on destruct; std::stop_token for cooperative cancellation | cpp |
| Rust | std::thread::spawn, thread::Builder | kernel | 2 MB default | Send/Sync trait-checked at compile time; JoinHandle::join() returns Result | rust |
| Java | Thread, ExecutorService, ForkJoinPool | kernel (until J21) | 1 MB (Linux) | Thread::ofPlatform for kernel thread (J21+); Thread::ofVirtual for virtual | java |
| [[Languages/csharp|C#]] | Thread class; Task on ThreadPool | kernel | 1 MB | Task is preferred; Thread for fine control | csharp |
| Swift | Thread legacy; GCD DispatchQueue; Swift Concurrency since 5.5 | kernel | 512 KB | Thread deprecated for new code; use Task | swift |
| Ruby | Thread.new | GIL until 3.0 Ractor | 1 MB | until 3.0 Ruby threads were essentially GIL-bound; Ractor (3.0+) is true parallelism | ruby |
| Python | threading.Thread | GIL until 3.13t (PEP 703) | 8 MB | GIL-bound for CPU; 3.13t free-threaded build is the major shift | python |
| Perl | threads (ithreads) | kernel | shared by copy | discouraged; fork preferred | perl |
| Julia | Threads.@spawn | kernel pool | varies | true threads; --threads=N at start | julia |
| MATLAB | parfeval + Parallel Computing Toolbox | kernel | varies | requires PCT licence | matlab |
| Ada | task (Ada built-in) + protected | kernel or runtime | varies | tasks are first-class language entities; rendezvous semantics | ada |
| Fortran | OpenMP directives; coarrays for distributed | kernel + MPI | varies | mostly directive-driven | fortran |
| Mojo | parallelize + DPC++-style; async fn later | runtime | varies | early; targeting GPU + SIMD parallelism over thread parallelism | mojo |
When to use: CPU-bound work that needs preemption, real OS-level scheduling, integration with C/native libraries that demand a real thread. Native threads are fine up to a few hundred concurrent units; beyond that, virtual threads or async/await scale better.
3. Green threads / M:N — runtime-multiplexed
Many lightweight threads scheduled onto few OS threads by a userspace runtime. Cheap (<2 KB stack), cheap context switch (~100 ns), can host millions of them.
| Language | Primitive | Scheduler | Stack | Notes | Linked note |
|---|---|---|---|---|---|
| Go | go fn() (goroutine) | runtime M:N; netpoller + work-stealing | 2 KB growable | the gold standard; 1M goroutines per process common | go |
| Erlang | spawn(fn) (process) | BEAM scheduler | 233 bytes initial, per-process heap | per-process isolation; up to 100M+ processes; preemptive (reduction-counted) | erlang |
| Elixir | spawn, Task.async | BEAM (Erlang VM) | as Erlang | same primitives + sugar | elixir |
| Gleam | process.start | BEAM | as Erlang | typed Erlang lineage | gleam |
| Haskell | forkIO, forkOS, async | GHC RTS M:N | 1 KB initial; growable | STM + MVar + TVar for synchronization; very cheap | haskell |
| Pony | actor | runtime work-stealing | per-actor heap | actor IS the green thread | pony |
| Crystal | spawn { ... } (Fiber) | runtime | 8 MB stack but copy-on-write | spawned via spawn; await via Channel | crystal |
| Nim | threads OR spawn (threadpool) OR async | varies | varies | mixes models | nim |
| Lua | coroutines (asymmetric); luaproc lib for multi-OS-thread | userland | small | coroutines are stackful but single-threaded by default | lua |
Go’s goroutines + work-stealing + netpoller (epoll/kqueue/IOCP) are the canonical end-state. Go programs routinely run 10⁵–10⁶ goroutines. Erlang/BEAM goes further — single-node WhatsApp servers ran 2M concurrent processes pre-Meta.
4. Async/await coroutines (stackless)
A Future / Promise / Task is a state-machine struct. await suspends by saving state + yielding to the event loop. No stack — bytes per suspension.
| Language | Primitive | Runtime | Notes | Linked note |
|---|---|---|---|---|
| Python | async def, await, asyncio | asyncio event loop (CPython); also Trio, Curio, AnyIO | PEP 492 (3.5, 2015); TaskGroup PEP 654 (3.11, 2022); colored (must be async-aware) | python |
| JavaScript | async function, await, Promise | V8 microtask queue + libuv | ES2017; single-threaded event loop; Promise.all, Promise.allSettled, Promise.race | javascript |
| TypeScript | same as JS | inherits | typed Promises (Promise<T>); Effect-TS adds typed error channel | typescript |
| Rust | async fn, .await, Future | runtime-of-choice: Tokio (de facto), async-std (deprecated), smol, embassy (embedded) | zero-cost — state machine compiled to enum; pollable; runtime-agnostic by language design | rust |
| [[Languages/csharp|C#]] | async, await, Task<T>, ValueTask<T> | ThreadPool + I/O completion ports (Windows) / epoll (Linux) | C# 5.0 (2012) — pioneered mainstream async/await | csharp |
| [[Languages/fsharp|F#]] | async { return! ... } + computation expressions; also task { ... } | inherits .NET Task | F# Async since 2007 (predates C# Task); task { } since F# 6.0 for Task<T> interop | fsharp |
| Kotlin | suspend fun, launch, async/await, coroutines | Dispatchers.Default, IO, Main, Unconfined | coroutineScope + supervisorScope for structured concurrency | kotlin |
| Swift | async fn, await, Task, actor | cooperative thread pool | Swift 5.5 (2021); actor type isolates state; structured concurrency via TaskGroup | swift |
| Dart | async, await, Future, Stream | Dart event loop + isolates (for parallelism) | Dart 1.0+ | dart |
| Zig | async fn, suspend, resume; async removed in 0.11, may return | runtime-customizable | controversial; async was pulled in 0.11 (2023) for redesign | zig |
| Nim | async, await, Future | asyncdispatch | dual: also has thread + spawn | nim |
| OCaml | Lwt (Lwt.return, let%lwt), Async (Jane Street), Eio (OCaml 5 effects) | varies | OCaml 5 effects enable direct-style Eio (no monadic await) | ocaml |
| Scala | Future, for-comprehension; Cats Effect IO; ZIO | runtime-of-choice | functional-effect libs dominate new microservices | scala |
Function coloring (Bob Nystrom 2015): in language with async/await, an async function can only be called from an async function (or with awkward bridging). “Red” (async) vs “Blue” (sync) functions infect the entire call graph. Swift, Kotlin, Rust, C#, Python all have this. Go + Erlang + Java virtual threads + Zig (proposed) avoid it.
5. Actors
A unit of computation that owns private state and processes a queue of messages. Concurrency is achieved by many actors running in parallel, communicating only through messages. No shared mutable state.
| Language / library | Primitive | Mailbox | Supervision | Notes | Linked note |
|---|---|---|---|---|---|
| Erlang | spawn/1, ! (send), receive | unbounded FIFO | OTP supervisor (gen_server, supervisor) | the canonical model since 1986 | erlang |
| Elixir | spawn, send, receive, GenServer | same | same | identical to Erlang | elixir |
| Gleam | typed actor via gleam_otp | BEAM | OTP supervisor | typed message channels | gleam |
| Scala | Akka classic Actor / Akka Typed | bounded/unbounded | supervisor strategy | Akka now in BSL license (2022); Pekko (Apache fork 2022) is the OSS path | scala |
| Java | Akka / Pekko Java DSL | as above | as above | same model on JVM | java |
| Pony | actor is first-class language entity | bounded by default | runtime-managed fault containment | reference capabilities prove no shared mutable state at compile time | pony |
| [[Languages/csharp|C#]] | Akka.NET; Orleans (Microsoft) | varies | grain activation | Orleans uses “virtual actors” with auto-activation/deactivation | csharp |
| Swift | actor keyword (Swift 5.5+) | serialized executor | none | language-level isolation enforced by compiler | swift |
| Rust | Actix actor lib; Bastion (less-maintained); Ractor | varies | manual | actix-web is the largest user | rust |
Actor systems excel at fault isolation. The OTP supervisor model treats actor crashes as routine — a supervisor catches the crash + restarts a fresh actor. This is why WhatsApp/Discord/RabbitMQ/Riak built on Erlang/Elixir.
Akka was BSL-licensed in Sept 2022; Apache Pekko (Akka 2.6.x fork) is the OSS-licensed continuation. Pekko 1.0 GA shipped 2023-08.
6. CSP channels
Communicating Sequential Processes (Hoare 1978): independent processes communicate by sending values on channels. No shared state.
| Language | Channel API | Buffered / unbuffered | Notes | Linked note |
|---|---|---|---|---|
| Go | ch := make(chan int, 10); ch <- v; v := <-ch; select | both | the canonical formulation; CSP + goroutine + select | go |
| Clojure | core.async; chan, >!, <!, go, alt! | both | borrowed from Go; runs on JVM thread pool | clojure |
| Rust | std::sync::mpsc, crossbeam::channel, tokio::sync::mpsc, flume | both | crossbeam: faster multi-producer multi-consumer; tokio for async; flume for performance | rust |
| Crystal | Channel(T) | both | typed; Crystal-fiber-friendly | crystal |
| Elixir | message passing via mailboxes (no explicit channels; mailbox IS the channel) | unbounded | actor model | elixir |
| OCaml | Event (events.ml in stdlib); domainslib Chan for OCaml 5 | both | Eio also provides streams | ocaml |
| Erlang | mailbox (process inbox) | unbounded | same model as Elixir | erlang |
| Kotlin | Channel<T>, produce, consume, select | both | part of kotlinx.coroutines; also Flow for streams | kotlin |
| Swift | AsyncStream, AsyncChannel (swift-async-algorithms) | both | async-algorithms package (2022+) | swift |
| Scala | ZIO Queue; Akka Streams Source | both | functional effect libs | scala |
select (Go) / alt! (Clojure) / select! (Tokio) is the cross-cutting primitive — wait on multiple channels and act on the first ready.
7. STM — Software Transactional Memory
Composable lock-free synchronization: a block runs in a transaction; conflicts retry. Composes safely (you can compose two transactions into one); locks don’t.
| Language | API | Notes | Linked note |
|---|---|---|---|
| Haskell | STM, TVar, atomically, retry, orElse | the canonical STM (Harris-Marlow 2005); enforced via type system — IO actions don’t compile inside atomically | haskell |
| Clojure | dosync, ref, alter, commute | second-best implementation; on JVM | clojure |
| Scala | scala-stm library; Akka STM (deprecated) | OK but less used | scala |
| Pony | reference capabilities give similar guarantees at compile time | no runtime STM but the semantics are checked statically | pony |
| Rust | parking_lot / crossbeam_epoch; STM crate exists but rarely used | Rust prefers compile-time enforced sharing | rust |
STM stayed niche because its performance + memory overhead grow with contention, and Erlang/Go solved the same problem with isolation rather than transactions.
8. Reactive streams
Push-based dataflow: a stream emits events, downstream operators transform/filter/buffer, eventually consumed. Backpressure built-in.
| Library | Language | Spec | Notes | Linked note |
|---|---|---|---|---|
| RxJava | JVM | Reactive Streams JVM spec | classic Rx port; ReactiveX family | java |
| RxJS | JS/TS | RxJava lineage | now rxjs@7+; common in Angular | javascript |
| RxSwift | Swift | RxJava lineage | being displaced by Combine + async/await | swift |
| Combine | Swift | Apple’s first-party reactive | iOS 13+; subsumed by AsyncSequence + structured concurrency in iOS 16+ | swift |
| Project Reactor | JVM | Reactive Streams spec | Spring WebFlux backbone | java / kotlin |
| Mutiny | JVM (Quarkus) | Reactive Streams spec | Quarkus framework | java |
| Akka Streams / Pekko Streams | JVM | Reactive Streams spec | actor + stream | scala |
| Flow | Kotlin | cold; coroutine-based | kotlinx.coroutines.flow.Flow — Kotlin-native; replaces Rx for Android | kotlin |
| AsyncSequence | Swift | language-level | iOS 15+ | swift |
| AsyncStream | C# | language-level | C# 8+ via IAsyncEnumerable<T> | csharp |
The 2018+ trend: language-level async streams displace third-party reactive libraries. Kotlin Flow, Swift AsyncSequence, C# IAsyncEnumerable, Rust Stream all provide the 80% of Rx with cleaner integration. Rx libraries persist where you need rich operators (windowing, multicast, retry-with-backoff).
9. Event loops
The single-thread + queue model behind most async runtimes.
| Loop | Language | Backend | Used by | Linked note |
|---|---|---|---|---|
| libuv | C | epoll/kqueue/IOCP/io_uring | Node.js, Julia, neovim | javascript / julia |
| Tokio | Rust | mio → epoll/kqueue/IOCP/io_uring | half the async-Rust ecosystem | rust |
| asyncio | Python | selectors (kqueue/epoll/IOCP) | stdlib (Python 3.4+) | python |
| Trio | Python | similar | Python; pioneered nursery + structured concurrency | python |
| libevent / libev | C | epoll/kqueue | older C servers | c |
| Boost.Asio | C++ | epoll/kqueue/IOCP/io_uring | dominant C++ I/O | cpp |
| io_uring (Linux 5.1+, 2019) | kernel | true async syscalls | gradually replacing epoll for high-perf I/O on Linux | c |
| Netty | JVM | epoll/kqueue | Java HTTP servers, gRPC, RSocket | java |
| Vert.x | JVM | Netty | Java/Kotlin reactive | java |
| .NET ThreadPool + IOCP | .NET | Windows IOCP / epoll | C# async backbone | csharp |
| GCD (Grand Central Dispatch) | Apple | kqueue | Swift / Obj-C | swift |
| Crystal Fiber scheduler | Crystal | epoll/kqueue | Crystal | crystal |
| BEAM scheduler | Erlang/Elixir | poll + reduction-counting | BEAM | erlang |
| Go runtime | Go | netpoller (epoll/kqueue/IOCP) | Go | go |
io_uring (Linux 5.1, 2019; stabilized 5.6+, mature 6.x 2023+) is the new high-performance Linux I/O — submission + completion queues in shared memory, no syscall per op. Tokio supports it as an optional backend; ScyllaDB Seastar uses it natively.
10. Thread pools + work-stealing
The bridge between many submitted tasks and few OS threads. Work-stealing balances load across pool threads.
| Pool / library | Language | Notes | Linked note |
|---|---|---|---|
Java ForkJoinPool | Java | the canonical work-stealing pool; underpins parallelStream + CompletableFuture.async + virtual threads | java |
ThreadPoolExecutor | Java | non-stealing; for bounded queue + named-thread scenarios | java |
| Go runtime | Go | M:N + work-stealing across P (logical processor) | go |
| Tokio multi-thread runtime | Rust | work-stealing across worker threads | rust |
| Rust Rayon | Rust | data-parallel work-stealing; par_iter() | rust |
| .NET ThreadPool | C# | hill-climbing heuristic + work-stealing | csharp |
| TBB (Intel oneAPI) | C++ | task-based parallelism + parallel_for | cpp |
| OpenMP | C/C++/Fortran | directive-based parallel for | c / cpp / fortran |
| Cilk Plus | C/C++ (deprecated 2017+) | the original work-stealing | — |
| Erlang BEAM | Erlang | per-scheduler run queue with stealing | erlang |
| OCaml 5 Domainslib | OCaml | work-stealing pool atop domains | ocaml |
| Julia threads | Julia | work-stealing | julia |
| Swift Concurrency cooperative pool | Swift | per-core cooperative executor | swift |
11. Structured concurrency
A scope-rooted task tree: every spawned task is owned by some parent scope, the parent does not return until all children complete (or are cancelled). Nathaniel Smith’s Trio (Python, 2018) established the pattern; mainstream uptake has been broad.
| Language | Primitive | Notes | Linked note |
|---|---|---|---|
| Python | Trio nursery; stdlib asyncio.TaskGroup (3.11+) | ExceptionGroup + except* | python |
| Kotlin | coroutineScope { }, supervisorScope { } | coroutines 1.0+ (2018) | kotlin |
| Swift | withTaskGroup, withThrowingTaskGroup | Swift 5.5+ | swift |
| Java | StructuredTaskScope JEP 453 (preview J21; final J25 2025-09) | first language-level Java structured concurrency | java |
| [[Languages/c|C#]] | not language-level; Task.WhenAll + CancellationToken | manual pattern; no built-in scope | csharp |
| Rust | Tokio JoinSet + select!; tokio::task::scope (unstable as of 1.85) | runtime-level only; not enforced | rust |
| Go | errgroup.Group; no language-level scope | de facto pattern via context | go |
| Elixir | Task.Supervisor + OTP supervisor | OTP supervises beyond a single task tree | elixir |
| OCaml | Eio Switch (OCaml 5 + effects) | first-class via effects | ocaml |
12. Virtual threads (Java) and fibers (JVM/C++)
A radical 2018–2024 shift: rather than colored async/await, give every developer cheap green threads + blocking syscalls and let the runtime intercept blocking and yield.
| Language | Primitive | Status | Notes | Linked note |
|---|---|---|---|---|
| Java | virtual threads (JEP 444, J21) | GA in J21 (2023-09) | OS-thread-like API, runtime-scheduled atop carrier threads; ~kB stack; uncolored: blocking I/O auto-yields | java |
| Quasar (JVM) | fibers (Parallel Universe, then deprecated 2018; replaced by Project Loom) | superseded | predecessor of Loom virtual threads | java |
| Project Loom (JVM) | research project | shipped as J21 virtual threads | research → JEP 444 + JEP 453 + JEP 491 | java |
| Boost.Fiber | C++ | mature | stackful coroutines on a thread pool | cpp |
| Go | goroutines | mature since 2009 | the original virtual-thread system | go |
| Skynet benchmark | comparison | benchmark shows 1M tasks: Erlang ~5s, Go ~6s, Java VT ~8s, Akka ~9s — all O(seconds) for 1M tasks | comparison sites | n/a |
Java virtual threads’ big idea: existing synchronized + blocking I/O code Just Works™ — without rewriting to async/await. As of Java 21 production deployments (Spring Boot 3.2+, Hibernate 7+, Helidon 4+) the migration story is “set -Dspring.threads.virtual.enabled=true and your servlet container scales to 100k concurrent”. J25 (2025-09) finalizes the structured-concurrency layer (JEP 491).
13. Per-language summary
| Language | Native primitive | Idiomatic for I/O concurrency | Coloring? | Linked note |
|---|---|---|---|---|
| Python | OS threads + GIL; asyncio coroutines | asyncio + TaskGroup; or threading (GIL-bound); 3.13t free-threaded | yes | python |
| JavaScript | event loop + async/await | async/await + Promise | yes | javascript |
| TypeScript | inherits JS | same; Effect-TS for new microservices | yes | typescript |
| Java | OS thread (legacy) → virtual thread (J21+) | virtual threads | no (J21+) | java |
| C | pthread | pthread + libuv/libev/io_uring | n/a (no coroutines without lib) | c |
| C++ | std::thread + std::jthread; coroutines (C++20) | Boost.Asio + coroutines OR Boost.Fiber | yes | cpp |
| [[Languages/csharp|C#]] | Task + async/await + ThreadPool | async/await + IOCP | yes | csharp |
| Go | goroutine + channel + select | goroutines + channels | no | go |
| Rust | Tokio async/await; thread for CPU | Tokio + structured spawn | yes | rust |
| Swift | actor + async/await + Task | async/await + actor | yes | swift |
| Kotlin | coroutines + Flow + Channel | coroutines + structured scope | yes (suspend) | kotlin |
| Ruby | Thread (GIL until 3.0) + Ractor (3.0+) + Fiber | Async (gem) for I/O, Ractor for parallelism | mixed | ruby |
| PHP | process-per-request (FPM) + ReactPHP / Amp / Swoole | Swoole / Amp for async | yes | php |
| Scala | Future + Akka/Pekko + Cats Effect + ZIO | Cats Effect 3 or ZIO 2 | varies | scala |
| Dart | Future + Stream + Isolate | async/await + Stream | yes | dart |
| Elixir | BEAM process | spawn + GenServer + Task | no | elixir |
| Haskell | green thread + STM + async lib | forkIO + STM + async package | yes (IO) | haskell |
| Clojure | JVM thread + core.async + future + agent | core.async or futures | mixed | clojure |
| [[Languages/fsharp|F#]] | Task + async + agent | async { } or task { } | yes | fsharp |
| Lua | coroutine (single-threaded) + LuaJIT + luaproc | coroutine + lib (Copas, OpenResty cosocket) | yes | lua |
| R | future + furrr; parallel | future package + furrr; mostly batch | n/a | r |
| Julia | Threads.@spawn + Task + Channel | @async + fetch + Channel | partial | julia |
| Zig | thread + async (removed 0.11 → being redesigned) | thread; async TBD | TBD | zig |
| Nim | thread + async + spawn | asyncdispatch | yes | nim |
| Crystal | Fiber + Channel | Fiber + Channel CSP | no | crystal |
| OCaml | OCaml 5 domain + Eio (effects) | Lwt (monad) or Eio (effects, OCaml 5+) | OCaml 5 effects: no | ocaml |
| Perl | thread (discouraged) + fork + AnyEvent | fork + AnyEvent / Mojolicious | mixed | perl |
| Erlang | BEAM process + mailbox | spawn + receive + supervisor | no | erlang |
| Racket | thread + place + future | thread + place | n/a | racket |
| Common Lisp | bordeaux-threads | impl-specific (SBCL: native threads) | n/a | common-lisp |
| Scheme | impl-specific; SRFI-18 threads | impl-specific | n/a | scheme |
| Fortran | OpenMP + coarray | OpenMP + MPI | n/a | fortran |
| COBOL | classical: single-threaded; modern: CICS multi-region | CICS + IMS | n/a | cobol |
| Ada | task (first-class) + protected | task + rendezvous | n/a | ada |
| Pascal | classical: none; Delphi: Thread + TParallel.For | Thread / TTask | n/a | pascal |
| Prolog | impl-specific (SWI: threads); engines in some | thread_create | n/a | prolog |
| Tcl | event loop + after + thread package | event loop | n/a | tcl |
| Groovy | inherits JVM | GPars + ParallelGroup | yes | groovy |
| Bash | & (fork) + wait + xargs -P | fork + parallel | n/a | bash |
| PowerShell | Job + Workflow + ForEach -Parallel (PS 7+) | -Parallel + ThreadJob | n/a | powershell |
| SQL | engine-managed (parallel query) | declarative | n/a | sql |
| V | go-style spawn + channel | go { … } | no | v |
| Odin | thread; sync package | thread + lib | n/a | odin |
| Roc | platform-defined | platform-supplied effects | platform-dep | roc |
| Gleam | BEAM process | OTP via gleam_otp | no | gleam |
| Pony | actor (first-class) | actor + reference caps | no | pony |
| Idris | impl-specific; effects + handlers | research-stage | n/a | idris |
| Lean | Task + IO monad | functional; not heavy async | yes | lean |
| Coq (Rocq) | n/a (proof asst) | OCaml runtime | n/a | coq |
| Agda | n/a | GHC runtime | n/a | agda |
| Smalltalk | Process (green thread) | Process + Semaphore | n/a | smalltalk |
| MATLAB | parfeval + Parallel Computing Toolbox | PCT + parfor | n/a | matlab |
| Mojo | parallelize + SIMD + GPU | parallelize / vectorize / GPU | TBD | mojo |
14. The convergence and divergence — 2020–2026
Two opposing trends fight in the language design space:
(A) Uncolored / Loom-style — virtual threads + blocking I/O. Pioneered by Erlang (1986) and Go (2009); brought to mainstream JVM by Project Loom (J21, 2023). The pitch: existing synchronous code scales to 100k+ concurrent without rewriting. Maintenance + onboarding cost much lower.
(B) Colored async/await — explicit async/await. Strong in Rust (zero-cost futures), C# (Task), Swift (Task), Kotlin (suspend), Python (asyncio), JS. The pitch: visible suspension points + zero-cost state machines + cancellation semantics.
The 2024–2026 movement is convergence on the middle:
- Java got virtual threads (uncolored) AND
StructuredTaskScope(structured concurrency). - Kotlin coroutines are colored but structured.
- Swift 6 added typed throws + actor isolation.
- Python 3.13t (free-threaded build) + asyncio TaskGroup gives both true parallelism + structured async.
- OCaml 5 effects are uncolored async —
let x = perform Read; ...works without async-coloring the function signature. - io_uring (Linux 2019, mature 2024) shifts kernel I/O closer to truly async.
The research-language frontier is algebraic effects (Koka, Eff, Effekt, OCaml 5) — uncolored async without a runtime, just typed effects + handlers. Eio is the production face of this.
Adjacent
- _compare_concurrency — the more abstract concurrency-model taxonomy.
- _compare_memory_models — virtual thread stacks + per-process heaps + atomic refcount under async.
- _compare_error-handling — structured concurrency error propagation.
- _compare_type_systems — effect typing + nullability + async coloring.
When to pick what
Workload?
├─ Mostly CPU-bound parallel compute
│ ├─ Data parallel → OpenMP / Rust Rayon / Julia Threads / MATLAB parfor / Mojo parallelize / Fortran coarray
│ ├─ Task parallel → ForkJoinPool / Rayon / TBB / OCaml Domainslib
│ └─ GPU → CUDA / Mojo / Triton (Python) / WebGPU
├─ Mostly I/O-bound, < 10⁴ connections
│ ├─ Any modern language's default is fine; use OS threads if convenient
│ └─ Erlang/Elixir/Go shine even at this scale due to simplicity
├─ I/O-bound, 10⁴–10⁶ connections
│ ├─ Go goroutines (smallest learning curve)
│ ├─ Erlang/Elixir BEAM (fault-tolerant)
│ ├─ Java virtual threads J21+ (existing Java codebase)
│ ├─ Rust Tokio (max throughput + safety)
│ └─ Node.js (JS skills, single-thread + cluster)
├─ Real-time / hard latency
│ ├─ Embedded → Rust no_std + embassy executor; C with RTOS (FreeRTOS, Zephyr)
│ ├─ Audio / video → C++ with lock-free + Boost.Asio; Rust real-time
│ └─ Trading → C++ + custom kernel-bypass (DPDK, Solarflare); Rust + io_uring
├─ Distributed / fault-tolerant millions of conns
│ └─ Erlang/Elixir OTP supervisor; or Akka/Pekko (JVM); or Orleans (.NET)
├─ Browser / single event loop
│ └─ async/await + Promise + Web Workers for parallelism
├─ Reactive UI / dataflow
│ ├─ Mobile → Kotlin Flow / Swift AsyncSequence / Combine
│ ├─ Web → RxJS or signals (SolidJS, Svelte 5, Vue 3 reactivity)
│ └─ Backend dataflow → Akka Streams / Pekko Streams / Project Reactor / Mutiny
├─ Data pipeline / ETL
│ ├─ Stream processing → Kafka Streams (JVM), Flink, Beam, Spark Structured Streaming
│ ├─ Batch → Apache Airflow / Prefect / Dagster + multiprocessing
│ └─ Functional effect-typed → ZIO / Cats Effect
├─ Composable transactions
│ └─ Haskell STM (canonical); Clojure dosync; Pony reference caps
├─ Need uncolored code today
│ ├─ Go, Erlang, Elixir, Gleam, Crystal — pure uncolored
│ └─ Java virtual threads (J21+) — uncolored for JVM
└─ Embedded / no_std / heap-free
└─ Rust embassy executor; C with RTOS; Zig (TBD async); Ada Ravenscar profile
The single biggest practical lesson 2018–2026: uncolored beats colored at scale-of-developer-time. The companies that have moved fastest at scale — Discord (Elixir), WhatsApp (Erlang), Cloudflare (Go heavy), Adobe Stock (Go), Stripe (Ruby + Go for hot paths), Twitter (Scala + JVM virtual threads in progress), and the entire Java enterprise stack post-Loom — all bet on uncolored. Async/await colored stays the right choice for library code (Rust, C#, Python), single-threaded event loops (JS, Node), and maximum throughput (Tokio, ZIO, Cats Effect). For everything else, prefer the model that lets you write straightforward sequential code that scales.