Concurrency Models — Cross-Language Comparison
Concurrency Models — Cross-Language Comparison
- Type: cross-cutting comparison
- Languages covered: 51 (see _index)
- Last updated: 2026-05-07
TL;DR
- “Concurrency” and “parallelism” are different problems. Concurrency = composition of independent activities; parallelism = simultaneous execution. Most languages do both, with very different tools.
- Five families dominate: OS threads + locks, lightweight M:N green threads, async/await + event loop, actor model (share-nothing), and CSP (channels + processes). STM is a sixth, less common.
- The biggest practical question: does your language have shared mutable state? If yes (Java, C++, Rust threads, Go shared maps), you need lock discipline. If no (Erlang, Pony, Elixir), data-race-freedom comes for free at the architectural level.
- Goroutines, virtual threads, fibers, and async tasks are converging — they’re all “stack on heap, scheduled by user-space, blocking on I/O is cheap”. Go got there in 2009; Java got there in 2023 (Loom); Python is mid-transition with asyncio.
- Pony is the only language where the type system enforces data-race freedom at compile time — via reference capabilities, not runtime checks.
The spectrum
SYNCHRONOUS ──────── THREADS+LOCKS ──── ASYNC ──── M:N THREADS ──── CSP ──── ACTORS ──── STM ──── REFCAPS
| | | | | | | |
SQL Java/C# JS/Node Go Go Erlang Haskell Pony
COBOL C++ Python Java Loom Clojure Elixir Clojure
Bash (subshells) Rust threads C# async Crystal Rust Akka refs
Swift Swift Tcl 9 thrd Smalltalk
F# async futures
Categories
1. No language-level concurrency (process-level only)
The language has no in-process concurrency primitives; you compose with subshells/pipelines/fork.
- Bash — backgrounding (
&),wait, subshells, pipelines, FIFOs,coproc.wait -n(5.1+) waits for any child. - SQL — query planner parallelizes within the engine; the language itself has no concurrency primitives. Transaction isolation levels are the closest thing.
- COBOL — classical COBOL has no threading; CICS provides task-level concurrency externally.
- awk / sed — (mention) pipeline-level only.
2. OS threads + shared memory + locks
The traditional model: kernel threads, shared address space, mutexes/semaphores/condvars/atomics. Maximum compatibility, maximum footgun surface.
- C — POSIX threads (
<pthread.h>); C11<threads.h>(rarely used). Atomics in<stdatomic.h>. - C++ —
std::thread,std::mutex,std::atomic,std::async. Coroutines (C++20) for async. - Rust —
std::thread::spawn;Send/Synctraits enforce thread-safety at compile time;Mutex,RwLock, atomics. - Swift — Grand Central Dispatch (GCD); modern code uses structured concurrency (Swift 5.5+) on top of GCD.
- [[Languages/csharp|C#]] / [[Languages/fsharp|F#]] —
System.Threading.Thread; modern code usesTask+ async (see category 3). - Java — platform threads +
java.util.concurrent; virtual threads (Loom, JDK 21+) are the modern path. - Kotlin — JVM threads + coroutines (recommended).
- Scala — JVM threads; modern code uses Cats Effect / ZIO (effect systems on top).
- Groovy — JVM threads.
- Ada —
taskkeyword; protected objects (monitors);entryfor rendezvous. Built into the language since 1983. - Pascal (Delphi/Free Pascal) —
TThreadclass, critical sections. - Fortran — coarrays (parallel images, distributed memory) +
do concurrent(vectorization hint) + OpenMP/OpenACC pragmas. - Zig —
std.Thread; minimalist; no built-in async runtime as of 0.16. - Odin —
core:thread; explicit; allocator-aware viacontext. - V —
spawnkeyword; channels (Go-style); explicit threads. - Nim —
threadingmodule +spawn/parallel; thread-local storage by default. - Crystal — fibers (M:N) + parallel mode (
-Dpreview_mt). See category 4.
3. Async/await + event loop (single-threaded by default)
A single OS thread runs an event loop; tasks yield at await points; I/O is non-blocking.
- JavaScript — single-threaded event loop. Web Workers + SharedArrayBuffer + Atomics for parallelism.
AsyncContext(TC39) is the modern story for cross-await context. - TypeScript — inherits JS.
- Python —
asyncio(since 3.4);async/awaitsyntax (3.5+). Free-threaded build (3.14, PEP 779) lets multiple threads run Python bytecode in parallel. - [[Languages/csharp|C#]] —
Task+async/await(since 2012). Truly multi-threaded:Taskruns on a thread pool by default. - [[Languages/fsharp|F#]] — async workflows (older, computation expression) + Task-based async (newer, .NET-aligned).
- Rust —
async fn(zero-cost); needs a runtime (tokio,smol,async-std). Pinning +Send/Syncconstraints make async multithreaded. - Swift — structured concurrency (Swift 5.5+):
async/await,Task,TaskGroup, actors (Swift’s actors, see category 5),@MainActor. - Dart —
Future+async/await; isolates for true parallelism (no shared memory). - Kotlin — coroutines (
suspend fun); structured concurrency viaCoroutineScope. - TypeScript — same as JS.
4. Lightweight M:N user-space scheduling (green threads / fibers / virtual threads)
The runtime multiplexes many lightweight tasks onto a smaller pool of OS threads. Blocking I/O is cheap because the runtime handles it.
- Go — goroutines + GMP scheduler. The original M:N goroutines (2009).
- Java — virtual threads (JDK 21 LTS, 2023). Project Loom.
- Erlang / Elixir / Gleam — BEAM processes (M:N, preemptive — unique). Each process has its own heap.
- Crystal — fibers (cooperative); preview multi-threading.
- Tcl — coroutines (
coroutine, since 8.6); separate from threads (Threadpackage). - Lua — coroutines built in (
coroutine.create,resume,yield); no scheduler — you build one. - Ruby — Fibers (since 1.9); FiberScheduler interface (3.0+) integrates with async I/O via async gem. Ractors for true parallelism (3.0+).
- PHP — Fibers (8.1+); Swoole / ReactPHP / Amp build async on top.
- Python —
gevent/eventlethistorically; asyncio is the modern standard.
5. Actor model — share-nothing message passing
Each actor has private state; communication is asynchronous messages. No shared mutable memory between actors → no data races at the model level.
- Erlang — the canonical actor model. Per-process heap; copy-on-send. Selective receive. OTP behaviors.
- Elixir — Erlang’s actor model, with macros + tooling.
- Gleam — typed actor model on BEAM via
gleam_otp. - Pony — actors + reference capabilities (compile-time data-race freedom). ORCA distributed GC.
- Scala / Java — Akka (now Apache Pekko) brings actors to JVM.
- Swift —
actorkeyword (Swift 5.5+); methods serialized through actor’s executor;nonisolatedto opt out. - Clojure —
agent(single-threaded actor-like) +core.async(CSP-flavored, see next). - Smalltalk — futures and async via
Promiseclass libraries; not built-in.
6. Communicating Sequential Processes (CSP) — channels + processes
Hoare’s CSP: independent processes communicate over channels. Channels are first-class; pattern-match on receive (select).
- Go — channels +
select. Goroutines + channels = the Go concurrency story. - Clojure —
core.asyncgo-blocks + channels. Compile-time CPS transform. - Crystal — channels + fibers;
Channel(T)is generic. - Rust —
std::sync::mpsc;crossbeamfor advanced. Async channels intokio. - V — channels (Go-style).
- Pony — actors with capability-typed messages; CSP-adjacent.
- Occam — (mention) the original CSP language.
7. Software Transactional Memory (STM)
Composable memory transactions: read+write is atomic, retried on conflict. No locks, no deadlocks, but conflict-rate overhead.
- Haskell —
STMmonad; the canonical implementation.atomically,retry,orElse. - Clojure —
ref+dosync. Built-in. - Scala — Cats Effect (
Ref,Deferred); ZIO STM. - Common Lisp — STM as library (cl-stm, libraries vary by impl).
8. Coroutines / fibers — cooperative, no built-in scheduler
Just the primitive: yield/resume. You compose into your own scheduler or async system.
- Lua — first-class coroutines.
- Python — generators historically;
async/awaitis the modern path. - C++ —
co_await/co_yield/co_return(C++20). - Ruby — Fibers.
- PHP — Fibers (8.1+).
- Tcl —
coroutine. - Perl — Coro (CPAN module; deprecated approach), Future::AsyncAwait modern.
9. Multiple dispatch + parallel collections (data parallelism)
The language is data-parallel-flavored — you express what to compute over arrays/collections, the runtime parallelizes.
- Julia —
@threadsfor thread-parallel loops;Distributedfor cluster; CUDA.jl/AMDGPU.jl for GPU. Multiple dispatch makes parallel libraries highly composable. - R —
parallelpackage (mclapply,parLapply);future+furrrfor futures-style;data.tableparallelizes via OpenMP. - Fortran — coarrays +
do concurrent; OpenMP/OpenACC. - C / C++ — OpenMP pragmas; CUDA/HIP/SYCL for GPU.
- Python — multiprocessing (process-level), Numpy/Numba for vectorization, mpi4py for MPI.
10. Logic-programming concurrency
Concurrent solving of logic programs; or-parallelism.
- Prolog — SWI-Prolog has threads + thread-local global vars. Mercury supports declarative parallelism via mode-correctness.
11. Reference-capability typed concurrency
Compile-time guarantees that messages between concurrent units are data-race-free.
- Pony — 6 reference capabilities (iso/val/ref/box/tag/trn) + actors. The data-race-free actor model.
12. Image-based / cooperative
Concurrency happens within the live image; green threads or VM-level threads.
- Smalltalk (Pharo) — green threads (
Process); VM-level cooperative scheduling. Modern Pharo supports OS threads viaProcess. - Common Lisp —
bordeaux-threadslibrary (portable wrapper); SBCL has native threads.
13. Theorem-prover languages — concurrency limited
- Idris / Agda / Lean / Coq (Rocq) — runtime concurrency depends on the backend (e.g., Idris uses Chez Scheme threads). The languages themselves are sequential by default.
Per-language quick reference
| Language | Primary mechanism | Tools / syntax | Watch out for |
|---|---|---|---|
| Python | asyncio + threads + multiprocessing | async/await; concurrent.futures; PEP 779 free-threaded | GIL (still in non-free-threaded builds); thread-vs-process choice |
| JavaScript | Single-threaded event loop + Workers | async/await; Promise; SharedArrayBuffer | No shared memory between workers (except SAB) |
| TypeScript | (inherits JS) | same as JS | same as JS |
| Java | Threads + virtual threads (Loom) + j.u.c | Thread.ofVirtual(); ExecutorService; synchronized | Pin-warning for legacy code |
| C | POSIX threads | pthread_create; mutexes; atomics | Memory model subtleties |
| C++ | std::thread + coroutines | std::async, co_await | Memory ordering; iterator invalidation |
| [[Languages/csharp|C#]] | Task + async + threads | async/await; Channel<T>; Parallel.ForEach | sync-over-async deadlock |
| Go | Goroutines + channels (CSP) | go, chan, select | Goroutine leaks; channel deadlocks |
| Rust | Threads + async runtime | tokio/smol; Send/Sync checked | Pinning + 'static constraints |
| Swift | Structured concurrency + actors | async/await, Task, actor | @MainActor boundaries |
| Kotlin | Coroutines | suspend fun, CoroutineScope | Cancellation cooperation |
| Ruby | Threads (with GVL) + Fibers + Ractors | Ractor for parallel; Fiber for cooperative | GVL on threads; Ractors limited |
| PHP | Fibers (8.1+) + Swoole/Amp | Fiber API; parallel extension | Per-request scope; ext-pthreads dead |
| Scala | JVM threads + Akka/Pekko + Cats Effect / ZIO | actor or effect-monad | Multiple ecosystems coexisting |
| Dart | Isolates + Futures | no shared memory between isolates | isolate spawn cost |
| Elixir | BEAM processes (actor) | Task, GenServer, Supervisor | Mailbox growth |
| Haskell | STM + lightweight threads | atomically, forkIO, MVar | Async exceptions; thunks in MVar |
| Clojure | core.async (CSP) + STM (refs) + agents | go, chan, dosync, agent, atom | Multiple primitives — pick wisely |
| [[Languages/fsharp|F#]] | Async workflows + Tasks | async {} or task {} | F# async vs .NET Task semantics differ |
| Lua | Coroutines | coroutine.create/resume/yield | No native scheduler |
| R | parallel + future packages | mclapply, future, furrr | RNG seed stability |
| Julia | Threads + Distributed + Tasks | Threads.@threads, @spawn | Type instability blocks parallel speedup |
| Zig | std.Thread (minimal) | atomics; no built-in async runtime as of 0.16 | Async story in flux |
| Nim | Threadpool + spawn + channels | parallel, spawn, Channel | Thread-local heaps by default |
| Crystal | Fibers + Channels | spawn, Channel | Preview multi-threading |
| OCaml | Effects + domains (multicore, OCaml 5+) | domain, effect handlers | Effects are still maturing |
| Perl | ithreads + AnyEvent / Future::AsyncAwait | Coro (legacy); Future::AsyncAwait (modern) | ithreads heavyweight (full interp copy) |
| Erlang | Actor model | spawn, !, receive, OTP | Mailbox copy cost |
| Racket | Threads + custodians | thread, place (parallelism) | Custodian-managed resources |
| Common Lisp | bordeaux-threads + impl-specific | impl varies (SBCL native threads) | Image-level state synchronization |
| Scheme | Impl-specific | varies | Continuations make threads tricky |
| Fortran | Coarrays + do concurrent + OpenMP | coarray, image, sync all | SPMD model; image counting |
| COBOL | None in-language | CICS task/external | Mainframe-environment-specific |
| Ada | Tasks + protected objects + rendezvous | task, entry, protected | Ravenscar/Jorvik for safety-critical |
| Pascal | TThread + critical sections | TThread, TCriticalSection | Per-impl varies |
| Prolog | Threads (SWI) + tabling | thread_create; CLP for declarative | Backtracking + threads = nuanced |
| Tcl | Thread package + coroutines | Thread, coroutine | Per-thread interpreter copy |
| Groovy | JVM threads + GPars | similar to Java | similar to Java |
| Bash | Process-level only | &, wait -n, coproc | No in-process sharing |
| PowerShell | Runspaces + ForEach-Object -Parallel | [runspacefactory], Start-Job | Apartment threading on Windows |
| SQL | Engine-level only | transactions; isolation levels | Isolation level semantics differ across dialects |
| V | spawn + channels | spawn fn(), chan | Pre-1.0 stability |
| Odin | core:thread (explicit) | manual; allocator-aware | Allocator must be thread-safe |
| Roc | Effects via platform | platform-specific | App is pure; platform handles concurrency |
| Gleam | BEAM processes (typed actor) | gleam_otp; supervisors | Types add safety on actor model |
| Pony | Actors + reference capabilities | actor, be (behaviors); refcaps | Capability typing is the learning cliff |
| Idris | Backend-dependent | Chez Scheme threads (default) | Concurrency story tied to backend |
| Lean | Tasks + IORef | Task.spawn; minimalist | Pure-function-first; concurrency secondary |
| Coq (Rocq) | OCaml runtime threads | minimal language-level support | Mostly used for proofs, not runtime |
| Agda | GHC backend threads | minimal | Concurrency rarely the use case |
| Smalltalk | Process (green threads) | [block] fork, semaphores | Image-wide scheduling |
Decision rubric
Pick threads + locks when you need fine-grained shared-memory parallelism on a few cores: numerical kernels, OS code, low-level systems. C, C++, Rust threads, Java platform threads.
Pick async/await + event loop when you have I/O-heavy workloads with many connections: web servers, RPC clients, scrapers. JS/Node, Python asyncio, C# async, Rust async.
Pick goroutines / virtual threads when you want async simplicity (write blocking code) with parallel scaling: web services that fan out across many concurrent connections. Go, Java Loom, BEAM languages.
Pick actors when fault isolation matters as much as concurrency: telecom, real-time messaging, multi-tenant systems. Erlang, Elixir, Pony, Akka.
Pick CSP when you want explicit message-flow control with channel pattern matching: pipelines, event-driven systems. Go, Clojure core.async.
Pick STM when transactional consistency matters and lock complexity has bitten you: in-memory databases, complex shared state. Haskell STM, Clojure refs.
Pick refcaps (Pony) when data-race freedom must be statically guaranteed AND you can pay the learning cliff. Telecom-class invariants.
Pick none / process-level when concurrency is rare in your domain: shell scripting, batch jobs, simple CLIs. Bash, classic Tcl.
Edge cases & nuances
- Goroutines vs virtual threads — superficially identical. Differences: Java’s virtual threads pin to a carrier when in a
synchronizedblock (legacy footgun); goroutines have no such constraint. Java has more mature tooling (JFR, etc.); Go is much simpler. - Async vs M:N threads — async forces a “function-color” split (red/blue functions); M:N threads don’t. Modern Java (Loom), Erlang, Go skip the colored-function problem entirely.
- GIL is dead, but not yet — Python 3.14 free-threaded build (PEP 779) is the no-GIL Python. Adoption through 2027.
- Erlang’s BEAM is unique: preemptive scheduling of green threads (every other M:N runtime is cooperative). The runtime can deschedule a process even if it’s CPU-bound.
- Swift actors are different from Erlang actors: serialization is per-actor (one method at a time), but actors live in the same process and can hold references to other actors directly. Closer to “monitor with a lock” than “process with a mailbox”.
- Rust async needs a runtime: the language defines the syntax + traits; runtime is library (
tokiois dominant;smol,async-stdare alternatives). - STM has a write-skew anomaly — read-only conflict that’s not detected; mostly fine with serializable isolation but worth knowing.
- Pony’s reference capabilities are a steep learning cliff but produce code with mathematically-proved data-race freedom. Read Sylvan Clebsch’s PhD thesis.
- OCaml multicore (5.0+, 2022) added effect handlers +
Domainfor parallelism. Effects are a generalization of async/await/exceptions; they’re still maturing in libraries.
Cross-references
For per-language detail, see each note’s Intermediate (concurrency primitives) and Advanced (concurrency deep dive) sections. The 51 individual notes are linked from _index.
Citations
- Hoare, Communicating Sequential Processes (1978) — foundation paper.
- Armstrong, Programming Erlang — actor model in practice.
- Clebsch, Pony’s Reference Capabilities (PhD thesis, 2018) — refcap semantics.
- Marlow, Parallel and Concurrent Programming in Haskell — STM, Par, Async.
- JEP 444 (Virtual Threads): https://openjdk.org/jeps/444
- Go scheduler design: https://go.dev/blog/scheduler
- PEP 703 (free-threaded Python): https://peps.python.org/pep-0703/
- Swift Concurrency Manifesto + SE-0306: https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md
- The 51 per-language notes in _index are the underlying source.