atomics-sync
Version:
JavaScript multithreading synchronization library
626 lines (614 loc) • 27.3 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["atomics-sync"] = {}));
})(this, (function (exports) { 'use strict';
/**
* Error thrown when a deadlock situation is detected in synchronization primitives.
* This typically occurs when threads are circularly waiting for resources/locks
* held by each other.
*/
class DeadlockError extends Error {
constructor(message) {
super(message);
this.name = "DeadlockError";
}
}
/**
* Error thrown when a thread attempts an operation it doesn't have permission for,
* such as unlocking a mutex it doesn't own or accessing protected resources.
*/
class PermissionError extends Error {
constructor(message) {
super(message);
this.name = "PermissionError";
}
}
/**
* Error thrown when invalid arguments or operations are detected,
* such as passing non-integer values where integers are required,
* or attempting operations on improperly initialized objects.
*/
class InvalidError extends Error {
constructor(message) {
super(message);
this.name = "InvalidError";
}
}
/**
* The maximum safe 32-bit signed integer value (2^31 - 1).
* This is the largest value that can be stored in an Int32Array or similar typed array.
* Useful for boundary checking in integer operations.
*/
const INT32_MAX_VALUE = 2147483647;
/**
* The minimum safe 32-bit signed integer value (-2^31).
* This is the smallest value that can be stored in an Int32Array or similar typed array.
* Useful for boundary checking in integer operations.
*/
const INT32_MIN_VALUE = -2147483648;
const { compareExchange: compareExchange$3, wait: wait$2, notify: notify$2, store: store$4, load: load$3 } = 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.
*/
class Mutex {
/**
* 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$4(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED);
store$4(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, threadId) {
Mutex.checkThreadIdBeforeLock(mutex, threadId);
// Spin-wait loop with atomic compare-exchange
for (;;) {
// Attempt atomic acquisition
if (compareExchange$3(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED, Mutex.STATE_LOCKED) === Mutex.STATE_UNLOCKED) {
store$4(mutex, Mutex.INDEX_OWNER, threadId);
return;
}
// Wait efficiently if mutex is locked
wait$2(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, threadId, timestamp) {
Mutex.checkThreadIdBeforeLock(mutex, threadId);
for (;;) {
if (compareExchange$3(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED, Mutex.STATE_LOCKED) === Mutex.STATE_UNLOCKED) {
store$4(mutex, Mutex.INDEX_OWNER, threadId);
return true;
}
const timeout = timestamp - Date.now();
const waitResult = wait$2(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, threadId) {
Mutex.checkThreadIdBeforeLock(mutex, threadId);
if (compareExchange$3(mutex, Mutex.INDEX_STATE, Mutex.STATE_UNLOCKED, Mutex.STATE_LOCKED) === Mutex.STATE_UNLOCKED) {
store$4(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, threadId) {
Mutex.checkThreadIdIsValid(threadId);
// Verify ownership
if (load$3(mutex, Mutex.INDEX_OWNER) !== threadId) {
throw new PermissionError("current thread is not owner of mutex");
}
// Clear owner first to prevent race conditions
store$4(mutex, Mutex.INDEX_OWNER, Mutex.OWNER_EMPTY);
// Verify locked state while unlocking
if (compareExchange$3(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$2(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
*/
static checkThreadIdBeforeLock(mutex, threadId) {
Mutex.checkThreadIdIsValid(threadId);
if (load$3(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
*/
static checkThreadIdIsValid(threadId) {
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");
}
}
}
// Constants for mutex state management
Mutex.OWNER_EMPTY = 0; // Value indicating no owner
Mutex.STATE_UNLOCKED = 0; // Mutex is available
Mutex.STATE_LOCKED = 1; // Mutex is acquired
Mutex.INDEX_STATE = 0; // Index for state in array
Mutex.INDEX_OWNER = 1; // Index for owner in array
const { compareExchange: compareExchange$2, wait: wait$1, notify: notify$1, store: store$3, load: load$2 } = 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.
*/
class Semaphore {
/**
* 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) {
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$3(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) {
for (;;) {
const value = load$2(sem, Semaphore.INDEX_VALUE);
if (value > 0) {
if (compareExchange$2(sem, Semaphore.INDEX_VALUE, value, value - 1) === value) {
return;
}
}
else {
wait$1(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, timestamp) {
for (;;) {
const value = load$2(sem, Semaphore.INDEX_VALUE);
if (value > 0) {
if (compareExchange$2(sem, Semaphore.INDEX_VALUE, value, value - 1) === value) {
return true;
}
}
else {
const timeout = timestamp - Date.now();
const waitResult = wait$1(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) {
for (;;) {
const value = load$2(sem, Semaphore.INDEX_VALUE);
if (value === 0) {
return false;
}
if (compareExchange$2(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) {
for (;;) {
const value = load$2(sem, Semaphore.INDEX_VALUE);
if (value === INT32_MAX_VALUE) {
throw new RangeError("maximum limit reached for semaphore value");
}
if (compareExchange$2(sem, Semaphore.INDEX_VALUE, value, value + 1) === value) {
if (value === 0) {
notify$1(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) {
return load$2(sem, Semaphore.INDEX_VALUE);
}
}
// Index for the value in the shared array
Semaphore.INDEX_VALUE = 0;
const { wait, notify } = Atomics;
/**
* A condition variable implementation for thread synchronization.
* Allows threads to wait for some condition to become true while properly releasing
* and reacquiring a mutex lock. Uses SharedArrayBuffer for cross-thread communication.
*/
class Condition {
/**
* Initializes a new condition variable in shared memory
* @returns A new Int32Array backed by SharedArrayBuffer
*/
static init() {
return new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
}
/**
* Wakes up one thread waiting on the condition variable
* @param cond The condition variable to signal
*/
static signal(cond) {
notify(cond, 0, 1);
}
/**
* Wakes up all threads waiting on the condition variable
* @param cond The condition variable to broadcast to
*/
static broadcast(cond) {
notify(cond, 0);
}
/**
* Blocks the current thread until the condition variable is signaled
* @param cond The condition variable to wait on
* @param mutex The associated mutex to release while waiting
* @param threadId The ID of the current thread
* @remarks Automatically releases mutex before waiting and reacquires after
*/
static wait(cond, mutex, threadId) {
Mutex.unlock(mutex, threadId);
wait(cond, 0, 0);
Mutex.lock(mutex, threadId);
}
/**
* Blocks the current thread until either:
* - The condition variable is signaled, or
* - The specified timeout expires
* @param cond The condition variable to wait on
* @param mutex The associated mutex to release while waiting
* @param threadId The ID of the current thread
* @param timestamp The absolute timeout timestamp in milliseconds
* @returns true if the condition was signaled, false if timed out
* @remarks Automatically releases mutex before waiting and reacquires after
*/
static timedWait(cond, mutex, threadId, timestamp) {
try {
Mutex.unlock(mutex, threadId);
return wait(cond, 0, 0, timestamp - Date.now()) !== "timed-out";
}
finally {
Mutex.lock(mutex, threadId);
}
}
}
const { store: store$2, load: load$1, add } = Atomics;
/**
* A synchronization primitive that enables multiple threads to wait for each other
* to reach a common execution point before continuing.
*
* Implements a reusable barrier using shared memory, mutex and condition variable.
*/
class Barrier {
/**
* Initializes a new barrier with the specified thread count
* @param count Number of threads that must reach the barrier before continuing
* @returns Initialized BarrierObject with shared structures
* @throws {InvalidError} If count is not an integer
* @throws {RangeError} If count is <= 0
*/
static init(count) {
Barrier.validateCount(count);
const barrier = new BigInt64Array(new SharedArrayBuffer(BigInt64Array.BYTES_PER_ELEMENT * 3));
store$2(barrier, Barrier.INDEX_COUNT, BigInt(count));
store$2(barrier, Barrier.INDEX_WAITED, 0n);
store$2(barrier, Barrier.INDEX_GENERATION, 0n);
const mutex = Mutex.init();
const cond = Condition.init();
return {
barrier,
mutex,
cond
};
}
/**
* Makes the calling thread wait at the barrier until all threads have arrived
* @param barrier The barrier object to wait on
* @param threadId Unique identifier for the calling thread
* @returns true if this thread was the last to arrive (releases others), false otherwise
*/
static wait(barrier, threadId) {
Mutex.lock(barrier.mutex, threadId);
const generation = load$1(barrier.barrier, Barrier.INDEX_GENERATION);
const count = load$1(barrier.barrier, Barrier.INDEX_COUNT);
const waited = add(barrier.barrier, Barrier.INDEX_WAITED, 1n) + 1n;
try {
if (waited >= count) {
store$2(barrier.barrier, Barrier.INDEX_WAITED, 0n);
add(barrier.barrier, Barrier.INDEX_GENERATION, 1n);
Condition.broadcast(barrier.cond);
return true;
}
while (load$1(barrier.barrier, Barrier.INDEX_GENERATION) === generation) {
Condition.wait(barrier.cond, barrier.mutex, threadId);
}
return false;
}
finally {
Mutex.unlock(barrier.mutex, threadId);
}
}
/**
* Validates that the thread count is a positive integer
* @param count Number to validate
* @throws {InvalidError} If count is not an integer
* @throws {RangeError} If count is <= 0
*/
static validateCount(count) {
if (!Number.isInteger(count)) {
throw new InvalidError("count should be integer");
}
if (count <= 0) {
throw new RangeError("count should be greater zero");
}
}
}
// Indexes for accessing different values in the barrier array
Barrier.INDEX_COUNT = 0; // Stores total threads required
Barrier.INDEX_WAITED = 1; // Stores number of threads currently waiting
Barrier.INDEX_GENERATION = 2; // Stores current barrier generation
const { compareExchange: compareExchange$1, store: store$1, load } = Atomics;
/**
* A spin lock implementation for low-level thread synchronization.
* Uses busy-waiting with atomic operations for acquiring the lock.
* More efficient than mutexes for very short critical sections.
* Tracks owning thread to prevent deadlocks and enforce proper usage.
*/
class SpinLock {
/**
* Initializes a new spin lock in shared memory
* @returns A new Int32Array backed by SharedArrayBuffer with:
* - index 0: state (initially unlocked)
* - index 1: owner (initially empty)
*/
static init() {
const lock = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2));
store$1(lock, SpinLock.INDEX_STATE, SpinLock.STATE_UNLOCKED);
store$1(lock, SpinLock.INDEX_OWNER, SpinLock.OWNER_EMPTY);
return lock;
}
/**
* Acquires the lock, spinning until available
* @param lock The spin lock to acquire
* @param threadId Unique identifier for the calling thread
* @throws {DeadlockError} If thread already owns the lock
* @throws {InvalidError} If threadId is invalid
* @remarks Uses Atomics.pause() when available to reduce contention
*/
static lock(lock, threadId) {
SpinLock.checkThreadIdBeforeLock(lock, threadId);
// Spin-wait loop with atomic compare-exchange
for (;;) {
// Attempt atomic acquisition
if (compareExchange$1(lock, SpinLock.INDEX_STATE, SpinLock.STATE_UNLOCKED, SpinLock.STATE_LOCKED) ===
SpinLock.STATE_UNLOCKED) {
store$1(lock, SpinLock.INDEX_OWNER, threadId);
return;
}
// Use pause instruction to reduce contention when available
// @ts-ignore
if (typeof Atomics.pause === "function") {
// @ts-ignore
Atomics.pause();
}
}
}
/**
* Attempts to acquire the lock without spinning
* @param lock The spin lock to try
* @param threadId Unique identifier for the calling thread
* @returns true if lock acquired, false if lock was busy
* @throws {DeadlockError} If thread already owns the lock
* @throws {InvalidError} If threadId is invalid
*/
static tryLock(lock, threadId) {
SpinLock.checkThreadIdBeforeLock(lock, threadId);
if (compareExchange$1(lock, SpinLock.INDEX_STATE, SpinLock.STATE_UNLOCKED, SpinLock.STATE_LOCKED) ===
SpinLock.STATE_UNLOCKED) {
store$1(lock, SpinLock.INDEX_OWNER, threadId);
return true;
}
return false;
}
/**
* Releases the lock
* @param lock The spin lock to release
* @param threadId Unique identifier for the calling thread
* @throws {PermissionError} If thread doesn't own the lock or lock wasn't locked
* @throws {InvalidError} If threadId is invalid
*/
static unlock(lock, threadId) {
SpinLock.checkThreadIdIsValid(threadId);
// Verify ownership
if (load(lock, SpinLock.INDEX_OWNER) !== threadId) {
throw new PermissionError("current thread is not owner of lock");
}
// Clear owner first to prevent race conditions
store$1(lock, SpinLock.INDEX_OWNER, SpinLock.OWNER_EMPTY);
// Verify locked state while unlocking
if (compareExchange$1(lock, SpinLock.INDEX_STATE, SpinLock.STATE_LOCKED, SpinLock.STATE_UNLOCKED) ===
SpinLock.STATE_UNLOCKED) {
throw new PermissionError("lock was not locked");
}
}
/**
* Validates threadId and checks for deadlock conditions before locking
* @param lock The spin lock being acquired
* @param threadId The thread attempting to lock
* @throws {DeadlockError} If thread already owns lock
* @throws {InvalidError} If threadId is invalid
*/
static checkThreadIdBeforeLock(lock, threadId) {
SpinLock.checkThreadIdIsValid(threadId);
if (load(lock, SpinLock.INDEX_OWNER) === threadId) {
throw new DeadlockError("thread already owns this lock");
}
}
/**
* 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
*/
static checkThreadIdIsValid(threadId) {
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 === SpinLock.OWNER_EMPTY) {
throw new InvalidError("threadId is empty owner");
}
}
}
// Constants for lock state management
SpinLock.OWNER_EMPTY = 0; // Value indicating no owner
SpinLock.STATE_UNLOCKED = 0; // Lock is available
SpinLock.STATE_LOCKED = 1; // Lock is acquired
SpinLock.INDEX_STATE = 0; // Index for state in array
SpinLock.INDEX_OWNER = 1; // Index for owner in array
const { compareExchange, store } = Atomics;
/**
* A synchronization primitive that ensures a function is executed only once,
* even when called from multiple threads.
* Uses atomic operations for thread-safe execution tracking.
*/
class Once {
/**
* Initializes a new Once primitive in shared memory
* @returns A new Int32Array backed by SharedArrayBuffer initialized to NOT_EXECUTED
*/
static init() {
const once = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
store(once, Once.INDEX_EXECUTED, Once.EXECUTED_NO);
return once;
}
/**
* Executes the provided function exactly once, even if called from multiple threads
* @param once The Once primitive to use for synchronization
* @param fn The function to execute (will be called at most once)
* @remarks The function will be called by whichever thread wins the atomic race
*/
static execute(once, fn) {
if (compareExchange(once, Once.INDEX_EXECUTED, Once.EXECUTED_NO, Once.EXECUTED_YES) === Once.EXECUTED_NO) {
fn();
}
}
/**
* Checks if the function has been executed
* @param once The Once primitive to check
* @returns true if the function has been executed, false otherwise
*/
static isExecuted(once) {
return Atomics.load(once, Once.INDEX_EXECUTED) === Once.EXECUTED_YES;
}
}
// Index for the execution state in the shared array
Once.INDEX_EXECUTED = 0;
// Possible execution states
Once.EXECUTED_NO = 0; // Function has not been executed
Once.EXECUTED_YES = 1; // Function has been executed
exports.Barrier = Barrier;
exports.Condition = Condition;
exports.DeadlockError = DeadlockError;
exports.INT32_MAX_VALUE = INT32_MAX_VALUE;
exports.INT32_MIN_VALUE = INT32_MIN_VALUE;
exports.InvalidError = InvalidError;
exports.Mutex = Mutex;
exports.Once = Once;
exports.PermissionError = PermissionError;
exports.Semaphore = Semaphore;
exports.SpinLock = SpinLock;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=atomics-sync.umd.js.map