SharedArrayBuffer en JavaScript

SharedArrayBuffer en JavaScript

Este artículo explica SharedArrayBuffer en JavaScript.

Proporcionaremos una explicación detallada de los conceptos básicos de SharedArrayBuffer, cómo usarlo, casos de uso específicos y consideraciones de seguridad.

YouTube Video

SharedArrayBuffer en JavaScript

SharedArrayBuffer es una herramienta poderosa en JavaScript para compartir memoria entre múltiples hilos. Especialmente en combinación con Web Workers, permite el procesamiento paralelo, haciéndolo efectivo para tareas intensivas en cálculo y aplicaciones que requieren capacidades en tiempo real.

¿Qué es SharedArrayBuffer?

SharedArrayBuffer proporciona un búfer de memoria en JavaScript que permite compartir datos binarios entre múltiples hilos (principalmente Web Workers). Un ArrayBuffer regular requiere copiar entre el hilo principal y los workers, pero SharedArrayBuffer permite el intercambio directo de memoria sin copiar, mejorando significativamente el rendimiento.

Características

  • Memoria compartida Permite que varios hilos trabajen con el mismo espacio de memoria.
  • Mejora del rendimiento Como se puede omitir la copia, se reduce la sobrecarga al procesar grandes cantidades de datos.
  • Sincronización de hilos Se puede usar junto con Atomics para realizar la sincronización y evitar conflictos al acceder a la memoria.

Ejemplo de uso básico

 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

En este ejemplo, creamos un área de memoria de 16 bytes utilizando SharedArrayBuffer y tratamos esa área de memoria como un Int32Array. Este búfer de memoria puede ser compartido entre múltiples hilos.

Uso con Web Workers

El verdadero valor de SharedArrayBuffer se demuestra cuando se utiliza en conjunto con Web Workers. El siguiente código es un ejemplo de cómo utilizar memoria compartida entre el hilo principal y un worker.

En el Hilo Principal

 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]);

En el Lado del Trabajador (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};
  • En este ejemplo, el hilo principal crea un búfer compartido y lo pasa al worker. El worker puede acceder a este búfer para leer y modificar valores. De esta manera, los datos pueden ser compartidos entre hilos sin copiar.

Confirmación de actualización bidireccional

Al utilizar SharedArrayBuffer, tanto el hilo principal como los trabajadores pueden leer y escribir en la misma memoria, lo que permite la confirmación de actualizaciones bidireccional. A continuación se muestra un ejemplo donde el hilo principal establece un valor, un trabajador cambia dicho valor y luego el hilo principal verifica la actualización.

En el Hilo Principal

 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};

En el Lado del Trabajador (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};
  • En este ejemplo, el hilo principal escribe primero el valor 100, y después de que el trabajador lo lee, lo reescribe como 200. Después de eso, el trabajador notifica al hilo principal, y el hilo principal lee nuevamente la memoria compartida para confirmar la actualización. De esta manera, la combinación de notificaciones permite la confirmación de actualizaciones bidireccional.

Sincronización con Atomics

Cuando se utiliza memoria compartida, se debe tener cuidado con las condiciones de carrera de datos y las inconsistencias. Cuando múltiples hilos acceden a la misma memoria simultáneamente, pueden ocurrir conflictos. Para prevenir esto, JavaScript utiliza el objeto Atomics para la sincronización.

Por ejemplo, para incrementar de forma segura un contador con múltiples hilos, puedes utilizar Atomics para prevenir conflictos.

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 incrementa el valor en un índice específico de forma atómica y devuelve el nuevo valor. Esta operación garantiza que no haya conflictos con otros hilos. Atomics.load también se utiliza para leer valores de forma segura desde la memoria compartida.

Espera y notificación usando Atomics.wait y Atomics.notify

Al usar SharedArrayBuffer, hay situaciones en las que un trabajador debe esperar hasta que se cumpla cierta condición, y una vez que otro trabajador cumple con esa condición, debe notificar al trabajador que está esperando. En tales casos, Atomics.wait y Atomics.notify son útiles.

Atomics.wait bloquea un hilo hasta que el valor en un índice específico de la memoria compartida cambie, mientras que Atomics.notify informa a los hilos en espera que pueden continuar. Esto permite una espera y notificación seguras entre varios trabajadores. Sin embargo, Atomics.wait no se puede usar en el hilo principal y solo está disponible dentro de los 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};
  • En el hilo principal, se crea un SharedArrayBuffer como memoria compartida y se convierte en un Int32Array con solo un elemento. Este solo espacio de entero se utiliza como una señal para sincronizar entre los workers. A continuación, se crean dos workers y a cada uno se le asigna un rol usando la propiedad name: waiter (el que espera) y notifier (el que notifica). Finalmente, el buffer compartido se pasa a ambos workers y se configuran los manejadores onmessage para que se puedan recibir los mensajes enviados por cada worker.

En el Lado del Trabajador (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!
  • En este ejemplo, el worker waiter permanece en un estado de espera usando Atomics.wait siempre que el valor en el índice 0 sea 0. Por otro lado, cuando el worker notifier cambia el valor a 123 con Atomics.store y llama a Atomics.notify, el worker waiter reanudará y podrá obtener el valor actualizado. Con esto, se puede lograr una espera y notificación eficiente y segura entre hilos.

Casos de Uso para SharedArrayBuffer

SharedArrayBuffer es especialmente útil para los siguientes casos de uso:.

  • Procesamiento en tiempo real Es adecuado para aplicaciones que requieren baja latencia, como el procesamiento de audio y video o motores de juegos, donde los datos deben compartirse instantáneamente entre los hilos.
  • Computación paralela Al procesar grandes cantidades de datos de manera simultánea con varios hilos, el uso de SharedArrayBuffer evita la copia de memoria y puede mejorar el rendimiento.
  • Aprendizaje automático Al paralelizar tareas como el preprocesamiento de datos y la inferencia, el cálculo eficiente se vuelve posible.

Consideraciones de Seguridad

SharedArrayBuffer es una característica poderosa, pero también conlleva riesgos de seguridad. En particular, las preocupaciones sobre ataques de canal lateral como Spectre han detenido temporalmente su soporte. Para mitigar esta vulnerabilidad, los navegadores han implementado las siguientes medidas:.

  • Aislamiento de sitios Los sitios que permiten el uso de SharedArrayBuffer se ejecutarán en un proceso completamente aislado de otros sitios.
  • Política de recursos de origen cruzado Para utilizar SharedArrayBuffer, los encabezados Cross-Origin-Opener-Policy y Cross-Origin-Embedder-Policy deben estar configurados correctamente.

Por ejemplo, al establecer encabezados como los siguientes, el uso de SharedArrayBuffer se vuelve posible:.

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

Esto evita que recursos externos interfieran con el contenido actual y aumenta la seguridad.

Resumen

SharedArrayBuffer es una herramienta muy poderosa para compartir memoria entre múltiples hilos. Es una tecnología esencial para mejorar el rendimiento, y sus efectos son particularmente evidentes en los campos del procesamiento en tiempo real y el cómputo paralelo. Sin embargo, también conlleva riesgos de seguridad, por lo que la configuración y sincronización correctas son importantes.

Al utilizar SharedArrayBuffer, se pueden crear aplicaciones web más avanzadas y de mayor rendimiento.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video