UNPKG

atomics-sync

Version:

JavaScript multithreading synchronization library

130 lines (115 loc) 4.1 kB
import { InvalidError } from "./errors.js"; import { INT32_MAX_VALUE } from "./limits.js"; const { compareExchange, wait, notify, store, load } = Atomics; /** * A counting semaphore implementation for thread synchronization. * Controls access to shared resources with a counter that atomically tracks available permits. * Supports blocking, timed, and non-blocking acquisition of permits. */ export class Semaphore { // Index for the value in the shared array private static readonly INDEX_VALUE = 0; /** * Initializes a new semaphore with the specified initial value * @param value Initial number of available permits (must be non-negative integer) * @returns A new Int32Array backed by SharedArrayBuffer * @throws {InvalidError} If value is not an integer * @throws {RangeError} If value is negative or exceeds INT32_MAX_VALUE */ static init(value: number) { if (!Number.isInteger(value)) { throw new InvalidError("initial value should be int32"); } if (value < 0 || value > INT32_MAX_VALUE) { throw new RangeError("initial value should be greater or equal zero and less or equal maximum int32 value"); } const sem = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)); store(sem, Semaphore.INDEX_VALUE, value); return sem; } /** * Acquires a permit, blocking until one is available * @param sem The semaphore to wait on * @remarks * - Uses atomic compare-exchange to safely decrement counter * - Efficiently waits when no permits are available */ static wait(sem: Int32Array<SharedArrayBuffer>) { for (;;) { const value = load(sem, Semaphore.INDEX_VALUE); if (value > 0) { if (compareExchange(sem, Semaphore.INDEX_VALUE, value, value - 1) === value) { return; } } else { wait(sem, Semaphore.INDEX_VALUE, value); } } } /** * Attempts to acquire a permit with a timeout * @param sem The semaphore to wait on * @param timestamp Absolute timeout timestamp in milliseconds * @returns true if permit acquired, false if timed out */ static timedWait(sem: Int32Array<SharedArrayBuffer>, timestamp: number) { for (;;) { const value = load(sem, Semaphore.INDEX_VALUE); if (value > 0) { if (compareExchange(sem, Semaphore.INDEX_VALUE, value, value - 1) === value) { return true; } } else { const timeout = timestamp - Date.now(); const waitResult = wait(sem, Semaphore.INDEX_VALUE, value, timeout); if (waitResult === "timed-out") { return false; } } } } /** * Attempts to acquire a permit without blocking * @param sem The semaphore to try * @returns true if permit was acquired, false if no permits available */ static tryWait(sem: Int32Array<SharedArrayBuffer>) { for (;;) { const value = load(sem, Semaphore.INDEX_VALUE); if (value === 0) { return false; } if (compareExchange(sem, Semaphore.INDEX_VALUE, value, value - 1) === value) { return true; } } } /** * Releases a permit back to the semaphore * @param sem The semaphore to post to * @throws {RangeError} If incrementing would exceed INT32_MAX_VALUE * @remarks Wakes one waiting thread if counter transitions from 0 to 1 */ static post(sem: Int32Array<SharedArrayBuffer>) { for (;;) { const value = load(sem, Semaphore.INDEX_VALUE); if (value === INT32_MAX_VALUE) { throw new RangeError("maximum limit reached for semaphore value"); } if (compareExchange(sem, Semaphore.INDEX_VALUE, value, value + 1) === value) { if (value === 0) { notify(sem, Semaphore.INDEX_VALUE, 1); } return; } } } /** * Gets the current number of available permits * @param sem The semaphore to check * @returns Current semaphore value (number of available permits) */ static getValue(sem: Int32Array<SharedArrayBuffer>) { return load(sem, Semaphore.INDEX_VALUE); } }