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.
  • Ruststd::thread::spawn; Send/Sync traits 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 uses Task + 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.
  • Adatask keyword; protected objects (monitors); entry for rendezvous. Built into the language since 1983.
  • Pascal (Delphi/Free Pascal) — TThread class, critical sections.
  • Fortran — coarrays (parallel images, distributed memory) + do concurrent (vectorization hint) + OpenMP/OpenACC pragmas.
  • Zigstd.Thread; minimalist; no built-in async runtime as of 0.16.
  • Odincore:thread; explicit; allocator-aware via context.
  • Vspawn keyword; channels (Go-style); explicit threads.
  • Nimthreading module + 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.
  • Pythonasyncio (since 3.4); async/await syntax (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: Task runs on a thread pool by default.
  • [[Languages/fsharp|F#]] — async workflows (older, computation expression) + Task-based async (newer, .NET-aligned).
  • Rustasync fn (zero-cost); needs a runtime (tokio, smol, async-std). Pinning + Send/Sync constraints make async multithreaded.
  • Swift — structured concurrency (Swift 5.5+): async/await, Task, TaskGroup, actors (Swift’s actors, see category 5), @MainActor.
  • DartFuture + async/await; isolates for true parallelism (no shared memory).
  • Kotlin — coroutines (suspend fun); structured concurrency via CoroutineScope.
  • 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 (Thread package).
  • 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.
  • Pythongevent/eventlet historically; 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.
  • Swiftactor keyword (Swift 5.5+); methods serialized through actor’s executor; nonisolated to opt out.
  • Clojureagent (single-threaded actor-like) + core.async (CSP-flavored, see next).
  • Smalltalk — futures and async via Promise class 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.
  • Clojurecore.async go-blocks + channels. Compile-time CPS transform.
  • Crystal — channels + fibers; Channel(T) is generic.
  • Ruststd::sync::mpsc; crossbeam for advanced. Async channels in tokio.
  • 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.

  • HaskellSTM monad; the canonical implementation. atomically, retry, orElse.
  • Clojureref + 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/await is the modern path.
  • C++co_await/co_yield/co_return (C++20).
  • Ruby — Fibers.
  • PHP — Fibers (8.1+).
  • Tclcoroutine.
  • 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@threads for thread-parallel loops; Distributed for cluster; CUDA.jl/AMDGPU.jl for GPU. Multiple dispatch makes parallel libraries highly composable.
  • Rparallel package (mclapply, parLapply); future + furrr for futures-style; data.table parallelizes 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 via Process.
  • Common Lispbordeaux-threads library (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

LanguagePrimary mechanismTools / syntaxWatch out for
Pythonasyncio + threads + multiprocessingasync/await; concurrent.futures; PEP 779 free-threadedGIL (still in non-free-threaded builds); thread-vs-process choice
JavaScriptSingle-threaded event loop + Workersasync/await; Promise; SharedArrayBufferNo shared memory between workers (except SAB)
TypeScript(inherits JS)same as JSsame as JS
JavaThreads + virtual threads (Loom) + j.u.cThread.ofVirtual(); ExecutorService; synchronizedPin-warning for legacy code
CPOSIX threadspthread_create; mutexes; atomicsMemory model subtleties
C++std::thread + coroutinesstd::async, co_awaitMemory ordering; iterator invalidation
[[Languages/csharp|C#]]Task + async + threadsasync/await; Channel<T>; Parallel.ForEachsync-over-async deadlock
GoGoroutines + channels (CSP)go, chan, selectGoroutine leaks; channel deadlocks
RustThreads + async runtimetokio/smol; Send/Sync checkedPinning + 'static constraints
SwiftStructured concurrency + actorsasync/await, Task, actor@MainActor boundaries
KotlinCoroutinessuspend fun, CoroutineScopeCancellation cooperation
RubyThreads (with GVL) + Fibers + RactorsRactor for parallel; Fiber for cooperativeGVL on threads; Ractors limited
PHPFibers (8.1+) + Swoole/AmpFiber API; parallel extensionPer-request scope; ext-pthreads dead
ScalaJVM threads + Akka/Pekko + Cats Effect / ZIOactor or effect-monadMultiple ecosystems coexisting
DartIsolates + Futuresno shared memory between isolatesisolate spawn cost
ElixirBEAM processes (actor)Task, GenServer, SupervisorMailbox growth
HaskellSTM + lightweight threadsatomically, forkIO, MVarAsync exceptions; thunks in MVar
Clojurecore.async (CSP) + STM (refs) + agentsgo, chan, dosync, agent, atomMultiple primitives — pick wisely
[[Languages/fsharp|F#]]Async workflows + Tasksasync {} or task {}F# async vs .NET Task semantics differ
LuaCoroutinescoroutine.create/resume/yieldNo native scheduler
Rparallel + future packagesmclapply, future, furrrRNG seed stability
JuliaThreads + Distributed + TasksThreads.@threads, @spawnType instability blocks parallel speedup
Zigstd.Thread (minimal)atomics; no built-in async runtime as of 0.16Async story in flux
NimThreadpool + spawn + channelsparallel, spawn, ChannelThread-local heaps by default
CrystalFibers + Channelsspawn, ChannelPreview multi-threading
OCamlEffects + domains (multicore, OCaml 5+)domain, effect handlersEffects are still maturing
Perlithreads + AnyEvent / Future::AsyncAwaitCoro (legacy); Future::AsyncAwait (modern)ithreads heavyweight (full interp copy)
ErlangActor modelspawn, !, receive, OTPMailbox copy cost
RacketThreads + custodiansthread, place (parallelism)Custodian-managed resources
Common Lispbordeaux-threads + impl-specificimpl varies (SBCL native threads)Image-level state synchronization
SchemeImpl-specificvariesContinuations make threads tricky
FortranCoarrays + do concurrent + OpenMPcoarray, image, sync allSPMD model; image counting
COBOLNone in-languageCICS task/externalMainframe-environment-specific
AdaTasks + protected objects + rendezvoustask, entry, protectedRavenscar/Jorvik for safety-critical
PascalTThread + critical sectionsTThread, TCriticalSectionPer-impl varies
PrologThreads (SWI) + tablingthread_create; CLP for declarativeBacktracking + threads = nuanced
TclThread package + coroutinesThread, coroutinePer-thread interpreter copy
GroovyJVM threads + GParssimilar to Javasimilar to Java
BashProcess-level only&, wait -n, coprocNo in-process sharing
PowerShellRunspaces + ForEach-Object -Parallel[runspacefactory], Start-JobApartment threading on Windows
SQLEngine-level onlytransactions; isolation levelsIsolation level semantics differ across dialects
Vspawn + channelsspawn fn(), chanPre-1.0 stability
Odincore:thread (explicit)manual; allocator-awareAllocator must be thread-safe
RocEffects via platformplatform-specificApp is pure; platform handles concurrency
GleamBEAM processes (typed actor)gleam_otp; supervisorsTypes add safety on actor model
PonyActors + reference capabilitiesactor, be (behaviors); refcapsCapability typing is the learning cliff
IdrisBackend-dependentChez Scheme threads (default)Concurrency story tied to backend
LeanTasks + IORefTask.spawn; minimalistPure-function-first; concurrency secondary
Coq (Rocq)OCaml runtime threadsminimal language-level supportMostly used for proofs, not runtime
AgdaGHC backend threadsminimalConcurrency rarely the use case
SmalltalkProcess (green threads)[block] fork, semaphoresImage-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 synchronized block (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 (tokio is dominant; smol, async-std are 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 + Domain for 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