UNPKG

atomics-sync

Version:

JavaScript multithreading synchronization library

175 lines (144 loc) 6.09 kB
import { DeadlockError, InvalidError, PermissionError } from "./errors.js"; import { INT32_MAX_VALUE, INT32_MIN_VALUE } from "./limits.js"; const { compareExchange, wait, notify, store, load } = Atomics; /** * A mutual exclusion lock implementation for thread synchronization. * Uses SharedArrayBuffer and Atomics for cross-thread operations. * Provides basic lock/unlock functionality with additional timed and try variants. * Tracks owning thread to prevent deadlocks and enforce proper usage. */ export class Mutex { // Constants for mutex state management private static readonly OWNER_EMPTY = 0; // Value indicating no owner private static readonly STATE_UNLOCKED = 0; // Mutex is available private static readonly STATE_LOCKED = 1; // Mutex is acquired private static readonly INDEX_STATE = 0; // Index for state in array private static readonly INDEX_OWNER = 1; // Index for owner in array /** * Initializes a new mutex in shared memory * @returns A new Int32Array backed by SharedArrayBuffer with: * - index 0: state (initially unlocked) * - index 1: owner (initially empty) */ static init() { const mutex = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2)); store(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED); store(mutex, Mutex.INDEX_OWNER, Mutex.OWNER_EMPTY); return mutex; } /** * Acquires the mutex, blocking until available * @param mutex The mutex to lock * @param threadId Unique identifier for the calling thread * @throws {DeadlockError} If thread already owns the mutex * @throws {InvalidError} If threadId is invalid */ static lock(mutex: Int32Array<SharedArrayBuffer>, threadId: number) { Mutex.checkThreadIdBeforeLock(mutex, threadId); // Spin-wait loop with atomic compare-exchange for (;;) { // Attempt atomic acquisition if ( compareExchange(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED, Mutex.STATE_LOCKED) === Mutex.STATE_UNLOCKED ) { store(mutex, Mutex.INDEX_OWNER, threadId); return; } // Wait efficiently if mutex is locked wait(mutex, Mutex.INDEX_STATE, Mutex.STATE_LOCKED); } } /** * Attempts to acquire the mutex with a timeout * @param mutex The mutex to lock * @param threadId Unique identifier for the calling thread * @param timestamp Absolute timeout timestamp in milliseconds * @returns true if lock acquired, false if timed out * @throws {DeadlockError} If thread already owns the mutex * @throws {InvalidError} If threadId is invalid */ static timedLock(mutex: Int32Array<SharedArrayBuffer>, threadId: number, timestamp: number) { Mutex.checkThreadIdBeforeLock(mutex, threadId); for (;;) { if ( compareExchange(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED, Mutex.STATE_LOCKED) === Mutex.STATE_UNLOCKED ) { store(mutex, Mutex.INDEX_OWNER, threadId); return true; } const timeout = timestamp - Date.now(); const waitResult = wait(mutex, Mutex.INDEX_STATE, Mutex.STATE_LOCKED, timeout); if (waitResult === "timed-out") { return false; } } } /** * Attempts to acquire the mutex without blocking * @param mutex The mutex to lock * @param threadId Unique identifier for the calling thread * @returns true if lock acquired, false if mutex was busy * @throws {DeadlockError} If thread already owns the mutex * @throws {InvalidError} If threadId is invalid */ static tryLock(mutex: Int32Array<SharedArrayBuffer>, threadId: number) { Mutex.checkThreadIdBeforeLock(mutex, threadId); if (compareExchange(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED, Mutex.STATE_LOCKED) === Mutex.STATE_UNLOCKED) { store(mutex, Mutex.INDEX_OWNER, threadId); return true; } return false; } /** * Releases the mutex * @param mutex The mutex to unlock * @param threadId Unique identifier for the calling thread * @throws {PermissionError} If thread doesn't own the mutex or mutex wasn't locked * @throws {InvalidError} If threadId is invalid */ static unlock(mutex: Int32Array<SharedArrayBuffer>, threadId: number) { Mutex.checkThreadIdIsValid(threadId); // Verify ownership if (load(mutex, Mutex.INDEX_OWNER) !== threadId) { throw new PermissionError("current thread is not owner of mutex"); } // Clear owner first to prevent race conditions store(mutex, Mutex.INDEX_OWNER, Mutex.OWNER_EMPTY); // Verify locked state while unlocking if (compareExchange(mutex, Mutex.INDEX_STATE, Mutex.STATE_LOCKED, Mutex.STATE_UNLOCKED) === Mutex.STATE_UNLOCKED) { throw new PermissionError("mutex was not locked"); } // Wake one waiting thread notify(mutex, Mutex.INDEX_STATE, 1); } /** * Validates threadId and checks for deadlock conditions before locking * @param mutex The mutex being locked * @param threadId The thread attempting to lock * @throws {DeadlockError} If thread already owns mutex * @throws {InvalidError} If threadId is invalid */ private static checkThreadIdBeforeLock(mutex: Int32Array<SharedArrayBuffer>, threadId: number) { Mutex.checkThreadIdIsValid(threadId); if (load(mutex, Mutex.INDEX_OWNER) === threadId) { throw new DeadlockError("thread already owns this mutex"); } } /** * Validates that a threadId is properly formatted and within range * @param threadId The thread ID to validate * @throws {InvalidError} If threadId is not an integer or is empty * @throws {RangeError} If threadId is outside int32 range */ private static checkThreadIdIsValid(threadId: number) { if (!Number.isInteger(threadId)) { throw new InvalidError("threadId should be int32"); } if (threadId < INT32_MIN_VALUE || threadId > INT32_MAX_VALUE) { throw new RangeError("threadId is out of int32 range"); } if (threadId === Mutex.OWNER_EMPTY) { throw new InvalidError("threadId is empty owner"); } } }