53、控制并发,100 个请求,每次最多 10 个?

张开发
2026/6/30 4:47:18 15 分钟阅读
53、控制并发,100 个请求,每次最多 10 个?
目录一、先说思路二、最容易讲清楚的思路三、面试里的标准回答四、代码实现 1推荐版示例代码使用方式五、这段代码为什么能控制并发六、如果面试官追问为什么不用 Promise.all七、代码实现 2更像“并发池”的写法使用方式八、这类题面试官真正想听什么1. 保证结果顺序2. 异常处理3. limit 可能大于任务数4. 请求函数而不是请求结果九、最适合面试的简洁实现测试代码十、如果题目变成“请求失败后重试一次”怎么办十一、面试高分回答模板十二、一句话总结这是前端面试里的经典题。本质考察的是Promise 理解任务调度能力并发控制思路异常处理是否完整一、先说思路题目意思是一共有100 个异步请求但不能一下子全发出去要保证同一时刻最多只有 10 个请求在执行某个请求完成后再补上下一个这其实就是一个并发池 / 任务池问题。二、最容易讲清楚的思路可以这样理解先启动前10 个任务谁先执行完就从剩余任务里再取一个补上一直到 100 个任务全部完成一句话概括始终让执行中的任务数不超过 10完成一个补一个。三、面试里的标准回答这类题我会用“任务池”来做并发控制。先维护一个最大并发数比如 10。初始化时先执行 10 个请求后续每当某个请求完成就从待执行队列里再取一个补进去这样能保证同时运行的请求最多只有 10 个直到所有任务执行完成。实现上通常会基于 Promise、递归调度或者维护一个执行中的任务集合来做。这段先说出去思路就已经比较清楚了。四、代码实现 1推荐版假设我们有 100 个请求函数每个函数调用后返回一个 Promise。示例代码async function runWithLimit(tasks, limit 10) { const results new Array(tasks.length) let nextIndex 0 async function worker() { while (nextIndex tasks.length) { const currentIndex nextIndex try { results[currentIndex] await tasks[currentIndex]() } catch (error) { results[currentIndex] error } } } const workers Array.from( { length: Math.min(limit, tasks.length) }, () worker() ) await Promise.all(workers) return results }使用方式const tasks Array.from({ length: 100 }, (_, i) { return () new Promise((resolve) { setTimeout(() { console.log(第 ${i 1} 个请求完成) resolve(i 1) }, Math.random() * 2000) }) }) runWithLimit(tasks, 10).then((res) { console.log(全部完成, res) })五、这段代码为什么能控制并发核心点在这里const workers Array.from( { length: Math.min(limit, tasks.length) }, () worker() )这句只创建了10 个 worker。每个 worker 都会不断从任务列表里取下一个任务执行while (nextIndex tasks.length) { const currentIndex nextIndex results[currentIndex] await tasks[currentIndex]() }因为一共只有 10 个 worker所以同一时刻最多也就 10 个任务在跑。六、如果面试官追问为什么不用Promise.all你可以这样回答Promise.all(tasks.map(fn fn()))会一次性把 100 个请求全部发出去虽然最终能一起拿结果但不能限制并发数。题目要求的是“最多同时 10 个”所以必须自己做调度维护一个并发池。这个点很关键。七、代码实现 2更像“并发池”的写法这种写法也很常见适合展示你对“执行中队列”的理解。function limitConcurrency(tasks, limit 10) { const results [] let i 0 const executing [] function enqueue() { if (i tasks.length) { return Promise.resolve() } const index i const p Promise.resolve() .then(() tasks[index]()) .then((res) { results[index] res }) .catch((err) { results[index] err }) executing.push(p) const clean () executing.splice(executing.indexOf(p), 1) p.then(clean).catch(clean) let r Promise.resolve() if (executing.length limit) { r Promise.race(executing) } return r.then(() enqueue()) } return enqueue().then(() Promise.all(executing)).then(() results) }使用方式const tasks Array.from({ length: 100 }, (_, i) { return () new Promise((resolve) { setTimeout(() { console.log(任务 ${i 1} 完成) resolve(i 1) }, 1000) }) }) limitConcurrency(tasks, 10).then((res) { console.log(完成:, res) })八、这类题面试官真正想听什么不只是要你写出来还想看你有没有这些意识1. 保证结果顺序虽然请求完成顺序可能乱但返回结果最好按原任务顺序存放results[currentIndex] ...这样更专业。2. 异常处理不能一个请求失败就导致整个调度崩掉。所以通常会try { results[currentIndex] await tasks[currentIndex]() } catch (error) { results[currentIndex] error }或者返回统一结构results[currentIndex] { success: false, error }3. limit 可能大于任务数比如只有 6 个请求却设置并发 10。这时应该取Math.min(limit, tasks.length)4. 请求函数而不是请求结果通常tasks里放的是函数不是已经执行过的 Promise。因为如果你提前写成const tasks urls.map(url fetch(url))那请求在创建时就已经发出去了根本没法控制并发。正确做法是const tasks urls.map(url () fetch(url))这个点在面试里说出来会很加分。九、最适合面试的简洁实现如果面试现场手写我建议用这个版本最稳async function asyncPool(limit, tasks) { const results new Array(tasks.length) let index 0 async function run() { while (index tasks.length) { const current index try { results[current] await tasks[current]() } catch (e) { results[current] e } } } const workers [] for (let i 0; i Math.min(limit, tasks.length); i) { workers.push(run()) } await Promise.all(workers) return results }测试代码const tasks Array.from({ length: 100 }, (_, i) { return () new Promise((resolve) { setTimeout(() { console.log(请求 ${i 1} 完成) resolve(结果 ${i 1}) }, Math.random() * 1000) }) }) asyncPool(10, tasks).then((res) { console.log(res) })十、如果题目变成“请求失败后重试一次”怎么办你可以顺手补一句如果要支持失败重试可以把每个任务再包装一层 retry 逻辑比如失败后自动重试 1~2 次再把最终结果放进并发池执行。例如function retry(task, count 2) { return async function () { let lastError for (let i 0; i count; i) { try { return await task() } catch (e) { lastError e } } throw lastError } }十一、面试高分回答模板这类题我会用并发池来实现。核心思路是先启动 10 个请求后续哪个请求完成了就从剩余任务里再补一个进去始终保证同时执行的请求数不超过 10直到所有 100 个请求都完成。实现上我一般不会直接存 Promise而是存返回 Promise 的任务函数这样可以避免请求提前发出。同时我会注意几个点返回结果按原顺序保存单个请求失败不能影响整个调度并发数要和任务总数取最小值。本质上就是一个任务调度问题可以用 worker 模型或者 Promise.race 的并发池模型实现。十二、一句话总结100 个请求并发限制 10 个本质就是维护一个大小为 10 的任务池先跑 10 个完成一个补一个直到全部执行完。

更多文章