Concurrency: A Thesis
Why SuperJ has no threads, and why that is the correct answer, not a limitation.
TL;DR
Shared-state concurrency — multiple execution contexts mutating common memory — is an engineering style with no correctness floor. It cannot be proven free of data races and deadlocks for any non-trivial program; the second of those is mathematically undecidable. The industry has spent two decades producing broken "thread-safe" code in every language that offers it, and the borrow checker, for all its power, narrows the failure surface without removing the fundamental reason concurrency defeats human reasoning.
SuperJ makes a different bet: don't share state. Within a process there is one thread, one event loop, one causal story. Across processes, state is shared only through a totally ordered event stream — the SEDA sequencer — which makes consistency a property of the event sequence, not the scheduler. This is not a workaround for missing threads. It is a thesis about how systems should be built, grounded in three results that are provably true.
1. Threads and processes are the same thing to the OS
At the kernel there is no categorical distinction. A Mach thread and a Mach task's main thread are both threads; a Linux task is a task. The OS schedules execution contexts; "thread" vs "process" is a userspace story about whether the contexts share an address space.
That sharing is not a performance feature — it is the origin of the entire class of bugs people call "concurrency issues." Choosing processes over threads does not give up concurrency (the OS gives you that either way); it removes the footgun. The performance cost of separate address spaces — cross-process IPC — is real but bounded, and for the workloads SuperJ targets (mmap-backed queues) it is sub-microsecond and deterministic.
The common claim "threads are cheaper than processes" is true only for context switch cost, which is not the bottleneck in a correctly-designed event system. The bottleneck is cache coherence across the cores running the shared state, and that cost is paid by threads, not avoided by them.
2. Deadlock freedom is undecidable
This is not an opinion. The general problem of deciding whether an arbitrary concurrent program can reach a deadlocked state reduces to the halting problem: you can encode a Turing machine's termination as a resource-acquisition pattern, so an algorithm that proved deadlock-freedom would decide halting. The proof is folklore in the concurrency-theory community and is why no practical tool attempts it.
Consequence: no static check, no language feature, no type system can give you deadlock freedom. You can get narrower failure modes — Rust's Send/Sync prove the absence of data races, which is a strictly weaker, syntactic property (it is about memory access patterns, statically checkable). But "fearless concurrency" is not "fearless correctness." You get one narrow guarantee and trade away everything else: deadlocks, livelocks, priority inversion, lost wakeups, and — critically — determinism, which no shared-state system provides.
This is why the empirical record is what it is. Twenty years of Java, C#, Go, and C++ have produced a corpus of deadlock-prone, race-prone, nondeterministically-broken "thread-safe" code, written by competent people, in languages far more ergonomic than C. The borrow checker narrows the failure mode; it does not change the reason concurrency defeats human reasoning, which is that correctness depends on global interleavings the human never wrote down.
3. The only tractable shared state is a total order
If you cannot share mutable state safely, the only way to coordinate is to not share it — to make every piece of state owned by exactly one execution context, and to propagate changes as an ordered stream of events. This is the LMAX Disruptor insight generalized, and it is what SEDA implements:
- Each Application owns its state. No other context touches it.
- Applications communicate by publishing commands to a queue they alone write.
- A single Sequencer drains all command queues, assigns a monotonic global
seq, stamps a wall-clock timestamp, and writes the result to one event queue. - Every Application reads the same event queue with its own cursor and sees the same total order.
Correctness is now a property of the event sequence, not the scheduler. There is no interleaving to reason about because there is no concurrency inside the ordering boundary. Replay from seq 0 reproduces identical state, bit-for-bit, because the state transition function is pure with respect to the event stream. This is the property shared-state systems cannot offer at any cost: determinism.
4. The latency/consistency trade is fundamental
Imposing a total order has a cost, and that cost is not negotiable. Two impossibility results bracket it:
- FLP (Fischer-Lynch-Paterson, 1985). In an asynchronous network with even one faulty process, deterministic consensus is impossible — no protocol always terminates. You cannot elect a leader, agree on a value, or install a total order distributedly with both safety and liveness.
- CAP. Consistency + availability + partition tolerance: pick two, and on a real network you cannot surrender partition tolerance.
SEDA resolves this by not attempting distributed consensus. The Sequencer is the ordering authority by construction — a single serialized point — which is the CAP choice of consistency over availability made explicit and local. The trade you pay:
- Latency. Every event traverses the Sequencer, so round-trip latency = Sequencer hop + queue depth. SEDA's measured 137 ns/round (M4 Max, in-process) shows this cost is small in absolute terms but it is nonzero and it is architecturally irreducible.
- Single point of failure. If the Sequencer process dies, ordering stops. This is accepted: the event queue is the journal, and restart replays from
seq 0. Availability is recovered by restart, not by distributed failover — which FLP says you cannot have safely anyway.
There is no architectural trick that avoids this. Any system that maintains a consistent global order pays the serialization cost somewhere, and FLP says you cannot fake it with cleverness. The industry keeps trying (lock-free structures, RCU, eventual consistency, CRDTs) and keeps rediscovering that the trade is fundamental. SEDA pays the cost upfront, in one visible place, rather than spreading it across thousands of lock acquisitions and coherence misses hidden inside "concurrent" code.
5. Why SEDA is faster than raw cross-CPU mmap
This SEDA-vs-mmap benchmark is not a curiosity. SEDA in-process measured 137 ns/round; raw cross-process mmap spin measured 152 ns/round — SEDA beat the thing it sits on top of. That result is the empirical signature of the thesis.
The reason is cache coherence. Two processes spinning on a shared mmap line pay cross-CPU invalidation traffic on every flag flip — the line ping-pongs between cores, and each bounce is tens to hundreds of nanoseconds of unavoidable latency. SEDA in-process never crosses cores: the Sequencer and the Applications share a cache hierarchy, the event queue is hot in L1/L2, and there is no coherence protocol to wait on.
The "concurrent" path (raw mmap) is paying the coherence tax not because it needs to but because the architecture forced two processes to coordinate via shared memory. Remove the architecture, and the cost disappears. This is the deep point: most of the overhead people associate with "concurrency" is not inherent to doing work in parallel — it is inherent to sharing state while doing work in parallel. Stop sharing, and the overhead vanishes.
6. What you give up, honestly
This thesis is not a claim that SuperJ can do everything Rust can. It is a claim about a useful and large subset, and the boundaries are real:
- No intra-process parallelism. A CPU-bound task that genuinely needs 8 cores must be 8 processes, not 8 threads. This changes deployment shape (more processes, IPC between them) but not correctness. For the workloads SuperJ targets — event-driven servers, data pipelines, parsers, network hot paths — one core per process is the right grain, and SEDA makes cross-process coordination cheap.
- No compile-time memory safety. Arena allocation gives deterministic reclamation, not static safety. A use-after-arena-drop is a runtime crash, not a compile error. Rust's value is that its speed comes with a proof; SuperJ's value is that its speed comes without the proof's ergonomic cost. Which trade wins depends on what you are building and who is building it.
- No general-purpose generic abstraction across arbitrary types. Rust's monomorphized traits give zero-cost abstraction over user-defined types. SuperJ's erased generics + vtable-free concrete dispatch cover the SDK's needs but would not carry a general-purpose library ecosystem the way Rust's do.
These are permanent by design, not TODOs. They are the price of the determinism and ergonomics bet.
7. The synthesis
The industry framing "SuperJ lacks concurrency" is wrong. SuperJ has a deliberately different concurrency model that trades raw intra-process parallelism for three properties no shared-state system provides:
- Determinism. Same inputs → same event sequence → same state. Replay is bit-for-bit. Bugs reproduce. Tests are not timing-dependent.
- Reasonability. A single competent engineer can hold the whole system in their head because correctness is local to one thread and one event order, not global across interleavings.
- Low latency without coherence tax. No locks in the hot path, no atomics in user code, no cross-CPU cache ping-pong. The SEDA numbers prove the "concurrent" baseline was paying for coherence it didn't need.
The bet is that for the large class of systems where the bottleneck is correctness and tail latency rather than raw core count, this is the better architecture. The impossibility results — undecidable deadlock freedom, FLP, CAP — say the trade is not avoidable; you pay it somewhere. SuperJ pays it upfront, in one visible, replayable place, rather than distributed across thousands of hidden lock acquisitions.
That is the thesis. It is not that threads are bad and events are good. It is that shared mutable state is the wrong primitive, and total-order event systems are the right one — and the math says you cannot have both the ergonomics of one and the guarantees of the other.