JavaScript - JavaScript SharedArrayBuffer and Atomics

Introduction

JavaScript traditionally executes code in a single main thread. In a browser, this means that a long-running calculation can make the webpage unresponsive. JavaScript provides mechanisms such as Web Workers to perform tasks in separate threads.

When multiple workers need to work with the same memory, however, simply sending messages between them may not be enough. SharedArrayBuffer and the Atomics API provide a way for multiple JavaScript execution contexts to share memory and coordinate access to that memory safely.

SharedArrayBuffer creates a block of memory that can be shared between workers, while Atomics provides operations that allow those workers to read, write, and synchronize access to shared data.


1. What Is SharedArrayBuffer?

SharedArrayBuffer is a JavaScript object that represents a region of memory that can be shared between different execution contexts.

Unlike a normal ArrayBuffer, the memory represented by a SharedArrayBuffer can be accessed by multiple workers.

The basic syntax is:

const sharedBuffer = new SharedArrayBuffer(1024);

Here, 1024 represents the size of the shared memory in bytes.

The SharedArrayBuffer itself does not provide convenient methods for directly storing numbers or other structured data. Instead, it is normally used with typed arrays.

For example:

const sharedBuffer = new SharedArrayBuffer(1024);

const numbers = new Int32Array(sharedBuffer);

numbers[0] = 100;
numbers[1] = 200;

console.log(numbers[0]);

The Int32Array provides a convenient way to interpret the shared memory as 32-bit signed integers.


2. SharedArrayBuffer vs ArrayBuffer

It is important to understand the difference between ArrayBuffer and SharedArrayBuffer.

ArrayBuffer

An ArrayBuffer creates memory that normally belongs to one JavaScript execution context.

const buffer = new ArrayBuffer(1024);

When data needs to be sent to another worker, it is generally transferred or copied according to the communication mechanism being used.

SharedArrayBuffer

A SharedArrayBuffer is specifically designed so that multiple workers can access the same underlying memory.

const buffer = new SharedArrayBuffer(1024);

This makes it possible for workers to communicate through shared memory rather than always exchanging complete messages.


3. Why Shared Memory Is Useful

Consider a situation where two workers need access to a large dataset.

With ordinary message passing, one worker might send data to another:

worker.postMessage(data);

For very large amounts of data, repeatedly passing data between workers can introduce additional overhead.

With shared memory, both workers can access the same memory region.

Conceptually:

Main Thread
     |
     v
Shared Memory
   /     \
  /       \
Worker 1  Worker 2

Worker 1 and Worker 2 can both work with the same underlying memory.

This can be useful for applications involving:

  • Large numerical calculations

  • Image processing

  • Audio processing

  • Simulations

  • Scientific calculations

  • Parallel data processing

  • High-performance applications

However, shared memory introduces another problem: multiple workers may attempt to modify the same data at the same time.

That is where Atomics becomes important.


4. What Are Atomics?

The JavaScript Atomics object provides operations for working safely with shared typed-array data.

Suppose two workers have access to this value:

numbers[0]

Worker 1 might read the value while Worker 2 is changing it.

Without appropriate synchronization, concurrent operations can produce unexpected results.

Atomics provides operations that are performed atomically.

An atomic operation is treated as an indivisible operation from the perspective of other threads.

For example:

Atomics.add(numbers, 0, 10);

This means:

  1. Access the value at index 0.

  2. Add 10.

  3. Store the result.

  4. Perform the operation atomically.


5. Creating Shared Memory

A simple example is:

const sharedBuffer = new SharedArrayBuffer(16);

const sharedArray = new Int32Array(sharedBuffer);

sharedArray[0] = 10;
sharedArray[1] = 20;
sharedArray[2] = 30;
sharedArray[3] = 40;

console.log(sharedArray);

The buffer contains 16 bytes.

Because an Int32Array uses 4 bytes for each element, it can contain four 32-bit integers.

Conceptually:

SharedArrayBuffer
+--------+--------+--------+--------+
|   10   |   20   |   30   |   40   |
+--------+--------+--------+--------+
   [0]      [1]      [2]      [3]

6. Sharing the Buffer with a Worker

A SharedArrayBuffer can be passed to a Web Worker.

For example, the main JavaScript file might contain:

const sharedBuffer = new SharedArrayBuffer(16);

const sharedArray = new Int32Array(sharedBuffer);

sharedArray[0] = 100;

const worker = new Worker("worker.js");

worker.postMessage(sharedBuffer);

The worker can receive the shared buffer:

self.onmessage = function(event) {
    const sharedArray = new Int32Array(event.data);

    console.log(sharedArray[0]);
};

The important point is that the worker is accessing the shared memory rather than receiving an independent copy of the underlying data.


7. Atomics.load()

Atomics.load() reads a value from a shared typed array.

Syntax:

Atomics.load(typedArray, index);

Example:

const buffer = new SharedArrayBuffer(16);
const numbers = new Int32Array(buffer);

numbers[0] = 50;

const value = Atomics.load(numbers, 0);

console.log(value);

Output:

50

It provides an atomic read of the value.


8. Atomics.store()

Atomics.store() writes a value atomically.

Syntax:

Atomics.store(typedArray, index, value);

Example:

Atomics.store(numbers, 0, 100);

Now the first element contains:

100

This is useful when multiple execution contexts are accessing the same shared memory.


9. Atomics.add()

Atomics.add() atomically adds a value.

Atomics.add(numbers, 0, 5);

Suppose:

numbers[0] = 10;

After:

Atomics.add(numbers, 0, 5);

the value becomes:

15

The method returns the old value before the addition.

For example:

const oldValue = Atomics.add(numbers, 0, 5);

console.log(oldValue);

If the original value was 10, oldValue is 10, while the stored value becomes 15.


10. Atomics.sub()

Atomics.sub() performs an atomic subtraction.

numbers[0] = 20;

const oldValue = Atomics.sub(numbers, 0, 5);

console.log(oldValue);
console.log(numbers[0]);

The output is conceptually:

20
15

The operation subtracts 5 atomically.


11. Atomics.and(), Atomics.or(), and Atomics.xor()

The Atomics API also provides atomic bitwise operations.

Atomics.and()

Atomics.and(numbers, 0, value);

Performs a bitwise AND operation.

Atomics.or()

Atomics.or(numbers, 0, value);

Performs a bitwise OR operation.

Atomics.xor()

Atomics.xor(numbers, 0, value);

Performs a bitwise XOR operation.

These operations can be useful when shared memory is being used for flags or compact state management.


12. Atomics.exchange()

Atomics.exchange() replaces a value atomically.

Example:

numbers[0] = 10;

const oldValue = Atomics.exchange(numbers, 0, 50);

console.log(oldValue);
console.log(numbers[0]);

Output:

10
50

The old value is returned, while the new value is stored.

This can be useful when one worker needs to replace a shared state value safely.


13. Atomics.compareExchange()

One of the most important operations is Atomics.compareExchange().

It allows JavaScript to perform a conditional replacement.

Syntax:

Atomics.compareExchange(
    typedArray,
    index,
    expectedValue,
    replacementValue
);

Suppose:

numbers[0] = 10;

Now:

Atomics.compareExchange(numbers, 0, 10, 20);

The operation checks whether the current value is 10.

If it is 10, it changes it to 20.

If the value is something else, the replacement does not happen.

This is useful for implementing synchronization techniques where an update should happen only if the shared value still has an expected value.


14. Why Race Conditions Are a Problem

A race condition occurs when multiple execution contexts access shared data concurrently and the final result depends on the order in which operations happen.

Suppose a shared counter contains:

100

Two workers want to increase it by 1.

If both workers perform a normal read-modify-write operation:

Worker 1 reads 100
Worker 2 reads 100

Worker 1 calculates 101
Worker 2 calculates 101

Worker 1 writes 101
Worker 2 writes 101

The expected result was:

102

But the final result may be:

101

This is a race condition.

Using:

Atomics.add(numbers, 0, 1);

makes the increment atomic.


15. Atomics.wait()

Atomics.wait() is used to make a worker wait until a shared value changes or a timeout occurs.

A simplified example is:

Atomics.wait(sharedArray, 0, 0);

This means that the worker waits while the value at index 0 is equal to 0.

Another worker can change the value and notify the waiting worker.

This can be useful for coordinating workers.

Atomics.wait() is intended for appropriate worker contexts and is not generally something you should call on the browser's main thread because blocking the main thread would make the webpage unresponsive.


16. Atomics.notify()

Atomics.notify() wakes workers that are waiting on a shared memory location.

For example:

Atomics.notify(sharedArray, 0);

This tells the JavaScript runtime that workers waiting on the specified memory location may be resumed.

A simplified communication sequence can look like this:

Worker 1
   |
   | waits
   v
Shared Memory
   ^
   | changes value
   |
Worker 2
   |
   | notifies
   v
Worker 1 continues

This provides a basic mechanism for synchronization.


17. Atomics.waitAsync()

JavaScript also provides Atomics.waitAsync() for situations where waiting should not block the calling execution context in the same way as Atomics.wait().

It returns an object describing an asynchronous wait.

For example:

const result = Atomics.waitAsync(sharedArray, 0, 0);

if (result.async) {
    result.value.then(() => {
        console.log("Value changed");
    });
}

This is particularly useful when asynchronous coordination is preferable to blocking.


18. Supported Typed Arrays

Atomic operations are not available for every JavaScript typed array.

They are primarily used with integer typed arrays such as:

Int8Array
Uint8Array
Int16Array
Uint16Array
Int32Array
Uint32Array
BigInt64Array
BigUint64Array

For example:

const buffer = new SharedArrayBuffer(16);

const data = new Int32Array(buffer);

Atomics.store(data, 0, 25);

The choice of typed array determines how the underlying bytes are interpreted.


19. SharedArrayBuffer and Web Workers

A common architecture is:

                Main Thread
                    |
                    |
             SharedArrayBuffer
              /             \
             /               \
       Worker 1             Worker 2
          |                    |
          |                    |
          +------ Atomics ----+

The main thread can create shared memory and provide it to workers.

The workers can then:

  • Read shared values

  • Update shared values

  • Perform atomic calculations

  • Wait for changes

  • Notify other workers

  • Coordinate their work

This can reduce the need to repeatedly transfer large datasets through messages.


20. Example: Shared Counter

Consider the following main-thread code:

const buffer = new SharedArrayBuffer(4);
const counter = new Int32Array(buffer);

counter[0] = 0;

const worker = new Worker("worker.js");

worker.postMessage(buffer);

The worker could perform:

self.onmessage = function(event) {
    const counter = new Int32Array(event.data);

    Atomics.add(counter, 0, 1);

    console.log(Atomics.load(counter, 0));
};

If multiple workers perform:

Atomics.add(counter, 0, 1);

the increments are performed atomically.

This makes the shared counter much safer than using an ordinary read-modify-write sequence.


21. SharedArrayBuffer Security Considerations

SharedArrayBuffer has security implications because shared memory can be used in certain timing-based attacks.

For this reason, browser environments impose security requirements before web pages can use SharedArrayBuffer in the relevant ways.

Modern browsers generally require cross-origin isolation for browser-side SharedArrayBuffer functionality.

This typically involves appropriate HTTP response headers such as:

Cross-Origin-Opener-Policy
Cross-Origin-Embedder-Policy

A web application therefore needs to be configured correctly before relying on shared memory in the browser.


22. SharedArrayBuffer Does Not Automatically Make Code Safe

An important point is that SharedArrayBuffer itself does not prevent race conditions.

For example:

sharedArray[0] = sharedArray[0] + 1;

is a read-modify-write sequence.

If several workers execute it simultaneously, they can interfere with one another.

Instead, an atomic operation can be used:

Atomics.add(sharedArray, 0, 1);

Therefore:

SharedArrayBuffer
       +
   Atomics
       =
Shared-memory coordination

The two technologies solve different parts of the problem.


23. Advantages

SharedArrayBuffer and Atomics provide several advantages.

Shared memory

Multiple workers can access the same memory region.

Reduced copying

Large datasets do not always need to be copied between workers.

Atomic operations

Operations such as addition, subtraction, exchange, and comparison can be performed safely.

Worker synchronization

Workers can wait for and notify each other through shared memory.

High-performance computing

They can be useful for applications requiring parallel processing and efficient communication.


24. Limitations

There are also important limitations.

Increased complexity

Shared-memory programming is more difficult than ordinary sequential JavaScript.

Race conditions

Incorrect synchronization can lead to unexpected results.

Browser security requirements

Browser applications may require cross-origin isolation.

Debugging difficulty

Problems involving concurrency can be difficult to reproduce and diagnose.

Not suitable for every application

For many applications, ordinary Web Worker messaging with postMessage() is simpler and sufficient.


25. SharedArrayBuffer vs postMessage()

These approaches solve different problems.

Feature postMessage() SharedArrayBuffer
Communication style Message-based Shared-memory based
Data sharing Usually message/transfer based Same memory can be accessed
Synchronization Message events Atomics
Complexity Lower Higher
Race-condition risk Lower Higher
Large shared datasets Can involve copying/transfer considerations Useful for shared access
Best suited for General worker communication Advanced concurrent applications

For ordinary applications, postMessage() is often easier.

For specialized high-performance applications, shared memory can provide significant advantages.


26. Practical Example

A simplified producer-consumer design can use shared memory.

The producer worker creates data:

Atomics.store(buffer, 0, 100);
Atomics.notify(buffer, 0);

The consumer worker waits for the value:

Atomics.wait(buffer, 0, 0);

const value = Atomics.load(buffer, 0);

console.log(value);

Conceptually:

Producer Worker
      |
      | writes data
      v
Shared Memory
      |
      | notify
      v
Consumer Worker
      |
      | reads data
      v
    Result

This demonstrates how SharedArrayBuffer and Atomics can work together to coordinate concurrent operations.


27. When Should You Use SharedArrayBuffer?

It is appropriate when an application has a genuine need for shared memory and concurrent processing.

Examples include:

  • Computationally intensive simulations

  • Real-time data processing

  • Audio processing

  • Image and video processing

  • Large numerical datasets

  • Parallel algorithms

  • High-performance browser applications

For a simple application that only needs to send occasional information between a page and a worker, postMessage() is usually easier.


28. Key Points to Remember

SharedArrayBuffer creates memory that can be shared between JavaScript execution contexts.

A typed array such as Int32Array is normally used to access the shared memory.

Atomics provides operations for safely accessing and modifying shared values.

Important atomic methods include:

Atomics.load()
Atomics.store()
Atomics.add()
Atomics.sub()
Atomics.exchange()
Atomics.compareExchange()
Atomics.wait()
Atomics.notify()
Atomics.waitAsync()

The primary purpose of these APIs is to support safe and coordinated shared-memory concurrency.

The most important distinction to remember is:

SharedArrayBuffer provides the shared memory, while Atomics provides mechanisms for safely operating on and synchronizing access to that memory.