100个请求,如何做好并发控制?

582 阅读3分钟

关于这个问题,”100个请求,如何实现并发控制“,不仅仅是面试中常见的问题,还是项目开发中真切存在的需求。做好请求的并发控制,无论是前端角度的浏览器性能瓶颈,还是服务器压力等,都是非常重要的性能优化手段。本文将从无控制、自定义并发控制、第三方库控制和Web Worker多线程角度来带你实现并发控制。

前置知识:

  1. Promise相关知识
  2. Fetch API
  3. Web Worker API
  4. 异步编程思想,这一点非常重要,因为JS是单线程的。

无控制,Promise.all直接并发:

  1. 先来看一下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

  1. 在此基础上,我们来看一下如果不控制,直接并发请求会是什么样子:
// 模拟代码
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来实现。

  1. 核心思路:通过维护一个正在执行的任务队列来限制同时进行的异步请求数量。
  • 初始化两个变量:

    • results:用于存放所有请求的Promise
    • runningPromises:用来存储当前正在请求的Promise
  • 按顺序处理请求:

    • 遍历urls 数组,每次通过api调用,生成一个Promise
    • 维护results数组,把请求的结果放进去
    • 把当前请求的Promise 们放进runningPromises 数组中
  • 并发控制(核心):

    • 如果当前executing数组长度达到了最大并发数量,就用await Promise.reac(runningPromises) 等待最先完成的任务。
    • 通过等待最先完成的任务,确保最大的并发数不会超出限制。
    • 移除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),我将会在近期更新~