关于这个问题,”100个请求,如何实现并发控制
“,不仅仅是面试中常见的问题,还是项目开发中真切存在的需求。做好请求的并发控制,无论是前端角度的浏览器性能瓶颈,还是服务器压力等,都是非常重要的性能优化手段。本文将从无控制、自定义并发控制、第三方库控制和Web Worker多线程角度来带你实现并发控制。
前置知识:
Promise相关知识
Fetch API
Web Worker API
- 异步编程思想,这一点非常重要,因为
JS
是单线程的。
无控制,Promise.all直接并发:
- 先来看一下Promise.all是什么:
- 来自MDN的解释:
Promise.all()
静态方法接受一个Promise
可迭代对象作为输入,并返回一个Promise
。当所有输入的 Promise 都被兑现时,返回的 Promise 也将被兑现(即使传入的是一个空的可迭代对象),并返回一个包含所有兑现值的数组。如果输入的任何Promise
被拒绝,则返回的Promise
将被拒绝,并带有第一个被拒绝的原因。 - 笔者的理解:
Promise.all
多用于控制并发请求(多个异步请求),传入一个数组(也可以是其他可迭代对象),数组中的每一项都是一个Promise
。随后Promise.all
便会等待数组中所有的Promise
完成,如果所有的都成功了就返回一个包含所有Promise结果的数组,否则呢,返回第一个失败的Promise
的错误。 - 基本用法:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // [1, 2, 3]
})
.catch(error => {
console.error(error);
});
注意点:只要有一个失败,就会被立即中断,如果要等待所有完成且失败不中断,可以使用Promise.allSettled
。
- 在此基础上,我们来看一下如果不控制,直接并发请求会是什么样子:
// 模拟代码
const requests = Array.from({ length: 100 }, (_, i) => fetch(`https://api.example.com/data/${i}`));
Promise.all(requests)
.then(responses => console.log('All requests finished', responses))
.catch(err => console.error('Some request failed', err));
通过代码,很显然,直接发送了100个请求,这样做显然是不好的,无论是浏览器压力还是服务端压力,因此项目中绝对不推荐这样使用,我们可以接下来探索,替换成以下方法。
自定义并发控制,每次发送一部分。
所以我们要设计一个程序让浏览器一次不发送那么多,当然还是用Promise.all
来实现。
- 核心思路:通过维护一个正在执行的任务队列来限制同时进行的异步请求数量。
-
初始化两个变量:
results
:用于存放所有请求的Promise
runningPromises
:用来存储当前正在请求的Promise
们
-
按顺序处理请求:
- 遍历
urls
数组,每次通过api
调用,生成一个Promise
- 维护
results
数组,把请求的结果放进去 - 把当前请求的
Promise
们放进runningPromises
数组中
- 遍历
-
并发控制(核心):
- 如果当前executing数组长度达到了最大并发数量,就用
await Promise.reac(runningPromises)
等待最先完成的任务。 - 通过等待最先完成的任务,确保最大的并发数不会超出限制。
- 移除executing中完成的任务,腾出空间,让新的请求进入。
- 如果当前executing数组长度达到了最大并发数量,就用
-
等待所有完成并返回结果: 最终,通过
Promise.all(results)
等待所有请求的Promise
完成,并返回最终的结果。
**一句话总结:**通过控制当前正在执行的请求数目(runningPromises
数组),每次发起请求时都检查当前并发数量,如果超过最大并发数,就等待最先完成的请求释放位置。 以上就是一个自定义并发控制的模型思想,接下来我们通过代码实现:
const limitConcurrency = async (urls, maxConcurrent) => {
const runningPromises = [];
const results = [];
const enqueue = async (url) => {
const promise = fetch(url).then(response => response.json());
results.push(promise);
// 限制并发数
if (runningPromises.length >= maxConcurrent) {
await Promise.race(runningPromises);
}
runningPromises.splice(0, 1); // 移除第一个完成的 promise
runningPromises.push(promise); // 把新的 promise 添加到队列中
return promise;
};
await Promise.all(urls.map(enqueue));
return results;
};
// 使用示例
const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/data/${i}`); // 1. Array.from生成一百个请求地址。
limitConcurrency(urls, 5).then(results => console.log('Finished:', results)); // 2. 函数执行
更快捷的方法:引入库;
这里就推荐使用p-limit
了,很轻量。
pnpm install p-limit
然后直接调用:
import pLimit from 'p-limit';
const limit = pLimit(5); // 设置最大并发数为 5
const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/data/${i}`);
const tasks = urls.map(url => limit(() => fetch(url).then(res => res.json())));
Promise.all(tasks)
.then(results => console.log('All tasks done:', results))
.catch(err => console.error('Error occurred:', err));
是的,库就是如此简单。但是面试中就要会原理了。。。
使用Web Worker!
(如果还不知道web Worker是什么,可以去看我之前的文章:Web Workder, 启动!)
总结来说使用Web Worker无非是以下三点:
- 并行执行(伪): Web Worker 可以让多个任务同时进行,充分利用多核 CPU 的性能(这一点有点违背JS单线程)。
- 主线程保护: 耗时操作放在 Web Worker 中执行,避免主线程卡顿,提升用户体验。
- 资源隔离: 每个 Worker 都有自己独立的执行环境,不会互相干扰。
话不多说,直接上代码:
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ urls, maxConcurrent: 5 });
worker.onmessage = (event) => {
const results = event.data;
console.log('Finished:', results);
};
// worker.js
onmessage = (event) => {
const { urls, maxConcurrent } = event.data;
// 在 worker 中实现并发控制逻辑,例如使用递归或异步队列
const limitConcurrency = async (urls, maxConcurrent) => {
// ... 与之前类似的逻辑
};
limitConcurrency(urls, maxConcurrent).then(results => {
postMessage(results);
});
};
更多,
当然,还可以使用更高级的用法(比如serviceWorker
),我将会在近期更新~