Skip to main content
The Thread Synchronization Simulator follows a clean separation between the execution engine (core logic) and the UI layer (visualization). This architecture enables accurate simulation of concurrent behavior while providing rich visual feedback.

High-Level Architecture

┌─────────────────────────────────────────────────────────┐
│                    Browser (index.html)                  │
│                                                           │
│  ┌─────────────────┐         ┌──────────────────────┐  │
│  │   UI Layer      │◄────────┤  Visualization        │  │
│  │   (ui/*.js)     │         │  - Timeline           │  │
│  │                 │         │  - Renderers          │  │
│  │  - ui.js        │         │  - State displays     │  │
│  │  - semaphoreUI  │         └──────────────────────┘  │
│  │  - conditionUI  │                                    │
│  │  - etc.         │                                    │
│  └────────┬────────┘                                    │
│           │                                              │
│           ▼                                              │
│  ┌─────────────────┐         ┌──────────────────────┐  │
│  │  Core Engine    │◄────────┤  Synchronization      │  │
│  │  (core/*.js)    │         │  Primitives           │  │
│  │                 │         │                        │  │
│  │  - Engine       │         │  - Mutex              │  │
│  │  - Thread       │         │  - Semaphore          │  │
│  │  - Instructions │         │  - ConditionVariable  │  │
│  └─────────────────┘         │  - Monitor            │  │
│           ▲                  │  - Barrier            │  │
│           │                  │  - JoinManager        │  │
│           │                  │  - PetersonLock       │  │
│  ┌────────┴────────┐         └──────────────────────┘  │
│  │   Scenarios     │                                    │
│  │   (scenarios/)  │                                    │
│  │                 │                                    │
│  │  - bankScenario │                                    │
│  │  - printerScen. │                                    │
│  │  - etc.         │                                    │
│  └─────────────────┘                                    │
└─────────────────────────────────────────────────────────┘

Engine Layer

The engine provides deterministic execution of concurrent threads through tick-based simulation.

Engine Class

The Engine is the core execution orchestrator:
// From core/engine.js
export class Engine {
  constructor() {
    this.threads = [];  // Lista de hilos registrados
    this.tick = 0;      // Ciclo global del simulador
  }

  addThread(thread) {
    this.threads.push(thread);
  }

  // Ejecuta un ciclo completo sobre todos los hilos
  step(context) {
    this.tick++;
    const logs = [];

    for (const thread of this.threads) {
      // Ignora hilos que no pueden avanzar
      if (thread.state === "blocked" || thread.state === "finished") continue;

      thread.state = "running";
      const inst = thread.currentInstruction();

      if (!inst) {
        thread.state = "finished";
        continue;
      }

      // Ejecuta la instruccion y guarda mensaje para timeline
      const resultLog = this.execute(thread, inst, context);
      if (resultLog) logs.push(resultLog);
    }

    return logs;
  }
}
Key Design Decision: The engine processes all ready threads in a single tick. This simulates concurrent execution while maintaining deterministic, reproducible behavior.

Execution Model

Each step() call:
  1. Increments the global tick counter
  2. Iterates through all registered threads
  3. Skips blocked and finished threads
  4. Executes one instruction per ready thread
  5. Returns log events for UI display
Threads don’t execute in parallel - they execute sequentially within each tick, but the interleaving creates the appearance of concurrency.

Thread Class

Threads are lightweight execution contexts with program counters:
// From core/thread.js
export class Thread {
  constructor(name, instructions) {
    this.name = name;               // Nombre visible en UI y logs
    this.instructions = instructions; // Lista ordenada de instrucciones
    this.pc = 0;                    // Indice de la instruccion actual
    this.state = "ready";           // ready, running, blocked, finished
    this.blockedBy = null;          // Recurso que bloqueo el hilo
  }

  currentInstruction() {
    return this.instructions[this.pc];
  }

  nextInstruction() {
    this.pc++;
  }
}

Thread States

ready

Thread is eligible for execution in the next tick

running

Thread is currently executing (set at start of tick)

blocked

Thread is waiting for a synchronization primitiveblockedBy points to the blocking resource (Mutex, Semaphore, etc.)

finished

Thread has completed all instructions

Instruction Processing

The engine’s execute() method is a large switch statement handling all instruction types:
// From engine.js - example instruction handlers
execute(thread, inst, ctx) {
  const { mutex, account } = ctx;

  switch (inst.type) {
    case Instructions.ACQUIRE:
      if (mutex.acquire(thread)) {
        thread.nextInstruction();
        return `🔑 ${thread.name} entro en Seccion Critica`;
      }
      return `🔒 ${thread.name} esta esperando el Mutex`;

    case Instructions.WITHDRAW:
      const requested = Number(inst.amount) || 0;
      const allowed = Math.min(requested, Math.max(0, account.balance));
      account.balance -= allowed;
      thread.nextInstruction();
      return `💸 ${thread.name} modifico el saldo: -$${allowed}`;

    case Instructions.RELEASE:
      mutex.release(thread);
      thread.nextInstruction();
      return `🔓 ${thread.name} salio y libero el Mutex`;
    
    // ... 20+ other instruction types
  }
}
Each instruction handler returns a human-readable log message (or null). These messages populate the timeline display.

Instruction Outcomes

An instruction can:
  1. Succeed and advance: thread.nextInstruction() is called, PC increments
  2. Block the thread: thread.state = "blocked", PC remains unchanged, thread added to wait queue
  3. Complete execution: thread.state = "finished" when reaching Instructions.END

Synchronization Primitives

Primitives are implemented as independent classes that manage thread queues and state.

Mutex Example

// From core/mutex.js
export class Mutex {
  constructor() {
    this.locked = false;
    this.owner = null;
    this.queue = [];  // Cola FIFO para hilos bloqueados
  }

  acquire(thread) {
    // Transferencia de propiedad: el hilo ya posee el mutex
    if (this.locked && this.owner === thread) {
      return true;
    }

    // Mutex libre: asignacion directa
    if (!this.locked) {
      this.locked = true;
      this.owner = thread;
      return true;
    }

    // Mutex ocupado: bloquear hilo
    thread.state = "blocked";
    thread.blockedBy = this;
    if (!this.queue.includes(thread)) {
      this.queue.push(thread);
    }
    return false;
  }

  release(thread) {
    if (this.owner !== thread) {
      throw new Error("Solo el poseedor puede liberar el mutex");
    }

    if (this.queue.length === 0) {
      this.locked = false;
      this.owner = null;
    } else {
      // Transferencia directa para respetar FIFO
      const nextThread = this.queue.shift();
      this.locked = true;
      nextThread.state = "ready";
      nextThread.blockedBy = null;
      this.owner = nextThread;
    }
  }
}
Critical Implementation Detail: The mutex transfers ownership directly to the next waiting thread without ever becoming “free”. This prevents race conditions in the queue.

UI Layer

The UI layer handles user input, triggers engine execution, and renders state changes.

UI Class

Each algorithm has its own UI controller (e.g., UI for mutex, SemaphoreUI, ConditionUI):
// From ui/ui.js
export const UI = {
  autoInterval: null,
  simulationFinished: false,

  init(engine) {
    const timeline = new Timeline($("timeline"));
    let context = null;

    // Setup event listeners
    $("generateScenario").onclick = () => {
      this.stopAuto();
      this.simulationFinished = false;
      engine.threads = [];

      const num = Number($("threadCount").value) || 3;
      const bal = Number($("initialBalance").value) || 1000;
      const operations = this.getOperationsFromUI(num);
      
      context = createBankScenario(engine, num, operations, bal);
      timeline.clear();
      this.update(engine, context);
    };

    $("stepBtn").onclick = () => {
      this.runTick(engine, context, timeline);
    };

    $("autoBtn").onclick = () => {
      if (this.autoInterval) {
        this.stopAuto();
      } else {
        this.startAuto(engine, context, timeline);
      }
    };
  }
}

Rendering Pipeline

The rendering pipeline transforms engine state into visual output:
┌──────────────┐
│ Engine.step()│
│ returns logs │
└──────┬───────┘


┌─────────────────┐
│ UI.runTick()    │
│ - Adds logs to  │
│   timeline      │
│ - Calls update()│
└──────┬──────────┘


┌─────────────────────────┐
│ UI.update()             │
│ - Updates state panels  │
│ - Updates queues        │
│ - Calls renderer        │
└──────┬──────────────────┘


┌─────────────────────────┐
│ Renderer.renderClients()│
│ - Creates DOM elements  │
│ - Applies styling       │
│ - Animates changes      │
└─────────────────────────┘

State Update Example

// From ui/ui.js
update(engine, context) {
  // Update summary displays
  $("balance").innerText = "$" + context.account.balance;
  $("owner").innerText = context.mutex.owner
    ? context.mutex.owner.name
    : "Libre";

  // Render mutex queue
  clear($("queue"));
  context.mutex.queue.forEach((t) => {
    const node = document.createElement("div");
    node.className = "bg-red-900/50 border border-red-500 ... animate-pulse";
    node.innerText = t.name;
    $("queue").appendChild(node);
  });

  // Render thread cards
  renderClients(
    $("threads"),
    engine.threads,
    context.mutex.owner,
    context.account,
    Number($("initialBalance").value) || 1
  );
}

Renderer Module

Renderers create visual representations of threads and resources:
// From ui/renderer.js
export function renderClients(container, threads, mutexOwner, account, initialBalance) {
  clear(container);

  // Render progress bar for account balance
  const percentage = Math.max(0, (account.balance / initialBalance) * 100);
  const barColor = percentage > 50 ? "bg-green-500"
    : percentage > 20 ? "bg-yellow-500" : "bg-red-500";

  // ... (create progress bar DOM)

  // Render thread cards
  threads.forEach((thread) => {
    const card = document.createElement("div");
    let border = "border-gray-600";
    let icon = "👤";

    if (thread.state === "blocked") {
      border = "border-red-500 shadow-[0_0_10px_rgba(239,68,68,0.5)]";
      icon = "🔒";
    } else if (thread.state === "finished") {
      card.style.opacity = "0.4";
      icon = "✅";
    } else if (mutexOwner === thread) {
      border = "border-yellow-400 shadow-[0_0_15px_rgba(250,204,21,0.6)]";
      icon = "💰";
    }

    card.className = `... border-2 ${border} transition-all duration-300 ...`;
    card.innerHTML = `
      <div class="text-3xl">${icon}</div>
      <div class="font-bold">${thread.name}</div>
      <div class="... uppercase">${thread.state}</div>
    `;
    container.appendChild(card);
  });
}
The renderer uses Tailwind CSS for styling and applies visual effects like glowing borders and pulsing animations to indicate thread states.

Scenario System

Scenarios encapsulate initial state and thread configurations:
// From scenarios/bankScenario.js
export function createBankScenario(engine, threadCount, operations, initialBalance) {
  const mutex = new Mutex();
  const account = { balance: initialBalance };

  for (let i = 1; i <= threadCount; i++) {
    const operation = operations[i - 1] ?? {
      type: Instructions.WITHDRAW,
      amount: 100,
    };
    const amount = Math.max(0, Number(operation.amount) || 0);

    // Flujo: entrar, operar, salir, terminar
    const instructions = [
      { type: Instructions.ACQUIRE },
      { type: operation.type, amount },
      { type: Instructions.RELEASE },
      { type: Instructions.END },
    ];

    const thread = new Thread(`Cliente-${i}`, instructions);
    engine.addThread(thread);
  }

  return { mutex, account };
}
Scenarios return a context object containing:
  • Synchronization primitives (mutex, semaphore, etc.)
  • Shared state (account, catalog, etc.)
  • Algorithm-specific resources (printers, queues, etc.)
The context object is passed to engine.step() on every tick, allowing instructions to access and modify shared state.

Module Organization

js/
├── core/               # Core execution engine
│   ├── engine.js       # Main execution loop
│   ├── thread.js       # Thread model
│   ├── instructions.js # Instruction type constants
│   ├── mutex.js        # Mutex implementation
│   ├── semaphore.js    # Semaphore implementation
│   ├── conditionVariable.js
│   ├── monitorLibrary.js
│   ├── barrier.js
│   ├── joinManager.js
│   └── petersonLock.js
├── scenarios/          # Scenario generators
│   ├── bankScenario.js
│   ├── printerScenario.js
│   ├── restaurantScenario.js
│   ├── libraryScenario.js
│   ├── raceBarrierScenario.js
│   ├── houseScenario.js
│   └── petersonScenario.js
├── ui/                 # UI controllers and renderers
│   ├── ui.js           # Mutex UI
│   ├── semaphoreUI.js
│   ├── conditionUI.js
│   ├── monitorUI.js
│   ├── barrierUI.js
│   ├── joinUI.js
│   ├── petersonUI.js
│   ├── renderer.js     # Mutex renderer
│   ├── semaphoreRenderer.js
│   ├── conditionRenderer.js
│   ├── monitorRenderer.js
│   ├── barrierRenderer.js
│   ├── joinRenderer.js
│   ├── petersonRenderer.js
│   └── timeline.js     # Timeline display
├── utils/
│   └── dom.js          # DOM helpers
└── main.js             # Entry point

Execution Flow Example

Here’s a complete execution flow for a mutex scenario:
1

Initialization

User clicks “Generar escenario”
// UI creates scenario
context = createBankScenario(engine, 3, operations, 1000);
// context = { mutex, account }
// engine.threads = [Cliente-1, Cliente-2, Cliente-3]
2

First tick

User clicks “Paso” → UI.runTick()engine.step(context)
// Engine iterates threads
// Cliente-1: state=ready, pc=0, inst=ACQUIRE
//   → mutex.acquire(Cliente-1) succeeds
//   → Cliente-1.pc++ (now 1)
//   → Returns log: "🔑 Cliente-1 entro en Seccion Critica"

// Cliente-2: state=ready, pc=0, inst=ACQUIRE
//   → mutex.acquire(Cliente-2) fails (mutex owned by Cliente-1)
//   → Cliente-2.state = "blocked"
//   → mutex.queue.push(Cliente-2)
//   → Returns log: "🔒 Cliente-2 esta esperando el Mutex"

// Cliente-3: similar to Cliente-2
3

UI Update

// Logs added to timeline
timeline.addEvent("🔑 Cliente-1 entro en Seccion Critica");
timeline.addEvent("🔒 Cliente-2 esta esperando el Mutex");

// UI state refreshed
UI.update(engine, context);
// Displays: owner="Cliente-1", queue=[Cliente-2, Cliente-3]
4

Subsequent ticks

// Tick 2: Cliente-1 executes WITHDRAW
// Tick 3: Cliente-1 executes RELEASE
//   → mutex.release() transfers ownership to Cliente-2
//   → Cliente-2.state = "ready", Cliente-2 removed from queue

// Tick 4: Cliente-2 executes its operation
// ...

Design Principles

Separation of Concerns

Engine handles execution logic, UI handles presentation. They communicate through the context object and return values.

Deterministic Execution

Same inputs always produce same outputs. No race conditions in the simulator itself.

Extensibility

New synchronization primitives can be added by:
  1. Creating a new primitive class in core/
  2. Adding instruction types to Instructions
  3. Adding handlers to engine.execute()
  4. Creating a scenario and UI module

Educational Focus

Rich visual feedback and detailed logging help students understand concurrency concepts.

Performance Characteristics

  • Tick execution: O(n) where n = number of threads
  • DOM updates: Throttled to user interaction or 1-second auto intervals
  • Memory: Minimal - only active threads and event logs stored
  • Browser support: Modern browsers with ES6 module support
The simulator is designed for educational scenarios (3-10 threads). It’s not optimized for large-scale simulations.

Next Steps

Quickstart

Run your first simulation

Extend the Simulator

Add new synchronization primitives or scenarios by following the existing patterns