JavaScript — ES8 (ECMA2017)-Parte IV
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:
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 unSharedArrayBuffer
.
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
yCross-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
yAtomics.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
mediantepostMessage()
sin copiar su contenido: es compartido por referencia.
¿Cómo funciona internamente o a bajo nivel? —
SharedArrayBuffer
asigna un bloque de memoria binaria directamente accesible desde múltiples workers.- A diferencia de
ArrayBuffer
, los datos no se copian al pasarlos entre workers; se comparten. 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 aAtomics.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 comoInt32Array
para poder acceder (leer/escribir) a su contenido.shared[0]
— Accedemos al dato como un array normal y le asignamos un valor0
.
Atomics.load
— Lee un valor atómicamente.
// Sintaxis:
Atomics.load(typedArray, index)
// Ejemplo
const val = Atomics.load(shared, 0); // val = 10
console.log(val); // 10
Atomics.add
— Suma 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.sub
— Resta 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.and
— Aplica 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.or
— Aplica 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.xor
— Aplica 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.store
— Almacena 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.exhange
— Reemplaza 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 sumarle1
al valor que hay enshared[0]
de forma segura.Atomics.load(shared, 0)
— Lee el valor del índice0
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
yCOEP
). - 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 —
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 conAtomics
. - 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.