Tag: Multithreading

  • Multithreading ≠ Concurrency

    Multithreading ≠ Concurrency

    A live game table explains the difference — and why “just add more threads” is not the answer

    Sometimes we can go back to basics.

    In interviews, design reviews, and late-night debugging, the same words get mixed up: concurrency, parallelism, multithreading. Someone says we need concurrency and the next sentence is “so we’ll make it multithreaded.” Someone else hears “concurrent users” and assumes the server must be full of threads. Another adds “virtual threads” because the JDK version made the slides.

    Those are not the same idea.

    Concurrency is about how you structure work that overlaps in time. Multithreading is one implementation tool. You can be highly concurrent with a single thread. You can run dozens of threads and still get races, starvation, and no real throughput win.

    In this article I separate the terms with a live game table example from high-scale gaming platforms we built — then bridge to the day thread starvation hit us at 100K requests/second on the  Dynamic Real-Time Query Engine and why virtual threads fixed a concurrency cost problem without magically fixing every other one.

    Core concepts. Clear vocabulary. A game you can picture.

    Three words people treat as one.

    TermPlain meaningNot the same as
    ConcurrencyMany tasks in progress — interleaved or overlapping in timeThe same as multithreading
    ParallelismWork truly running at the same time — e.g. multiple tables in the systemAlways being “concurrent by design”
    MultithreadingMultiple threads inside one process as a way to run workA guarantee of correctness or speed

    One line to keep: Concurrency is the problem shape. Parallelism is a performance mode. Multithreading is a mechanism. Don’t say one when you mean another.

    A useful mental model:

    Concurrency = one kitchen has many orders open at once (timers, players, wallets, heartbeats on a single table).

    Parallelism = many kitchens running at once — multiple tables in the system progressing at the same time.

    Multithreading = hiring more cooks for one kitchen — which only helps if they don’t fight over the same knife (shared pot / seats).

    Picture a live game table (or room): several players seated, a round in progress, money on the line, clients connected over a persistent channel.

    One live game table = concurrency — overlapping player inputs, timers, broadcasts, side effects, and heartbeats around shared table state; Design A thread-per-player vs Design B single-owner

    At any moment the server is dealing with overlapping work:

    1. Player inputs — fold, call, raise, buy-in, emoji, reconnect

    2. Timers — turn clock, sit-out, reconnect grace

    3. Broadcasts — seat state, pot, winners to everyone at the table

    4. Side effects — wallet / transaction messages that must not block the game loop forever

    5. Heartbeats / presence — who is still here

    That list is concurrency. The table must make progress on many concerns that are “in flight” together. Whether you use one thread or twenty is a separate design choice.

    What goes wrong if you confuse the words

    Confused sentenceWhat actually happens
    “We need concurrency → add threads per player”Shared table state gets races; seats desync; money bugs
    “More threads = faster gameplay”Context switching + lock contention; latency worse
    “Single-threaded means not concurrent”A well-designed game loop is concurrent work, serialized safely
    “Virtual threads will fix our race conditions”They won’t — they change cost of blocking, not shared-memory safety

    So: use threads where the work is embarrassingly parallel or I/O-bound and isolated. Don’t recruit them as a substitute for a clear concurrency model on shared state.

    When “more classic threads” stopped scaling.

    On the  Dynamic Real-Time Query Engine, a behavioral query often touched multiple models. We mapped the work and ran a path per table with `CompletableFuture` so Cassandra reads overlapped — pay roughly the slowest read, not the sum.

    That is concurrency (many reads in flight) implemented with multithreading / async tasks.

    It worked until traffic climbed toward ~100K requests/second. Latency rose — not because Cassandra was dead, but because of thread starvation. Classic platform threads are scarce. Each concurrent table read wanted an OS thread from a bounded pool. The pool became the bottleneck.

    Java 21 virtual threads changed the cost model of that concurrency: many blocking reads could be in flight without tying up a scarce platform thread each. Starvation eased. Memory and GC pressure rose — we tuned with ZGC. Different problem, still real.

    What that night taught us:

    1. We already had concurrency.

    2. Multithreading (classic) was the implementation that hit a wall.

    3. Virtual threads were a better implementation of the same concurrent fan-out — not a synonym for “we finally added concurrency.”

    4. Measuring told us the bottleneck was the threading model, not “buy more database.”

    Cheat sheet

    If you hear…Ask…Often choose…
    “We need concurrency”What overlaps? What must stay ordered?Event loop / actor / queues or threads — by domain
    “Make it multithreaded”What shared state? Who owns writes?Isolate state; parallelize only independent work
    “Bigger thread pool”Are we CPU-bound, I/O-bound, or lock-bound?Fix contention / ownership first; then size pools
    “Virtual threads”Are we blocked on I/O with huge fan-out?Yes → strong candidate; races → still your problem
    “Single-threaded is slow”Slow where — one core saturated, or waiting on I/O?Measure; don’t assume

    Anti-patterns

    Thread per player mutating the same pot without a clear ownership model

    synchronized everywhere as architecture

    Equating concurrent users (product metric) with multithreading (implementation)

    Assuming more threads ⇒ lower latency under shared locks

    Treating virtual threads as a free pass on backpressure and memory

    Wrapping-Up

    Back to basics:

    Concurrency means many things are in progress — for a live table: inputs, timers, broadcasts, money side-effects.

    Parallelism means work truly runs at the same time — for example, running multiple tables in the system across cores or workers.

    Multithreading is one way to chase either — powerful when work is independent, dangerous when everyone writes the same seat map.

    Design the ownership of state first. Then pick the mechanism: single-threaded game loop, thread pool, actors, virtual threads, or a broker handoff. Name the drawbacks — races, starvation, GC, stalled table loops — so nobody is surprised in production.

    Don’t start with “we’ll multithread it.”

    Start with what must overlap, what must stay ordered, and who owns the write.