Concurrency Gaps: Lessons from Building a Real Language
The rest of this section teaches concurrency through Java's standard toolbelt. This page is a companion from the other direction: what actually goes wrong when a language's concurrency support is incomplete, drawn from real gaps found (and worked around, not always fixed) while building PatLang — a language whose interpreter, compiler, and standard library were all written from scratch, including its threading support.
The bug that happened three times: a registry that forgot about other threads
PatLang's event-handler registry (backing its when/emit mechanism) was originally stored per-thread — a completely reasonable choice for a single-threaded interpreter, and completely wrong the moment real OS threads arrived. Every new thread — a fiber, a parallel_map worker — started with its own fresh, empty copy of the registry, so a handler registered on the main thread was invisible to emit(...) called from anywhere else. Nothing crashed. Nothing errored. The handler simply never ran, silently, which is the worst kind of concurrency bug: no exception to catch, no stack trace to read, just an event that should have done something and quietly did nothing.
The fix — a single shared registry behind a mutex instead of one copy per thread — is the standard answer, and it's worth being precise about what it actually buys you: it guarantees every thread sees the same registry, and it guarantees two threads registering or firing a handler at the same instant don't corrupt the registry itself. It does not make the handler's own code thread-safe, and it does not add any ordering guarantee beyond "the registry itself won't tear." A shared, mutex-protected registry is a fix for visibility, not a fix for every concurrency problem a handler's body might still have.
What makes this worth a whole section rather than one paragraph: the identical bug — a store that should be shared across threads but was accidentally per-thread — was found and fixed a second time in the object store (the mechanism backing every PatLang object's state), on a different day, in a different feature. Same root cause, same fix, found independently because nobody had generalized the lesson from the first occurrence into "audit every shared store for this," only fixed it where it had already actually broken something.
A structural gap, not a race: workers that never saw the setup code
A more interesting failure mode than a data race is a completeness gap: a function called via parallel_map runs on a worker thread with a brand-new interpreter that jumps straight into that one function. It never executes the program's own top-level setup code first. Reference what looks like an ordinary constant — a value assigned once, near the top of the file, exactly the way you'd write a constant in any other language — from inside a function that might get called by a worker, and it silently resolves to nothing. Not an error. Not a crash. An empty value, quietly wrong.
This has not been fixed at the root. It has been worked around: every library written with this constraint in mind avoids referencing anything that looks like a top-level constant from inside a function, replacing it with either a small getter function (called fresh each time, so it works identically whether the caller is the main thread or a worker) or an explicit parameter. That workaround is safe for as long as the discipline holds — every function written this way behaves correctly regardless of which thread calls it — but it is a discipline, not a guarantee the language enforces. Nothing stops a future function from quietly reintroducing the exact same mistake, because the language doesn't refuse to compile the broken pattern; it just silently produces an empty value at runtime, on whichever thread happens to trigger it.
When the honest answer is "we don't have real concurrency control here"
Building a small transactional database on top of this language surfaced the cleanest example of a decision every concurrent system eventually has to make: what do you do when you genuinely don't have the primitive you'd need to solve a problem properly? PatLang has no real lock. Building a database with multiple concurrent writers safely requires one. Rather than fake a solution — a "lock" that doesn't actually exclude anyone, or an optimistic scheme with no real conflict detection behind it — the database was built single-writer, on purpose, and said so out loud in the code: exactly one transaction can be open at a time, full stop.
That's not a cop-out; it's the right answer to a question that doesn't have a good one otherwise. But it only stays safe as long as the assumption behind it stays true. The whole design — staging writes separately, then swapping them in wholesale on commit — is safe specifically because there is never more than one writer touching that staging area at once. The same code, reused naively to support multiple simultaneous writers without adding real conflict detection first, would silently stop being safe the moment two transactions actually overlapped — nothing in the code itself would announce that the assumption had been violated.
Lessons, generalized
- A store that "should" be shared across threads and isn't will fail silently, not loudly. No exception, no crash — just a handler, a value, or a piece of state that quietly doesn't do what it should. Audit every shared store the moment real threading is added to a system that didn't originally have it, rather than waiting for each one to break independently.
- Sharing something across threads (a mutex-protected registry) and making it individually thread-safe (the code that runs when you touch it) are two different guarantees. Fixing the first doesn't give you the second for free.
- A workaround is only as safe as the assumption it silently depends on. If a "fix" is really a discipline someone has to keep following rather than something the system enforces, write the assumption down as clearly as the workaround itself — ideally somewhere a future change is likely to actually read it.
- If you don't have the right primitive, say so and design around the gap rather than fake a fix. Single-writer, clearly stated, is safer than a lock that doesn't really lock.
See also
The Concurrent Toolbelt and The Shared Mutability Disaster for the general vocabulary and standard-library answers to these problems in a language that actually has them. Risk Management: Lessons from Testing Real Systems for the same project's broader testing story, including how the single-writer database decision was risk-scored explicitly rather than left implicit.