JavaScript
Secuencialidad, concurrencia y límites
Distingue ejecución secuencial y concurrente, muestra cómo iniciar operaciones independientes juntas y cómo limitar trabajo para no saturar recursos.
- Última actualización
- Actualizada
- Nivel
- Aplicación
JavaScript
Distingue ejecución secuencial y concurrente, muestra cómo iniciar operaciones independientes juntas y cómo limitar trabajo para no saturar recursos.
const user = await loadUser(userId);
const orders = await loadOrders(user.id);La segunda operación necesita el resultado de la primera.
for (const migration of migrations) {
await migration.run();
}Aunque las migraciones no compartan datos directos, el orden puede formar parte del contrato.
const [profile, permissions] = await Promise.all([
loadProfile(userId),
loadPermissions(userId),
]);Las operaciones empiezan sin esperar el resultado de la otra.
Dos fetches concurrentes pueden progresar a la vez gracias al host y la red. Dos cálculos JavaScript normales continúan utilizando el mismo hilo del agente salvo que uses workers u otro entorno paralelo.
await Promise.all(
files.map(uploadFile),
);Con diez archivos puede funcionar. Con cien mil puede saturar:
async function mapWithConcurrency(
items,
limit,
operation,
) {
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (true) {
const index = nextIndex++;
if (index >= items.length) {
return;
}
results[index] = await operation(
items[index],
index,
);
}
}
const workerCount = Math.min(limit, items.length);
await Promise.all(
Array.from(
{ length: workerCount },
() => worker(),
),
);
return results;
}Mantiene hasta limit operaciones activas y conserva el orden de resultados.
Un pool debe definir:
No existe una política universal.
Backpressure significa que el productor no debe generar trabajo más rápido de lo que el consumidor puede procesar.
productor rápido
↓
cola crece sin límite
↓
memoria y latencia aumentanStreams e iteradores asíncronos permiten consumir de forma gradual.
Un proveedor puede permitir cierta cantidad de solicitudes por periodo. Limitar concurrencia no es idéntico a limitar tasa.
concurrencia → cuántas están activas
rate → cuántas comienzan por tiempoPuedes necesitar ambas.
Reintenta únicamente fallos transitorios y operaciones seguras.
async function retry(operation, attempts) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!isTransient(error) || attempt === attempts) {
throw error;
}
await wait(calculateBackoff(attempt));
}
}
throw lastError;
}Un pago o creación puede duplicarse si no existe idempotencia.
let latestRequest = 0;
async function search(query) {
const requestId = ++latestRequest;
const results = await searchProducts(query);
if (requestId !== latestRequest) {
return;
}
renderResults(results);
}Evita que una respuesta antigua reemplace una búsqueda nueva. Cancelar la anterior suele ser mejor cuando la API lo permite.
const results = await Promise.allSettled(
items.map(saveItem),
);Decide qué significa éxito parcial. No presentes “todo guardado” cuando algunas operaciones fallaron.
const results = await mapWithConcurrency(
products,
5,
async (product) => {
validateProduct(product);
return saveProduct(product);
},
);El límite protege la base de datos y el proceso cliente.
¿Por qué Promise.all con miles de uploads puede ser peor que un pool?
Porque inicia todas las operaciones inmediatamente y puede saturar memoria, conexiones y límites del servicio. Un pool mantiene una cantidad controlada de trabajo activo.
Cancelación con AbortController explica cómo una operación coopera para dejar de trabajar cuando su resultado ya no se necesita.