A user_4a13383d 2026-09-03 00:17 8 次浏览 Javascript 并发控制,同时限制执行任务 通过并发控制,同时限制执行任务,充分利用网络资源,提高接口请求效率。 ## 目录 * [一、实战案例](#%E4%B8%80%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8B) * [二、执行过程详解](#%E4%BA%8C%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3) * [三、应用场景](#%E4%B8%89%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF) * [四、实现 limitConcurrency 函数](#%E5%9B%9B%E5%AE%9E%E7%8E%B0-limitconcurrency-%E5%87%BD%E6%95%B0) * [五、关键点拆解](#%E4%BA%94%E5%85%B3%E9%94%AE%E7%82%B9%E6%8B%86%E8%A7%A3) ## 一、实战案例 假设有 7 个请求任务(响应时间各不相同),我们希望**最多同时执行 3 个**,避免一次性把服务器打爆。 先说明一下 `tasks`:它是任务列表,每个元素都是一个「返回 Promise 的函数」,形如: ```javascript const tasks = [ () => fetch('/1'), () => fetch('/2'), () => fetch('/3'), () => fetch('/4'), // ... 共 7 个 ] ``` 为了模拟接口延迟,先准备几个工具函数,然后调用待实现的 `limitConcurrency`: ```javascript // 模仿接口延迟 const delay = (time = 1000) => new Promise((resolve) => { setTimeout(resolve, time) }) // 生成请求响应时间:1~9 秒 const generateRequestTime = () => Math.floor(Math.random() * 9) + 1; // mock 请求接口 const mockApi = async (res, requestTime) => { await delay(requestTime * 1000) return { task: res, requestTime } } // 待会要实现 limitConcurrency 函数 const nowTime = Math.floor(Date.now() / 1000); limitConcurrency(tasks, 3).then(results => { const endTime = Math.floor(Date.now() / 1000); console.log(`${endTime - nowTime}秒`, results) }) ``` 预期的输出结果如下(7 个任务的响应时间分别为 3、4、3、4、5、5、5 秒): ```text 12秒 [ { "task": 0, "requestTime": 3 }, { "task": 1, "requestTime": 4 }, { "task": 2, "requestTime": 3 }, { "task": 3, "requestTime": 4 }, { "task": 4, "requestTime": 5 }, { "task": 5, "requestTime": 5 }, { "task": 6, "requestTime": 5 } ] ``` ## 二、执行过程详解 7 个任务(响应时间 3、4、3、4、5、5、5 秒),并发数限制为 3,执行时间线如下: | 时间点 | 事件 | 正在执行 | | --- | --- | ---- | | 0s | 启动 0、1、2 | 0、1、2 | | 3s | 0、2 完成 → 启动 3、4 | 1、3、4 | | 4s | 1 完成 → 启动 5 | 3、4、5 | | 7s | 3 完成 → 启动 6 | 4、5、6 | | 12s | 全部完成 | — | **任何时候,正在执行的任务数都 ≤ 3。** > 如果不用并发控制,7 个任务一个接一个串行执行,总耗时 = 3 + 4 + 3 + 4 + 5 + 5 + 5 = **29 秒**;现在缩短到 **12 秒**,而且因为最后用了 `Promise.all`,返回结果的顺序仍然和任务列表保持一致。 ## 三、应用场景 并发控制这个能力,在真实项目里出现频率很高,随便举几个: ### 1\. 批量请求接口 需要一次性调用几十上百个接口时,如果全部同时发出,很可能触发服务端限流(比如返回 429),严重的还会被封 IP。 ```javascript // 100 个请求,最多同时跑 5 个,稳一点 limitConcurrency(tasks, 5) ``` ### 2\. 爬虫 / 数据抓取 抓取大量页面时,限制并发既是「礼貌」(别把人家网站打垮),也是「自保」(别被封)。一次只抓 3\~5 个,友好又安全。 ### 3\. 大文件分片上传 文件切成多个分片同时上传,但浏览器对同一域名的并发连接数有限制(HTTP/1.1 通常 6 个),得靠并发控制别把连接占满。 ### 4\. 批量入库 / 发消息 批量写数据库、批量发消息时,限制并发能避免数据库连接池被瞬间打爆。 一句话总结:**凡是「任务很多、但不能一次性全上」的场景,都能用上它。** ## 四、实现 limitConcurrency 函数 ```javascript async function limitConcurrency(tasks, limit) { if (typeof limit !== "number" || limit <= 0) { throw new Error( "limit 必须是大于 0 的整数", ); } if (!Array.isArray(tasks) || tasks.length === 0) return []; // 不需要并发控制 if (limit >= tasks.length) { return Promise.all(tasks.map((task) => Promise.resolve().then(task))); } const results = []; const executing = new Set(); for (const [index, task] of tasks.entries()) { const p = Promise.resolve() .then(task) .then((value) => { results[index] = value; return value; }) .finally(() => { executing.delete(p); }); executing.add(p); // 达到并发上限,等待任意一个任务完成 if (executing.size >= limit) { await Promise.race(executing); } } // 等待最后剩余的任务 await Promise.all(executing); return results; } ``` ## 五、关键点拆解 ### 1\. 为什么用 Promise\.resolve\(\)\.then\(\) 包装任务? 最直观的写法,可能是直接调用: ```javascript const p = task() ``` 但这里有个坑:如果 `task` 是一个会**同步抛错**的函数(`throw new Error('出错了')`),这个错误会直接中断整个 `for` 循环,后面的任务一个都执行不了。 用 `Promise.resolve().then(task)` 包装后,一举两得: * **保证异步**:任务始终以微任务方式执行,不会同步阻塞循环; * **兜住错误**:同步抛出的错误会被捕获进 Promise 的 reject 链,不会中断循环。 ### 2\. 为什么用 Set 管理 executing,并用 finally 移除? 这里用 `Set` 而不是数组,是因为我们只需要「加入」和「移除」两个动作,`Set` 的 `add` / `delete` 语义更直白,也省去了用数组时 `indexOf` 定位元素的麻烦。 每个任务都挂一个 `.finally()`,无论任务**成功还是失败**,都会把自己从 `executing` 里移除,腾出一个并发位: ```javascript const p = Promise.resolve() .then(task) .then((value) => { results[index] = value; // 记录结果 return value; }) .finally(() => { executing.delete(p); // 完成后从集合中移除,腾出位置 }); ``` > 之所以用 `.finally()` 而不是 `.then()`:如果任务失败(reject),`.then()` 的成功回调不会执行,任务就会一直占着并发位不放;`.finally()` 无论成功失败都会执行,保证并发位一定能被释放。 ### 3\. Promise\.race\(executing\) 的作用 当 `executing` 的数量达到 `limit`(`executing.size >= limit`),`await Promise.race(executing)` 会「卡住」循环,直到集合里**任意一个**任务完成、被移除,才继续启动下一个任务——这正是「限制并发数」的核心。 > `Promise.race` 接受任意可迭代对象(数组、Set 等),所以直接把 `Set` 传进去就能用,不用再转成数组。 点赞0 收藏0
评论
还没有评论,来分享你的想法吧。