Planteamiento y contexto
Esta pregunta evalúa la concurrencia en el navegador, la memoria compartida y el control del ciclo de vida. Atomics.waitAsync() devuelve una Promise mientras una posición de entero compartida todavía tenga el valor esperado, lo que permite al hilo que la llama continuar gestionando el trabajo de la interfaz de usuario. Solo acepta una vista de tipo Int32Array o BigInt64Array respaldada por un SharedArrayBuffer. Una respuesta sólida vincula el protocolo de espera con la propiedad, la cancelación y un fallback funcional.
Qué evalúa el entrevistador
- Si distingues la espera no bloqueante en el hilo principal del uso potencialmente bloqueante de
Atomics.wait()en un Worker. - Si los valores de estado, el momento de notificación, los timeouts y las notificaciones duplicadas cuentan con invariantes explícitos.
- Si la cancelación, el ocultamiento de la página, las caídas de Workers y la liberación del buffer se gestionan de forma segura.
- Si comprendes el aislamiento de origen cruzado (cross-origin isolation), la detección de capacidades y los fallbacks basados en Transferable.
Preguntas para clarificar
Confirma si se pueden descartar datos, el objetivo de latencia, la cantidad de productores y consumidores, y si la memoria debe compartirse entre Workers. Pregunta sobre la matriz de compatibilidad de navegadores y si la página puede usar un contexto seguro y aislamiento de origen cruzado. Verifica si scripts de terceros, iframes o ventanas emergentes de inicio de sesión se ven afectados por COOP/COEP. Define si la cancelación implica una detención por parte del usuario, un timeout o la destrucción de la página.
Esquema de respuesta en 30 segundos
Dividiría la región compartida en palabras de control y ranuras de datos, utilizando estados versionados como 0=waiting, 1=ready, 2=cancelled y 3=closed. Un consumidor lee el estado atómicamente y llama a Atomics.waitAsync solo mientras está esperando. Un productor escribe los datos, publica el estado con Atomics.store y llama a Atomics.notify. La Promise es solo un resultado de reactivación; el consumidor debe releer el estado antes de procesar. El timeout y la cancelación son transiciones de estado, y el buffer se libera solo después de que cada Worker haya finalizado. Los navegadores no compatibles usan fragmentos de tipo Transferable con la misma semántica de cancelación y errores.
Solución paso a paso
1. Definir el estado compartido y la propiedad
El área de control debe incluir una versión del protocolo, estado, secuencia, longitud y una bandera de cierre. Un productor solo escribe en las ranuras de las que es propietario y publica la disponibilidad después de que la escritura finaliza. Un consumidor lee el estado y la secuencia antes de procesar; no puede tratar una sola notificación como un mensaje duradero único. Cada reactivación vuelve a verificar el estado porque las notificaciones pueden fusionarse, competir entre sí o hacer referencia a un lote anterior.
2. Usar waitAsync y notify correctamente
Atomics.waitAsync(view, index, expected, timeout) primero compara la posición de memoria. Un valor diferente devuelve not-equal; un timeout devuelve timed-out; de lo contrario, devuelve una Promise que se puede esperar con await. Tras cambiar el estado, el productor llama a Atomics.notify(view, index, count). El protocolo se puede representar como:
const control = new Int32Array(new SharedArrayBuffer(16));
const WAITING = 0;
const READY = 1;
const CANCELLED = 2;
async function waitForData(timeout = 1000) {
const result = Atomics.waitAsync(control, 0, WAITING, timeout);
const outcome = result.async ? await result.value : result.value;
const state = Atomics.load(control, 0);
return { outcome, state };
}
function publishData() {
Atomics.store(control, 0, READY);
Atomics.notify(control, 0, 1);
}3. Agregar estados de cancelación, timeout y cierre
No intentes interrumpir una Promise existente. En su lugar, escribe un estado de cancelación y notifica a los hilos en espera. Después de recibir ok, timed-out o not-equal, el hilo en espera vuelve a leer el estado y decide si continuar, reintentar o salir. El cierre primero detiene la producción, luego marca la región como cerrada y notifica a todos los hilos en espera; solo después de confirmar la salida de los Workers se debe liberar el buffer.
4. Manejar condiciones de carrera y contrapresión (backpressure)
Múltiples productores necesitan CAS (compare-and-swap) o un asignador para reclamar ranuras; no deben escribir la misma secuencia de manera concurrente. Cuando la cola esté llena, elige descartar, sobrescribir o aplicar contrapresión según el contrato del producto, y registra la profundidad y los descartes. Un consumidor debe vaciar todos los números de secuencia publicados en un bucle en lugar de interpretar una reactivación como un único mensaje.
5. Gestionar caídas y el ciclo de vida de la página
El hilo principal es propietario de la máquina de estados de los Workers y del latido (heartbeat). Ante un error o límite de tiempo, detén nuevas escrituras, marca la tarea como fallida y solicita a los Workers restantes que salgan. Al ocultar la página o desmontar el componente, envía una cancelación, espera un periodo acotado y luego termina los Workers. Un reinicio crea una nueva versión de la región de control en lugar de reutilizar valores obsoletos de secuencia y estado.
6. Detectar capacidades y degradar de forma segura
Al inicio, verifica crossOriginIsolated, SharedArrayBuffer, Atomics.waitAsync y el soporte de Workers. El aislamiento de origen cruzado altera el comportamiento de recursos de terceros y ventanas emergentes, por lo que el rendimiento no es una razón para debilitar la política de seguridad. Sin las capacidades requeridas, utiliza fragmentos de tipo Transferable ArrayBuffer o un postMessage ordinario, preservando los campos de cancelación, timeout, progreso y error. Mide la proporción de uso de cada vía y la latencia en la cola.
Respuesta modelo de alta calidad
Detectaría las capacidades y verificaría los requisitos de contexto seguro y aislamiento de origen cruzado antes de crear una región de control versionada. Un productor publica el estado de manera atómica tras escribir los datos y notifica a los hilos en espera. Un consumidor llama a Atomics.waitAsync solo mientras el estado sea de espera, y luego vuelve a leer el estado y la secuencia sin importar el resultado de la Promise. La cancelación, el timeout y el cierre son estados explícitos. El apagado detiene la producción, despierta a los hilos en espera, confirma la salida de los Workers y solo entonces libera la memoria compartida. La propiedad y los números de secuencia evitan el consumo duplicado, mientras que el comportamiento ante una cola llena es una decisión de producto medida. Si la memoria compartida o la API no están disponibles, los fragmentos Transferable proporcionan el mismo contrato de ciclo de vida y errores.
Errores comunes
- Asumir que
notifygarantiza exactamente una reactivación por mensaje. - Invocar una espera bloqueante en el hilo principal y congelar la interfaz de usuario.
- Confiar en el resultado de la Promise sin volver a leer el estado compartido y la secuencia.
- Liberar el buffer inmediatamente tras la cancelación mientras los Workers aún se están ejecutando.
- Ignorar las restricciones de aislamiento de origen cruzado y recursos de terceros.
- Cambiar a
postMessagepero perder la semántica de timeout, cancelación o errores.
Preguntas de seguimiento
¿Cuándo elegirías waitAsync en lugar de wait?
waitAsync devuelve una Promise sin bloquear el hilo que la llama, lo cual es ideal para el hilo principal y el código orientado a eventos. wait puede bloquear un Worker, pero solo cuando el tiempo de bloqueo de ese Worker no perjudique a la interfaz de usuario ni a otro trabajo crítico. Ambos requieren comprobaciones de estado, timeouts y un protocolo de cierre.
¿Por qué volver a leer el estado después de una notificación?
Una notificación solo indica que una condición puede haber cambiado. Las notificaciones pueden fusionarse, despertar a un consumidor competidor o corresponder a un lote anterior. Leer el estado, la secuencia y la longitud determina si los datos están realmente disponibles.
¿Cómo evitas que se realice trabajo tras una cancelación?
Publica la cancelación en el estado compartido y compruébala al reclamar una ranura, antes de procesar y antes de confirmar el resultado. Incluye una versión de la tarea en los mensajes de resultado; el hilo principal rechazará los resultados provenientes de versiones revocadas.
¿Cuándo se debe abandonar la memoria compartida?
Usa fragmentos Transferable cuando las cabeceras de aislamiento rompan dependencias críticas, la cobertura de navegadores sea insuficiente, el costo de depuración supere el beneficio o el rendimiento requerido sea moderado. Deja que la detección de capacidades y la telemetría elijan la vía en lugar de asumir que la memoria compartida es siempre la optimización adecuada.