Synchronization · Correctness · System Tradeoffs

Concurrent
Pipeline Lab

A deterministic simulator for a five-stage processing pipeline. Same workload, three synchronization strategies — semaphores, monitors, and message passing — and meaningfully different system behavior.

Pipeline Ingress → Parse → Validate → Execute → Persist
Models Semaphores · Monitors · Message Passing (sync + async)
Engine Tick-based discrete simulation · seeded PRNG · deterministic replay
Course McMaster COMPSCI 3SH3 — Concurrent Systems

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.

What this demonstrates
  • Producer-consumer coordination with bounded buffers
  • Mutual exclusion and condition synchronization
  • Backpressure propagation through a pipeline
  • Safety, liveness, and fairness under load
  • How synchronization primitives change system reasoning

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.

The key difference

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

Tick 0
Speed
Arrival rate 0.30
Metrics
Correctness & Safety
Event trace
No events yet — start the simulation

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.

Why this matters

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

  • Queue overflow — when arrival rate exceeds throughput, upstream queues fill and tasks are dropped. Try the "Overloaded" preset.
  • Backpressure cascade — a slow downstream stage blocks upstream workers, which fills upstream queues, which blocks even further upstream. The entire pipeline stalls from one bottleneck.
  • Starvation — with unbalanced workers, some stages are permanently under-resourced while others sit idle. Throughput collapses even though the system has total capacity.
  • Synchronous channel coupling — sync channels eliminate buffering, forcing every stage to run at the speed of the slowest. Any variation in service time creates blocking waves.
  • Tiny buffer amplification — very small queues amplify backpressure. A momentary service time spike has nowhere to absorb, so it immediately blocks the producer.
The lesson

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

  • Semaphores are powerful but low-level. They can implement any coordination pattern, but the programmer carries the full burden of correctness. One misplaced signal breaks everything, and the bug is invisible until it isn't.
  • Monitors improve modular reasoning. By encapsulating synchronization inside data structures, monitors let you verify correctness locally. The interface is cleaner, the invariants are easier to state, and the risk of scattered signal/wait errors drops.
  • Message passing changes ownership semantics. When stages communicate through channels instead of shared memory, you reason about communication topology instead of shared state. Sync channels force rendez-vous, which is restrictive but eliminates entire categories of coordination bugs.
  • Correctness is about invariants, not individual runs. A system that "seems to work" under testing can fail under different timing. Deterministic simulation with reproducible seeds is a better foundation for reasoning than ad-hoc stress tests.
  • Buffering is a design choice, not a default. Queue capacity directly affects system behavior under load. Too much buffering hides latency. Too little amplifies backpressure. The right amount depends on the workload and the coordination model.

What I'd improve next

  • Probabilistic service times — add variance to processing times, exposing how each model handles jitter differently.
  • Priority scheduling — implement priority queues and show how priority inversion manifests under different synchronization strategies.
  • Replayable traces — export full event traces as JSON for offline analysis and comparison between runs.
  • Formal invariant specification — express safety properties as temporal logic formulas and machine-check them against the trace.
  • Deadlock detection — implement cycle detection on the wait-for graph to identify actual deadlocks, not just suspected ones.
On scope

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.