Skip to content

Advanced synchronization primitives for JavaScript 🚦

License

Notifications You must be signed in to change notification settings

slavamuravey/atomics-sync

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Atomics Sync

Atomics Sync is lightweight library providing thread-safe synchronization primitives for JavaScript environments with shared memory support (Web Workers, Node.js worker_threads). Implements essential concurrency control mechanisms using SharedArrayBuffer and Atomics API.

Features

  • Mutex - Mutual exclusion lock for critical sections
  • SpinLock - Low-level busy-wait lock for very short operations
  • Semaphore - Counting semaphore for resource management
  • Condition - Condition variables for thread signaling
  • Barrier - Synchronization point for multiple threads
  • Once - One-time initialization primitive

Important: For browsers, your server must send these headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Installation

npm install atomics-sync

Why This Library?

Modern JavaScript applications increasingly use:

  • Web Workers for parallel processing
  • SharedArrayBuffer for shared memory
  • CPU-intensive tasks (WASM, WebGL, etc.)

These primitives help coordinate work between threads while preventing:

  • Race conditions
  • Data corruption
  • Deadlocks

Usage Examples

Important: There is no reliable way for a thread to know its own ID automatically in JavaScript environments. The parent/main thread must explicitly assign and pass a unique thread ID to each worker thread it creates.

Mutex

Init mutex to work safely with shared data:

const shared = new Int32Array(
  new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
);
const mutex = Mutex.init();

Pass mutex and shared to threads. Remember about threadId:

const worker = new Worker("./worker.js", {
  workerData: { threadId, shared, mutex }
});

Within thread use lock/unlock methods to wrap critical section:

try {
  Mutex.lock(mutex, threadId);
  // work with shared data here
} finally {
  Mutex.unlock(mutex, threadId);
}

See full example.

Semaphore

Here's a practical example demonstrating how to use a semaphore to make one thread wait for another thread to complete certain actions:

Init semaphore:

const sem = Semaphore.init(0);

One thread creates another thread and must wait some initialization actions within it:

new Worker("./worker.js", { workerData: { sem } });
Semaphore.wait(sem);
// continue execution
// ...

Created thread performs necessary operations and notify parent thread:

// ...
initSomeImportantThings();
Semaphore.post(sem);

See full example.

Condition

Using a condition variable, we can make one thread wait for a change in a shared variable (protected by a mutex) before proceeding with its operation.

Init condition variable and mutex, allocate shared variable:

const cond = Condition.init();
const mtx = Mutex.init();
const shared = new Int32Array(
  new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
);
shared[0] = -1;

One thread produces value:

Mutex.lock(mtx, threadId);
shared[0] = Math.floor(Math.random() * 10);
Condition.signal(cond);
Mutex.unlock(mtx, threadId);

Another thread consumes the value and makes some work with it:

Mutex.lock(mtx, threadId);

while (shared[0] < 0) {
  Condition.wait(cond, mtx, threadId);
}

shared[0] *= 10;
Mutex.unlock(mtx, threadId);

See full example.

SpinLock

A spinlock provides an interface nearly identical to a mutex (lock()/unlock()), but is optimized for very short wait times where spinning (busy-waiting) is more efficient than thread suspension.

Barrier

A barrier synchronizes multiple threads at a specific execution point.

In this example, we launch 10 threads that execute at variable speeds and create a barrier with a count of 5.

// main.js

const barrier = Barrier.init(5);

for (let i = 0; i < 10; i++) {
  const threadId = i + 1;
  const worker = new Worker("./worker.js", {
    workerData: { threadId, barrier }
  });
}

// worker.js

setTimeout(() => {
  // ...
  Barrier.wait(barrier, threadId);
  // ...
}, threadId * 100);

The first 5 threads to reach the barrier will block and wait.

Once the 5th thread arrives, the barrier releases all waiting threads.

The remaining 5 threads then proceed through the barrier in the same way.

See full example.

Once

A Once primitive ensures one-time initialization in concurrent environments.

Init once value:

const once = Once.init();

Pass it into some threads:

const worker = new Worker("./worker.js", {
  workerData: { once }
});

Within thread:

Once.execute(once, () => {
  // some logic that should be executed only once
});

See full example.

Documentation

For complete API reference, see API documentation.