🚦 手写 Promise 并发控制:如何优雅限制异步请求的并发数?

问题场景

需要一次性处理几百个请求/任务,如果直接全部 Promise.all,瞬间并发太多:

js 复制代码
// 100 个请求同时发出 → 服务器扛不住,甚至被限流/封 IP
const results = await Promise.all(
  urls.map(u => fetch(u))
)

浏览器对同一域名的并发连接本身有限制,但你自己发起的异步任务(HTTP 请求、爬虫、批量图片处理、API 限速)并不会被自动节流。结果就是:内存暴涨、服务器 429、超时重试满天飞。

你需要一个限制并发数的调度器:同时最多 N 个在跑,跑完一个补一个。

原因分析

Promise.all 是"一次全开",没有并发上限的概念,也没有让任务在"队列里等待"的机制。要实现并发控雨,本质是手动维护一个任务队列 + 运行中的计数:每当一个任务结束(无论成功失败),就从队列里弹出下一个去执行,始终保持同时进行中的任务数 ≤ N。

解决方案

方案 1:最简版 ------ p-limit 式思想(推荐理解)

js 复制代码
function createLimiter(limit) {
  const queue = [];        // 等待队列
  let active = 0;          // 进行中数量

  function next() {
    if (active >= limit || queue.length === 0) return;
    const { fn, resolve, reject } = queue.shift();
    active++;
    Promise.resolve()
      .then(fn)
      .then(resolve, reject)
      .finally(() => { active--; next(); });  // 结束,补下一个
  }

  return function enqueue(fn) {
    return new Promise((resolve, reject) => {
      queue.push({ fn, resolve, reject });
      next();
    });
  };
}

// 用法
const limit = createLimiter(3);   // 最多 3 个并发
const results = await Promise.all(
  urls.map(u => limit(() => fetch(u).then(r => r.json())))
);

关键点:

  • enqueue 返回新 Promise,把任务的 resolve/reject 存进队列,不立刻执行。
  • next() 负责"名额有空就发车",任务完成(finally)后再拉下一个,形成滚动执行。
  • 成功/失败都走 resolve/reject,且不影响队列继续跑(异常被捕获、不会拖垮整个批处理)。

方案 2:m 个一组 Promise.all 分波(简单但效率略低)

js 复制代码
async function chunkAll(items, limit, worker) {
  const results = [];
  for (let i = 0; i < items.length; i += limit) {
    const batch = items.slice(i, i + limit);
    results.push(...await Promise.all(batch.map(worker)));
  }
  return results;
}

缺点:前一批全部完成才开后一批,如果第一批里有个任务很慢,其余快的任务也得干等,整体吞吐不如滚动式。

方案 3:现成库

bash 复制代码
npm i p-limit   # 生态最广
js 复制代码
import pLimit from 'p-limit';
const limit = pLimit(3);
const results = await Promise.all(urls.map(u => limit(() => fetch(u))));

p-limit 内部实现和方案 1 基本相同,生产直接用这个,稳定可靠。

要点总结

  • Promise.all 无并发上限,批量任务必须手动限流,否则服务器 429 / 内存爆掉。
  • 核心是 任务队列 + 进行中计数 :结束一个(finally)就补下一个,保持并发数恒定 ≤ N。
  • 推荐滚动式 (跑完即补)而不是分波式(一批全完再下一批),后者会被慢任务拖累吞吐。
  • 任务无论成功失败都要 next(),别用 then 而漏了 catch 导致队列停摆------务必 finally。
  • 生产项目直接上 p-limit,自写用于学习和理解原理。
  • 进阶:可再加重试 (失败入队重试)、超时 (Promise.race 兜底)、优先级(队列按权重排序)。

一句话:并发控制 = "同时只放 N 个在跑"的队列调度器。理解了 queue + active + next() 三个要素,你就吃透了所有并发库的底层逻辑。

相关推荐
FungLeo2 小时前
成为全栈·React 管理后台篇·总复盘:从“接口能调通”到“后台值得使用”
前端·react.js·前端框架·项目复盘·成为全栈
Csvn4 小时前
并发模式:让渲染学会排队、插队和让路
前端
可乐鸡翅yeah_4 小时前
新手梳理:M3U8 线上问题,哪些是前端锅,哪些是后端锅
前端·ios·音视频·实时音视频·m3u8·音视频在线播放
Flynt4 小时前
Linear 用 1000 个 PR 换掉 styled-components,我写了 200 个按钮,把这笔账复现了一遍
前端·css·preact
JavaGuide5 小时前
NVIDIA 又开源了!这次给 AI Agent 加上权限管控
前端·后端
excel5 小时前
prisma 如何处理数据库竞态
前端·数据库·后端
莪_幻尘6 小时前
Skill 体检:30 个 Skill 全凭感觉?体检器先自曝了 8 个“假 0 分
前端·人工智能·llm
风骏时光牛马6 小时前
AI模型综合能力评测:性能、指令遵循与多场景实测对比
前端
Frag0ut7 小时前
Chrome与Chromium内核浏览器在Windows 11上的新特性全景解析
前端·chrome·windows·web安全·chromium·gemini ai·playready drm
hiahiahia1237 小时前
实现完整 Tool Dispatcher
开发语言·前端