SharedArrayBuffer in JavaScript

SharedArrayBuffer in JavaScript

This article explains SharedArrayBuffer in JavaScript.

We will provide a detailed explanation of the basics of SharedArrayBuffer, how to use it, specific use cases, and security considerations.

YouTube Video

SharedArrayBuffer in JavaScript

SharedArrayBuffer is a powerful tool in JavaScript for sharing memory between multiple threads. Especially in combination with Web Workers, it enables parallel processing, making it effective for computation-intensive tasks and applications requiring real-time capabilities.

What is SharedArrayBuffer?

SharedArrayBuffer provides a memory buffer in JavaScript that allows sharing binary data between multiple threads (mainly Web Workers). A regular ArrayBuffer requires copying between the main thread and workers, but SharedArrayBuffer allows direct memory sharing without copying, thereby significantly improving performance.

Features

  • Shared Memory It allows multiple threads to work with the same memory space.
  • Performance Improvement Since copying can be omitted, the overhead is reduced when processing large amounts of data.
  • Thread Synchronization You can use it together with Atomics to perform synchronization in order to avoid conflicts when accessing memory.

Basic Usage Example

 1// Create a 16-byte shared memory
 2const sharedBuffer = new SharedArrayBuffer(16);
 3
 4// Treat it as an Int32Array
 5const sharedArray = new Int32Array(sharedBuffer);
 6
 7// Set a value
 8sharedArray[0] = 42;
 9
10console.log(sharedArray[0]);  // 42

In this example, we create a 16-byte memory area using SharedArrayBuffer and treat that memory area as an Int32Array. This memory buffer can be shared among multiple threads.

Using with Web Workers

The true value of SharedArrayBuffer is demonstrated when used in conjunction with Web Workers. The following code is an example of using shared memory between the main thread and a worker.

On the Main Thread

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Modify the memory
12// Output : Main thread: 100
13sharedArray[0] = 100;
14console.log("Main thread: ", sharedArray[0]);

On the Worker Side (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    // Use the received shared buffer
 4    const sharedArray = new Int32Array(event.data);
 5
 6    // Read the contents of the memory
 7    // Output : Worker thread: 100
 8    console.log("Worker thread: ", sharedArray[0]);
 9
10    // Change the value
11    sharedArray[0] = 200;
12};
  • In this example, the main thread creates a shared buffer and passes it to the worker. The worker can access this buffer to read and modify values. In this way, data can be shared between threads without copying.

Bidirectional Update Confirmation

By using SharedArrayBuffer, both the main thread and workers can read and write to the same memory, enabling bidirectional update confirmation. Below is an example where the main thread sets a value, a worker changes this value, and then the main thread checks for the update.

On the Main Thread

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Set initial value
12// Output : Main thread initial: 100
13sharedArray[0] = 100;
14console.log("Main thread initial:", sharedArray[0]);
15
16// Listen for worker confirmation
17worker.onmessage = () => {
18    // Output : Main thread after worker update: 200
19    console.log("Main thread after worker update:", sharedArray[0]);
20};

On the Worker Side (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    // Read initial value
 6    // Output : Worker thread received: 100
 7    console.log("Worker thread received:", sharedArray[0]);
 8
 9    // Update the value
10    sharedArray[0] = 200;
11
12    // Notify main thread
13    self.postMessage("Value updated");
14};
  • In this example, the main thread writes the value 100 first, and after the worker reads it, it rewrites it to 200. After that, the worker notifies the main thread, and the main thread reads the shared memory again to confirm the update. In this way, combining notifications enables bidirectional update confirmation.

Synchronization with Atomics

When using shared memory, one must be cautious of data race conditions and inconsistencies. When multiple threads access the same memory simultaneously, conflicts can occur. To prevent this, JavaScript uses the Atomics object for synchronization.

For example, to safely increment a counter with multiple threads, you can use Atomics to prevent conflicts.

1const sharedBuffer = new SharedArrayBuffer(16);
2const sharedArray = new Int32Array(sharedBuffer);
3
4// Increment the counter
5Atomics.add(sharedArray, 0, 1);
6
7console.log(Atomics.load(sharedArray, 0));  // 1

Atomics.add increments the value at a specific index atomically and returns the new value. This operation is guaranteed to be conflict-free with other threads. Atomics.load is also used to safely read values from shared memory.

Waiting and Notification Using Atomics.wait and Atomics.notify

When using SharedArrayBuffer, there are situations where a worker needs to wait until a certain condition is met, and once another worker fulfills that condition, it needs to notify the waiting worker. In such cases, Atomics.wait and Atomics.notify are useful.

Atomics.wait blocks a thread until the value at a specific index in the shared memory changes, while Atomics.notify notifies waiting threads that they may proceed. This enables safe waiting and notification between multiple workers. However, Atomics.wait cannot be used on the main thread and is only available inside workers.

 1// Create a shared buffer (1 Int32 slot is enough for signaling)
 2const sharedBuffer = new SharedArrayBuffer(4);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create workers with names
 6const waiter = new Worker('worker.js', { name: 'waiter' });
 7const notifier = new Worker('worker.js', { name: 'notifier' });
 8
 9// Pass the shared buffer to both
10waiter.postMessage(sharedBuffer);
11notifier.postMessage(sharedBuffer);
12
13// Listen for messages
14waiter.onmessage = (event) => {
15    console.log(`[Main] Message from waiter:`, event.data);
16};
17notifier.onmessage = (event) => {
18    console.log(`[Main] Message from notifier:`, event.data);
19};
  • On the main thread, a SharedArrayBuffer is created as shared memory and is converted into an Int32Array with only one element. This single integer slot is used as a signal to synchronize between workers. Next, two workers are created, and each is assigned a role using the name property: waiter (the waiting role) and notifier (the notifying role). Finally, the shared buffer is passed to both workers, and onmessage handlers are set up so that messages sent from each worker can be received.

On the Worker Side (worker.js)

 1// worker.js
 2onmessage = (event) => {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    if (self.name === 'waiter') {
 6        postMessage('Waiter is waiting...');
 7        // Wait until notifier signals index 0
 8        Atomics.wait(sharedArray, 0, 0);
 9        postMessage('Waiter was notified!');
10    }
11
12    if (self.name === 'notifier') {
13        postMessage('Notifier is preparing...');
14        setTimeout(() => {
15            // Notify waiter after 2 seconds
16            Atomics.store(sharedArray, 0, 1);
17            Atomics.notify(sharedArray, 0, 1);
18            postMessage('Notifier has sent the signal!');
19        }, 2000);
20    }
21};
22// Output
23// [Main] Message from waiter: Waiter is waiting...
24// [Main] Message from notifier: Notifier is preparing...
25// [Main] Message from notifier: Notifier has sent the signal!
26// [Main] Message from waiter: Waiter was notified!
  • In this example, the waiter worker remains in a waiting state using Atomics.wait as long as the value at index 0 is 0. On the other hand, when the notifier worker changes the value to 123 with Atomics.store and calls Atomics.notify, the waiter worker will resume and be able to get the updated value. With this, efficient and safe waiting and notification between threads can be achieved.

Use Cases for SharedArrayBuffer

SharedArrayBuffer is especially useful for the following use cases:.

  • Real-Time Processing It is suitable for applications requiring low latency, such as audio and video processing or game engines, where data needs to be shared instantly between threads.
  • Parallel Computing When processing large amounts of data simultaneously with multiple threads, using SharedArrayBuffer avoids memory copying and can improve performance.
  • Machine Learning By parallelizing tasks such as data preprocessing and inference, efficient computation becomes possible.

Security Considerations

SharedArrayBuffer is a powerful feature, but it also carries security risks. In particular, concerns about side-channel attacks like Spectre have temporarily halted its support. To mitigate this vulnerability, browsers have implemented the following measures:.

  • Site Isolation Sites that allow the use of SharedArrayBuffer will run in a process that is completely isolated from other sites.
  • Cross-Origin Resource Policy To use SharedArrayBuffer, the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers must be properly set.

For example, by setting headers like the following, the use of SharedArrayBuffer becomes possible:.

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

This prevents external resources from interfering with the current content and increases security.

Summary

SharedArrayBuffer is a very powerful tool for sharing memory between multiple threads. It is an essential technology for improving performance, and its effects are particularly evident in the fields of real-time processing and parallel computing. However, it also involves security risks, so correct configuration and synchronization are important.

By utilizing SharedArrayBuffer, you can build more advanced and higher-performance web applications.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video