A Semaphore is a counting synchronization primitive that controls access to a limited pool of resources. Unlike a binary mutex, a semaphore maintains a counter that allows multiple threads to access the resource simultaneously (up to the counter limit).
Decrements count if > 0 and assigns a resource. If count = 0, thread blocks in FIFO queue.
2
signal() - Release Resource
Releases the resource. If threads are waiting, transfers resource directly to the next in queue. Otherwise, increments count.
Semaphore vs Mutex: A mutex is essentially a binary semaphore (count = 1) with ownership semantics. Semaphores allow count > 1 and don’t enforce “only owner can release” rules.
Here’s the complete semaphore implementation from the simulator:
source/js/core/semaphore.js
// Semaforo contador con cola FIFO y asignacion de recurso (impresoras).export class Semaphore { constructor(initialCount) { // count representa cuantos hilos mas pueden entrar a "zona de impresion". this.count = Math.max(0, Number(initialCount) || 0); // Cola FIFO para respetar orden de llegada cuando se bloquean. this.queue = []; // Mapeo para recordar que impresora tiene cada hilo. this.assignments = new Map(); } // Intenta tomar un token y una impresora libre. wait(thread, printers) { // Si el hilo ya tiene recurso asignado por transferencia, puede continuar. if (this.assignments.has(thread)) { // Devuelvo la misma impresora para que el hilo avance sin bloquearse de nuevo. return this.assignments.get(thread); } // Solo puedo asignar si hay tokens libres. if (this.count > 0) { // Busco primera impresora sin dueño. const printerIndex = printers.findIndex((printer) => !printer.owner); if (printerIndex === -1) return null; // Consumo token y asigno impresora al hilo. this.count--; this.assignments.set(thread, printerIndex); printers[printerIndex].owner = thread; return printerIndex; } // No hay token: se bloquea en cola. thread.state = "blocked"; thread.blockedBy = this; if (!this.queue.includes(thread)) this.queue.push(thread); return null; } // Libera token/impresora y transfiere al siguiente hilo si existe. signal(thread, printers) { if (!this.assignments.has(thread)) { // Esto ayuda a detectar bugs: signal sin haber hecho wait. throw new Error("El hilo no tiene recurso asignado para signal"); } // Obtengo impresora actual para liberarla. const releasedPrinter = this.assignments.get(thread); this.assignments.delete(thread); printers[releasedPrinter].owner = null; // Si hay cola, hago transferencia directa de la misma impresora. if (this.queue.length > 0) { const nextThread = this.queue.shift(); // El siguiente pasa de bloqueado a listo. nextThread.state = "ready"; nextThread.blockedBy = null; // Asigno impresora al siguiente sin subir count. this.assignments.set(nextThread, releasedPrinter); printers[releasedPrinter].owner = nextThread; return { releasedPrinter, nextThread }; } // Si nadie espera, entonces si regreso token al semaforo. this.count++; return { releasedPrinter, nextThread: null }; } // Devuelve el indice de impresora del hilo, o null si no tiene. getAssignedPrinter(thread) { return this.assignments.has(thread) ? this.assignments.get(thread) : null; }}
The signal() method implements direct resource transfer:
if (this.queue.length > 0) { const nextThread = this.queue.shift(); nextThread.state = "ready"; nextThread.blockedBy = null; // Transfer the same resource this.assignments.set(nextThread, releasedPrinter); printers[releasedPrinter].owner = nextThread; return { releasedPrinter, nextThread };}
Direct Transfer Strategy: Like the mutex, the semaphore transfers resources directly to waiting threads without incrementing the count. This maintains FIFO fairness and prevents “line cutting.”
Let’s trace how the count changes with 3 printers and 5 jobs:Key observation: The count never goes above the initial value (3) until all waiting threads are served.
The semaphore maintains strict FIFO ordering for fairness:
// Add to back of queueif (!this.queue.includes(thread)) { this.queue.push(thread);}// Remove from front of queueconst nextThread = this.queue.shift();
This ensures:
Fairness: Threads are served in arrival order
No starvation: Every waiting thread will eventually get a resource
Signal without Wait: Calling signal() without a corresponding wait() throws an error:
if (!this.assignments.has(thread)) { throw new Error("El hilo no tiene recurso asignado para signal");}
Deadlock: If threads acquire resources and never release them, eventually all resources are exhausted and remaining threads block forever.
Count Overflow: In some implementations, calling signal() too many times can increment count beyond the initial value, creating “phantom resources.” This implementation prevents that with direct transfer.
Semaphores are commonly used in pairs for producer-consumer synchronization:
const empty = new Semaphore(bufferSize); // Available slotsconst full = new Semaphore(0); // Available items// Producerempty.wait(); // Wait for empty slotbuffer.push(item);full.signal(); // Signal item available// Consumerfull.wait(); // Wait for itemconst item = buffer.pop();empty.signal(); // Signal slot available