ConcurrencyTypeScriptverifiedVerified
Mutex / Lock Pattern in TypeScript
Guarantee that only one thread at a time can access a shared resource by requiring threads to acquire an exclusive lock before proceeding.
How to Implement the Mutex / Lock Pattern in TypeScript
1Step 1: Implement the Mutex with acquire and release
class Mutex {
private locked = false;
private queue: Array<() => void> = [];
acquire(): Promise<void> {
if (!this.locked) {
this.locked = true;
return Promise.resolve();
}
return new Promise<void>(resolve => this.queue.push(resolve));
}
release(): void {
if (this.queue.length > 0) {
const next = this.queue.shift()!;
next(); // pass lock to next waiter
} else {
this.locked = false;
}
}
async withLock<T>(fn: () => Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
}2Step 2: Protect a shared counter with the mutex
const mutex = new Mutex();
let counter = 0;
async function increment(): Promise<void> {
await mutex.withLock(async () => {
const current = counter;
await new Promise(r => setTimeout(r, 1)); // simulate async work
counter = current + 1;
});
}
await Promise.all(Array.from({ length: 10 }, increment));
console.log(counter); // always 10// ── Async Mutex with Timeout for Critical Sections ───────────────
class MutexTimeoutError extends Error {
constructor(public readonly waitedMs: number) {
super(`Mutex acquisition timed out after ${waitedMs}ms`);
this.name = "MutexTimeoutError";
}
}
interface MutexOptions {
timeoutMs?: number;
label?: string;
}
class AsyncMutex {
private locked = false;
private waiters: Array<{ resolve: () => void; reject: (e: Error) => void }> = [];
private owner?: string;
private acquisitionCount = 0;
constructor(private label = "mutex") {}
async acquire(opts: MutexOptions = {}): Promise<() => void> {
const { timeoutMs, label } = opts;
const start = Date.now();
if (!this.locked) {
this.locked = true;
this.owner = label;
this.acquisitionCount++;
return () => this.release();
}
return new Promise<() => void>((resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const waiter = {
resolve: () => {
if (timeoutId) clearTimeout(timeoutId);
this.owner = label;
this.acquisitionCount++;
resolve(() => this.release());
},
reject,
};
this.waiters.push(waiter);
if (timeoutMs !== undefined) {
timeoutId = setTimeout(() => {
const idx = this.waiters.indexOf(waiter);
if (idx !== -1) this.waiters.splice(idx, 1);
reject(new MutexTimeoutError(Date.now() - start));
}, timeoutMs);
}
});
}
private release(): void {
const next = this.waiters.shift();
if (next) {
next.resolve();
} else {
this.locked = false;
this.owner = undefined;
}
}
async withLock<T>(fn: () => Promise<T>, opts: MutexOptions = {}): Promise<T> {
const unlock = await this.acquire(opts);
try {
return await fn();
} finally {
unlock();
}
}
get isLocked(): boolean { return this.locked; }
get currentOwner(): string | undefined { return this.owner; }
get totalAcquisitions(): number { return this.acquisitionCount; }
get queueDepth(): number { return this.waiters.length; }
}
// Example: serialised cache refresh
const cacheMutex = new AsyncMutex("cache-refresh");
const cache = new Map<string, { value: unknown; expiresAt: number }>();
async function getOrRefresh<T>(
key: string,
fetcher: () => Promise<T>,
ttlMs = 60_000
): Promise<T> {
const cached = cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return cached.value as T;
}
return cacheMutex.withLock(async () => {
// Re-check after acquiring lock (another waiter may have refreshed)
const fresh = cache.get(key);
if (fresh && fresh.expiresAt > Date.now()) return fresh.value as T;
const value = await fetcher();
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
return value;
}, { timeoutMs: 5_000, label: `refresh:${key}` });
}
export { AsyncMutex, MutexTimeoutError, getOrRefresh, type MutexOptions };Mutex / Lock Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Mutex / Lock Pattern in the Real World
“A single-occupancy public restroom with a door latch is a perfect mutex. The latch (lock) ensures only one person (thread) occupies the restroom (critical section) at a time. Anyone who finds the door locked must wait outside; when the occupant leaves and unlatches the door, one waiting person may enter.”