Sitemap

JavaScript — ES8 (ECMA2017)-Parte IV

10 min readMay 13, 2025

Temario

  • Introducción
  • Nuevas Características
    SharedArrayBuffer
    Atomics

i. Introducción

Esta story es la continuación de:

  • ES8 — ES2017 (I[ref], II [ref], III [ref])
  • ES7 — ES2016[ref]
  • ES6 — ES2015 (I[ref], II[ref],III[ref],IV[ref],V[ref])
  • JavaScript — Historia, Estándar & Motores [ref]

Vamos a ver la cuarta parte de novedades que nos trae la edición ES8 (ES2017) de JavaScript.

Importante: Hay que tener en cuenta que es posible que NO TODOS los navegadores modernos admitan estas “nuevas” funcionalidades, por lo que te sugiero primero validar las versiones de los mismos antes de subir a producción. Si estas usando algún transpilador (ej. Babel) no deberás preocuparte.

Antes de comenzar te sugiero que primero leas:

  • Secuencia, concurrencia y paralelismo [ref]
  • JavaScript — Web Workers API [ref]

ii. Nuevas Características

01. SharedArrayBuffer [ref] y Atomics [ref]

SharedArrayBuffer y Atomics son dos nuevas APIs introducidas en ECMAScript 2017 diseñadas para habilitar operaciones de concurrencia [ref] segura en JavaScript, particularmente entre hilos (threads) como los Web Workers [ref].

  • SharedArrayBuffer — Es una estructura de datos que permite compartir un bloque de memoria entre múltiples contextos de ejecución.
  • Atomics — Es un objeto global que proporciona operaciones atómicas de lectura, escritura y sincronización sobre un SharedArrayBuffer.

Estas APIs permiten trabajar con memoria compartida y evitan condiciones de carrera, permitiendo ejecutar tareas paralelas complejas en entornos como navegadores o Node.js.

Nota: Actualmente, su uso en navegadores requiere habilitar cabeceras de seguridad como Cross-Origin-Opener-Policy y Cross-Origin-Embedder-Policy, por razones de seguridad relacionadas con Spectre/Meltdown.

Principales características —

  • Memoria compartida entre workers, con lectura y escritura en tiempo real.
  • Operaciones atómicas y seguras.
  • Compatible con tipos Int8Array, Uint8Array, Int32Array, etc.
  • Acceso de bajo nivel a la memoria, similar a lenguajes como C o Rust.
  • Bloqueo y espera activa con Atomics.wait y Atomics.notify.

Ventajas —

  • Mejora el rendimiento de ciertas operaciones concurrentes (como renderizado paralelo, procesamiento de audio/video, algoritmos de simulación).
  • Permite sincronización explícita entre threads.
  • Proporciona control fino sobre la memoria compartida.
  • Usa APIs estándar del lenguaje (sin librerías externas).

¿Dónde se usa? —

  • Motores de juegos que renderizan en paralelo.
  • Edición de audio/video en tiempo real (WebAssembly + Workers).
  • Aplicaciones científicas con simulaciones multithread.

Sintaxis —

// Crear un buffer compartido de 1024 bytes
const sab = new SharedArrayBuffer(1024);

// Crear una vista (por ejemplo, de enteros de 32 bits)
const int32 = new Int32Array(sab);

// Escribir un valor
int32[0] = 123;

// Leerlo en otro thread (Worker) usando el mismo buffer

En un entorno con Web Workers, puedes enviar la variable sab mediante postMessage() sin copiar su contenido: es compartido por referencia.

¿Cómo funciona internamente o a bajo nivel? —

  1. SharedArrayBuffer asigna un bloque de memoria binaria directamente accesible desde múltiples workers.
  2. A diferencia de ArrayBuffer, los datos no se copian al pasarlos entre workers; se comparten.
  3. Atomics actúa como un “candado” de sincronización:
  • Atomics.load / Atomics.store aseguran lectura/escritura segura.
  • Atomics.add, sub, and, or, xor, exchange modifican datos sin riesgo de colisión.
  • Atomics.wait permite que un thread se bloquee hasta que otro llame a Atomics.notify.

Importante: Para que funcione, va a depender de implementaciones del motor JavaScript (como V8, SpiderMonkey o JavaScriptCore) que usan primitivas del sistema operativo para garantizar atomicidad.

Métodos estáticos de Atomics —

Para realiza operaciones atómicas seguras vamos a ocupar los métodos estáticos que tiene Atomics, por lo que lo primero que debes saber es que todos los métodos operan sobre tipos enteros compartidos (Int8Array, Int16Array, Int32Array, Uint8Array, Uint16Array, Uint32Array) que estén montados sobre un SharedArrayBuffer.

Vamos a partir de este ejemplo:

const sab = new SharedArrayBuffer(4); // 4 bytes = 1 Int32
const shared = new Int32Array(sab); // Vista de enteros de 32 bits

shared[0] = 10; // Inicializamos el valor

Podemos observar que:

  • const sab = new SharedArrayBuffer(4)— Estamos asignando memoria de manera controlada y explícita para compartirla entre threads.
  • Se crea con 4 bytes de memoria ya que vamos a trabajar con enteros de 34 bits (4 bytes = 32 bits), así que con esto nos da el espacio para un solo entero.
  • SharedArrayBuffer por sí solo no permite manipular datos, por lo que necesita un front como Int32Array para poder acceder (leer/escribir) a su contenido.
  • shared[0] — Accedemos al dato como un array normal y le asignamos un valor 0.

Atomics.loadLee un valor atómicamente.

// Sintaxis: 
Atomics.load(typedArray, index)

// Ejemplo
const val = Atomics.load(shared, 0); // val = 10
console.log(val); // 10

Atomics.addSuma un valor al elemento en index y devuelve el valor anterior.

// Sintaxis: 
Atomics.add(typedArray, index, value)

// Ejemplo
Atomics.add(shared, 0, 1); // shared[0] += 1
console.log(Atomics.load(shared, 0)); // 11

Importante: Al hacer la operación Atomics.add() NO puede ser interrumpida por otro hilo, por lo tanto es thread-safe.

Atomics.subResta un valor. Retorna el valor anterior.

// Sintaxis:
Atomics.sub(typedArray, index, value)

// Ejemplo
Atomics.sub(shared, 0, 5); // shared[0] -= 5
console.log(Atomics.load(shared, 0)); // 6

Atomics.andAplica un AND binario entre el valor actual y el value.

// Sintaxis:
Atomics.and(typedArray, index, value)

// Ejemplo
// 6 → 110 en binario
Atomics.and(shared, 0, 3); // 110 & 011 = 010

/* Devuelve 1 solo si ambos bits son 1
* 110
* &011
* -----
* 010 → 2
*/

console.log(Atomics.load(shared, 0)); // 2

// Imprime 2 y no 010 ya que JS por defecto, imprime los números en base
// decimal en console.log

Atomics.orAplica un OR binario entre el valor actual y el value.

// Sintaxis:
Atomics.or(typedArray, index, value)

// Ejemplo
// 2 → 010 en binario
Atomics.or(shared, 0, 1); // 010 | 001 = 011

/* Devuelve 1 si alguno de los bits son 1
* 010
* &001
* -----
* 011 → 3
*/

console.log(Atomics.load(shared, 0)); // 3

Atomics.xorAplica un XOR binario entre el valor actual y el value.

// Sintaxis:
Atomics.xor(typedArray, index, value)

// Ejemplo
// 3 → 011 en binario
Atomics.xor(shared, 0, 1); // 011 ^ 001 = 010

/* Devuelve 1 solo si un bits es 1
* 011
* ^001
* -----
* 010 → 2
*/

console.log(Atomics.load(shared, 0)); // 2

Atomics.storeAlmacena un nuevo valor de forma atómica.

// Sintaxis
Atomics.store(typedArray, index, value)

// Ejemplo
Atomics.store(shared, 0, 10); // shared[0] = 10
console.log(Atomics.load(shared, 0)); // 10

Atomics.exhangeReemplaza el valor en el índice con uno nuevo, devolviendo el valor anterior.

// Sintaxis
Atomics.exchange(typedArray, index, value)

// Ejemplo
const old = Atomics.exchange(shared, 0, 99); // shared[0] = 99
console.log(old); // 10
console.log(Atomics.load(shared, 0)); // 99

Atomics.compareExchange — Solo reemplaza si el valor actual coincide con expected.

// Sintaxis
Atomics.compareExchange(typedArray, index, expected, replacement)

// Ejemplo
console.log(Atomics.load(shared, 0)); // 99
Atomics.compareExchange(shared, 0, 99, 200); // shared[0] =200

console.log(Atomics.load(shared, 0)); // 200
Atomics.compareExchange(shared, 0, 123, 300); // No coincide, no cambia

console.log(Atomics.load(shared, 0)); // 200

Atomics.wait — Bloquea el thread (Workers o entornos con soporte) hasta que otro lo notifique o cambie el valor.

// Sintaxis
Atomics.wait(typedArray, index, expectedValue[, timeout])

// Ejemplo
Atomics.wait(shared, 0, 200); // Bloquea si sared[0] === 200

A tomar en cuenta: Solo funciona con Int32Array. En entornos no worker, puede no estar disponible o bloquear todo.

Atomics.notify — Desbloquea hasta N (count) hilos que estén esperando en Atomics.wait.

// Sintaxis
Atomics.notify(typedArray, index[, count])

// Ejemplo
Atomics.notify(shared, 0, 1); // Desbloquea 1 hilo que este esperando en shared[0]

Ejemplo 1 — Contador compartido

const sab = new SharedArrayBuffer(4); // 4 bytes = 1 Int32
const shared = new Int32Array(sab);

shared[0] = 0;
Atomics.add(shared, 0, 1); // Incrementa el contador shared[0] += 1
console.log(Atomics.load(shared, 0)); // 1

Podemos observar que:

  • Atomics.add(shared, 0 , 1) — Esta operación atómica nos permite sumarle 1 al valor que hay en shared[0] de forma segura.
  • Atomics.load(shared, 0) — Lee el valor del índice 0 de forma atómica, es decir se asegura que el valor sea correcto, incluso si otro hilo esra escribiendo al mismo tiempo.

Ejemplo 2 — Sincronización entre workers

// main.js

const sab = new SharedArrayBuffer(4);
const shared = new Int32Array(sab);

const worker = new Worker('worker.js');
worker.postMessage(sab);

setTimeout(() => {
// Se ejecuta despues de 1s
Atomics.store(shared, 0, 42); // Almacena un nuevo valor
Atomics.notify(shared, 0); // Desbloquea al worker
}, 1000);
// worker.js

self.onmessage = (e) => {
const shared = new Int32Array(e.data);
console.log('Esperando actualización...');
Atomics.wait(shared, 0, 0); // Espera a que cambie el valor
console.log('Nuevo valor:', Atomics.load(shared, 0)); // 42
};

Ejemplo 3 — Colas de tareas concurrentes

Simularemos un sistema donde varios workers leen tareas desde una cola compartida en memoria, que es escrita por un productor (main thread).

Donde:

  • main.js— Actuará como productor.
  • worker.js— Cada worker leerá y ejecutará tareas.
  • SharedArrayBuffer— Almacenará la cola circular.
  • Atomics— Garantizará la sincronización de acceso.

Nota: Para poder ver los ejercicios vamos a levantar un servidor con Express [ref], ya que por default los navegadores por seguridad bloquean SharedArrayBuffer.

package.json

{
"name": "sab-demo",
"version": "1.0.0",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}

server.js

import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = 3001;

// Permitir COOP y COEP
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});

app.use(express.static(path.join(__dirname, 'public')));

app.listen(PORT, () => {
console.log(`🚀 Servidor en http://localhost:${PORT}`);
});

public/index.html

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cola de tareas concurrente</title>
</head>
<body>
<script type="module" src="main.js"></script>
</body>

</html>

public/main.js

// main.js

// Número máximo de tareas que se generarán en total
const MAX_TASKS = 20;
// Capacidad máxima de la cola circular (tareas simultáneas en memoria)
const TASK_CAPACITY = 10;
// Tamaño total del buffer: 2 espacios para índices + N espacios para tareas
const BUFFER_SIZE = 2 + TASK_CAPACITY;
// Crear un SharedArrayBuffer para compartir entre el hilo principal y los workers
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * BUFFER_SIZE);
// Crear una vista de enteros de 32 bits sobre el buffer compartido
const queue = new Int32Array(sab);

// Inicializar punteros de lectura y escritura
queue[0] = 0; // readIndex (índice de lectura)
queue[1] = 0; // writeIndex (índice de escritura)

// Crear un pool de 3 workers y enviarles el buffer compartido junto con su nombre
const workers = Array.from({ length: 3 }, (_, i) => {
const w = new Worker('worker.js');
w.postMessage({ sab, name: `Worker-${i + 1}` });
return w;
});

let taskId = 1;
// Generar una nueva tarea cada 1 segundo hasta alcanzar el límite
const interval = setInterval(() => {
// Si ya se alcanzó el número máximo de tareas permitidas
if (taskId > MAX_TASKS) {
clearInterval(interval);
console.log(
'%c[MAIN] Se han generado las tareas permitidas.',
'color: cyan; font-weight: bold;'
);

// Apagar todos los workers
workers.forEach((worker) => worker.postMessage({ type: 'shutdown' }));
return;
}

// Obtener los índices actuales de lectura y escritura
const read = Atomics.load(queue, 0);
const write = Atomics.load(queue, 1);

// Calcular la próxima posición de escritura de forma circular
const next = (write + 1) % TASK_CAPACITY;

// Verificar si la cola está llena (no sobrescribir)
if (next === read) {
console.warn('%c[MAIN] Cola llena, no se puede escribir aún.', 'color: orange');
return;
}

// Agregar la nueva tarea al buffer
const task = taskId++;
queue[2 + write] = task;

// Actualizar el índice de escritura de forma atómica
Atomics.store(queue, 1, next);

// Notificar a los workers que hay una nueva tarea disponible
Atomics.notify(queue, 2);

// Log con color para distinguir fácilmente los mensajes del hilo principal
console.log(
`%c[MAIN] Nueva tarea agregada: ${task}`,
'color: cyan; font-weight: bold;'
);
}, 1000);

public/server.js

// worker.js

// Nombre por defecto si no se proporciona
let WORKER_NAME = 'anon';

self.onmessage = (e) => {
const { sab, name, type } = e.data;
if (name) WORKER_NAME = name;

// Apagar el worker si se envía la señal de "shutdown"
if (type === 'shutdown') {
console.log(
`%c[${WORKER_NAME}] desconectado`,
'color: gray; font-weight: bold;'
);
self.close(); // Finaliza el hilo de ejecución
return;
}

// Crear la vista sobre el buffer compartido
const queue = new Int32Array(sab);
const BUFFER_SIZE = queue.length;
const TASK_CAPACITY = BUFFER_SIZE - 2; // Espacio dedicado a tareas

// Función principal que consume tareas del buffer
function consume() {
const read = Atomics.load(queue, 0);
const write = Atomics.load(queue, 1);

if (read !== write) {
// Reclamar la tarea de forma atómica (evita colisiones entre workers)
const claimed = Atomics.compareExchange( queue, 0, read, (read + 1) % TASK_CAPACITY);

if (claimed === read) {
const task = queue[2 + read];
// Ya actualizamos el readIndex arriba (compareExchange)
console.log(
`%c[${WORKER_NAME}] ejecutando tarea ${task} (read=${read}, write=${write})`,
'color: orange; font-weight: bold;'
);

doWork(task);
}
}

// Repetir el ciclo sin bloquear el event loop del worker
setTimeout(consume, 0);
}

// Simula trabajo con la tarea (tiempo aleatorio)
function doWork(task) {
const time = Math.random() * 2000 + 1000;

setTimeout(() => {
console.log(
`%c[${WORKER_NAME}] completó tarea ${task}`,
'color: yellow; font-weight: bold;'
);
}, time);
}

// Iniciar el ciclo de consumo
consume();
};

Al iniciar el servidor con npm run start y acceder a http://localhost:3001, podrás observar lo siguiente en consola:

  • Asegúrate de configurar correctamente las cabeceras de seguridad (COOP y COEP).
  • Este tipo de arquitectura está más cerca de la programación multithread de bajo nivel que de los típicos patrones JavaScript.
  • Es ideal para tareas CPU-bound que no pueden resolverse fácilmente con Promise + async/await.

Soporte —

  • SharedArrayBuffer [ref]
  • Atomic [ref]

A considerar —

  • No se puede usar en todos los contextos sin configuración extra. (por ejemplo, navegadores exigen COOP [Cross-Origin-Opener-Policy] / COEP [Cross-Origin-Embedder-Policy]).
  • Solo tipos enteros (como Int32Array) son compatibles con Atomics.
  • Evita busy waiting innecesario (uso excesivo de Atomics.wait) ya que puede congelar el hilo principal.
  • Usa try/catch y validación de tipos para evitar errores difíciles de depurar.

--

--

No responses yet