Skip to main content

Overview

A Mutex (mutual exclusion lock) is a binary synchronization primitive that ensures only one thread can access a critical section at a time. It’s the most fundamental building block for protecting shared resources from race conditions.

When to Use Mutex

Critical Sections

Protecting code sections where only one thread should execute at a time

Shared Resources

Guarding access to shared data structures, files, or hardware

State Consistency

Ensuring atomic updates to shared state (e.g., bank account balance)

Binary Access

When exactly one thread needs exclusive access (unlike semaphores with N>1)

How It Works

A mutex operates as a binary lock with two states:
  • Locked: A thread owns the critical section
  • Unlocked: The critical section is available

Key Properties

1

Ownership

Only the thread that acquired the mutex can release it
2

FIFO Queue

Blocked threads wait in a first-in-first-out queue for fairness
3

Direct Transfer

On release, ownership transfers directly to the next waiting thread without unlocking

Implementation

Here’s the complete mutex implementation from the simulator:
source/js/core/mutex.js
// Mutex binario con cola FIFO para hilos bloqueados.
export class Mutex {
  constructor() {
    this.locked = false; // true cuando la seccion critica esta tomada.
    this.owner = null; // Hilo que tiene el mutex actualmente.
    this.queue = []; // Cola de espera en orden de llegada.
  }

  // Intenta adquirir el mutex para un hilo.
  acquire(thread) {
    // Si release transfirio propiedad, este hilo puede continuar.
    if (this.locked && this.owner === thread) {
      return true;
    }

    // Camino normal: mutex libre, se asigna al hilo.
    if (!this.locked) {
      this.locked = true;
      this.owner = thread;
      return true;
    }

    // Mutex ocupado: hilo bloqueado y enviado a cola.
    thread.state = "blocked";
    thread.blockedBy = this;
    // Evita duplicados cuando el hilo vuelve a intentar acquire.
    if (!this.queue.includes(thread)) {
      this.queue.push(thread);
    }
    return false;
  }

  // Libera el mutex y, si existe cola, lo transfiere al siguiente hilo.
  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 sin dejar el mutex libre.
      const nextThread = this.queue.shift();
      this.locked = true;
      nextThread.state = "ready";
      nextThread.blockedBy = null;
      this.owner = nextThread;
    }
  }
}

Acquire Operation

The acquire() method handles three cases:
When a thread was previously granted ownership by release(), it can continue immediately:
if (this.locked && this.owner === thread) {
  return true;
}
If the mutex is free, the thread takes ownership:
if (!this.locked) {
  this.locked = true;
  this.owner = thread;
  return true;
}
If another thread owns the mutex, the requesting thread blocks and enters the FIFO queue:
thread.state = "blocked";
thread.blockedBy = this;
if (!this.queue.includes(thread)) {
  this.queue.push(thread);
}
return false;

Release Operation

The release() method implements direct ownership transfer to maintain FIFO fairness:
if (this.queue.length === 0) {
  this.locked = false;
  this.owner = null;
}
Why direct transfer? By transferring ownership without unlocking, we guarantee the next thread in the queue gets the mutex. If we unlocked first, a newly arriving thread could “cut in line” and violate FIFO ordering.

FIFO Queue Management

The mutex maintains strict FIFO ordering: Key insight: The queue uses shift() to remove from the front and push() to add to the back, implementing perfect FIFO behavior.

Usage Example: Bank Account

The bank scenario demonstrates mutex protecting a shared account balance:
import { Mutex } from "../core/mutex.js";
import { Thread } from "../core/thread.js";
import { Instructions } from "../core/instructions.js";

const mutex = new Mutex();
const account = { balance: 1000 };

// Create threads for withdrawals/deposits
for (let i = 1; i <= threadCount; i++) {
  const instructions = [
    { type: Instructions.ACQUIRE },
    { type: Instructions.WITHDRAW, amount: 100 },
    { type: Instructions.RELEASE },
    { type: Instructions.END },
  ];
  
  const thread = new Thread(`Cliente-${i}`, instructions);
  engine.addThread(thread);
}

Common Pitfalls

Deadlock: If a thread acquires a mutex but never releases it (e.g., due to an exception), all other threads will block forever.
Wrong Owner: Only the thread that acquired the mutex can release it. The implementation throws an error if a different thread tries to release:
if (this.owner !== thread) {
  throw new Error("Solo el poseedor puede liberar el mutex");
}
Priority Inversion: High-priority threads may be blocked waiting for a low-priority thread to release the mutex.

Visualizing Mutex Behavior

In the simulator, you can see:
  • Green lock icon: Mutex is free
  • Red lock icon: Mutex is locked
  • Owner name: Which thread currently owns the mutex
  • Queue display: Threads waiting in FIFO order

Bank Account

Multiple clients accessing a shared account balance

Restaurant Kitchen

Chefs competing for access to the shared cooking station

Key Takeaways

1

Binary Lock

Mutex provides exclusive access - only one thread at a time
2

FIFO Fairness

Blocked threads are served in arrival order, preventing starvation
3

Direct Transfer

Ownership moves directly from releasing thread to next waiter
4

Ownership Model

Only the acquiring thread can release the mutex

See Also