Synchronization · Correctness · System Tradeoffs
A deterministic simulator for a five-stage processing pipeline. Same workload, three synchronization strategies — semaphores, monitors, and message passing — and meaningfully different system behavior.
Overview
A multi-stage processing pipeline is a natural concurrency problem. Tasks arrive, pass through independent stages, and compete for shared resources — bounded queues, limited workers, finite capacity. The challenge isn't parallelism for speed. It's coordination without losing data, starving workers, or deadlocking the system.
This project simulates the same five-stage pipeline under three different synchronization strategies. Each model uses different primitives to solve the same coordination problem — and each produces different behavior under pressure. The simulation is deterministic, step-able, and designed to make the consequences of each design choice visible.
Why I built this
In my concurrent systems course, we study semaphores, monitors, and channels as solutions to the same underlying coordination problem. But it's hard to build intuition for how these models behave under pressure — where they differ, where they fail, and why one abstracts better than another. Textbook definitions explain the mechanism. They don't show you what happens when a queue fills up, a worker stalls, and backpressure cascades upstream.
I built this simulator so I could run the same pipeline under different models and actually see the consequences: where blocking propagates, how queue pressure builds, and what correctness looks like in a system that's genuinely under load. The goal is not animation — it's a faithful, deterministic model of concurrent behavior that I can step through, reason about, and explain.
Three synchronization strategies
Semaphores use counting variables to coordinate access to bounded buffers. Each queue has an empty count (available slots) and a full count (available items). Producers wait on empty, consumers wait on full. It works, but the reasoning is fragile — the programmer must manage multiple semaphores correctly, and a signal/wait ordering mistake can deadlock the system.
Monitors encapsulate shared state inside a thread-safe module. Instead of raw semaphores, the queue exposes put() and take() operations that internally manage condition variables (notFull, notEmpty). The abstraction is cleaner: coordination logic lives inside the data structure, not scattered across producer and consumer code.
Message passing replaces shared memory with channels. Stages don't access shared queues — they send and receive through bounded (or synchronous) channels. Synchronous channels force rendez-vous: the sender blocks until a receiver is ready, creating tight coupling and zero buffering. This changes system behavior fundamentally.
Semaphores, monitors, and channels are not just three names for the same thing. They change how you reason about the system. Semaphores force you to think about counts. Monitors let you think about module interfaces. Channels let you think about process isolation and communication topology. The simulation makes these differences tangible by showing how each model responds to the same overload scenario.
Interactive Simulator
Correctness conditions
Safety means nothing bad happens. In this pipeline: queue capacity is never exceeded, no task is processed by two workers simultaneously, tasks visit stages in pipeline order, and no task is silently lost between stages. If any of these are violated, the system has a bug.
Liveness means something good eventually happens. Under stable load (arrival rate below system throughput), every accepted task should eventually complete. No worker should remain blocked indefinitely if the system has capacity. If liveness fails, the system may be deadlocked or starved.
Fairness means no task or worker waits indefinitely while others make progress. FIFO ordering within each queue ensures arrival-order fairness. Balanced worker utilization within a stage indicates scheduling fairness. The correctness panel in the simulator tracks all three properties in real time.
Correctness in concurrent systems is not "it ran once without crashing." It's about invariants that hold across all interleavings. The simulator is deterministic, which means every run is reproducible — but the interesting question is whether the invariants hold by design, not by accident. The correctness panel makes this visible.
Failure modes & tradeoffs
Most concurrency bugs don't happen because the code is wrong in isolation. They happen because the interaction between components creates emergent behavior that no single component can prevent. A system that works under low load can collapse under high load — not from a bug, but from a design tradeoff that only matters at scale. The presets in the simulator are designed to make these collapse modes visible.
What I learned
What I'd improve next
I intentionally kept this project focused. The goal was a clear, correct, explainable simulation — not a feature-complete distributed systems framework. Every extension above would add value, but shipping a coherent system I fully understand matters more than a larger one I don't.