青训营 使用 Promise控制并发请求 | 豆包MarsCode AI刷题

216 阅读7分钟

在开发中,我们会遇到处理大量的异步操作,例如发送网络请求、读取文件等。当并发请求数量过多时,可能会导致浏览器或服务器性能下降,甚至出现崩溃的情况,并且会导致一些请求挂起。本文将介绍如何使用 Promise 来实现并发池来控制请求数量,从而提高的性能和稳定性。

我们先聊聊什么是并发:

并发(Concurrency)是指在同一时间段内处理多个任务的能力。这并不意味着这些任务在同一时刻被执行,而是指它们在时间上重叠,交替进行,从而给人一种同时执行的感觉。

举个生活中的例子:你在做饭的同时,可能会接听电话,或者回复短信。虽然你不能同时进行这两项操作,但你可以在短时间内快速切换,从而在一段时间内同时处理这两件事情,这就是并发。

而JavaScript是单线程语言在处理并发一种基于事件循环(Event Loop)的单线程并发模型

哪我们实践开发中会遇到一些并发问题:

在某些场景下,我们需要同时发起大量的异步请求。例如:

  • 大文件分片上传:将一个大文件分割成多个小片段,然后并发上传这些片段。
  • 图片批量加载:在一个页面中加载大量的图片。
  • 数据批量处理:同时向服务器发送多个请求以获取或更新数据。

当并发请求数量过多时,可能会出现以下问题:

  • 浏览器或服务器资源耗尽:每个请求都会占用一定的内存、CPU 和网络带宽。过多的并发请求可能导致资源耗尽,从而影响页面响应速度甚至导致浏览器崩溃。
  • 服务器过载:服务器同时处理过多的请求可能会导致负载过高,从而影响服务器的性能和稳定性。
  • 请求超时:由于资源限制或服务器过载,某些请求可能会超时,从而导致操作失败。

由于 JavaScript 是单线程语言,对高并发处理并不友好。因此,我们需要一种机制来控制并发请求的数量,以避免上述问题的发生。

这里使用并发池控制并发

并发池是一种常用的控制并发请求数量的技术。它的基本思想是:

  1. 维护一个请求队列和一个正在执行的请求数组。
  2. 设置一个最大并发数。
  3. 当有新的请求时,将其添加到请求队列中。
  4. 如果正在执行的请求数量小于最大并发数,则从请求队列中取出一个请求并执行。
  5. 当一个请求完成后,将其从正在执行的请求数组中移除,并从请求队列中取出一个新的请求执行。

为实现并发池,这里我的选择是使用Promise对象并使用axios,模拟实践情况。

先简单介绍Promise对象:

Promise 是 JavaScript 中用于处理异步操作的一种机制。它代表一个异步操作的最终结果,可以是成功(resolved)或失败(rejected)。通过 Promise,我们可以更优雅地编写异步代码,避免回调地狱(callback hell)。

一个 Promise 对象有三种状态:

  • Pending(进行中)
  • Fulfilled(已成功)
  • Rejected(已失败)

以下是Promise对象的一个使用的简单示例:

// 创建一个 Promise 对象
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = Math.random() < 0.8; // 模拟 80% 成功率
    if (success) {
      resolve('操作成功'); // 成功时调用 resolve
    } else {
      reject('操作失败'); // 失败时调用 reject
    }
  }, 1000);
});

// 使用 then 方法处理成功结果,使用 catch 方法处理失败结果
promise
  .then(result => {
    console.log(result); // 输出 "操作成功"
  })
  .catch(error => {
    console.error(error); // 输出 "操作失败"
  });

以下是并发池的代码:

// 引入axios库,用于发送HTTP请求等相关操作
import axios from 'axios';

// 定义一个名为ConcurrentPool的类,用于管理并发任务
class ConcurrentPool {
    // 构造函数,用于初始化ConcurrentPool实例
    constructor(maxConcurrency) {
        // 允许同时运行的最大任务数量
        this.maxConcurrency = maxConcurrency;
        // 存储等待执行的任务队列,每个元素是一个包含任务、resolve和reject函数的对象
        this.pendingQueue = [];
        // 存储正在运行的任务列表
        this.runningTasks = [];
        // 添加一个标志,用于表示任务池是否处于暂停状态,初始化为false
        this.isPaused = false; 
    }

    // 向任务池中添加一个任务
    add(task) {
        // 返回一个Promise,以便在任务完成时进行相应的处理(resolve或reject)
        return new Promise((resolve, reject) => {
            // 将任务及其对应的resolve和reject函数封装成一个对象,添加到等待队列中
            this.pendingQueue.push({ task, resolve, reject });
            // 尝试运行任务,检查是否有任务可以立即执行
            this.run();
        });
    }

    // 异步方法,用于运行任务
    async run() {
        // 如果任务池处于暂停状态,或者正在运行的任务数量已经达到最大并发限制,或者等待队列中没有任务了,就直接返回,不执行新任务
        if (this.isPaused || this.runningTasks.length >= this.maxConcurrency || this.pendingQueue.length === 0) {
            return;
        }

        // 从等待队列中取出第一个任务及其对应的resolve和reject函数
        const { task, resolve, reject } = this.pendingQueue.shift();
        // 将取出的任务添加到正在运行的任务列表中
        this.runningTasks.push(task);

        try {
            // 执行任务,并等待任务完成,获取任务的结果
            const result = await task();
            // 如果任务成功完成,调用resolve函数,将结果传递出去,以解决对应的Promise
            resolve(result);
        } catch (error) {
            // 如果任务执行过程中出现错误,调用reject函数,将错误传递出去,以拒绝对应的Promise
            reject(error);
        } finally {
            // 无论任务成功还是失败,都从正在运行的任务列表中移除该任务
            this.runningTasks.splice(this.runningTasks.indexOf(task), 1);
            // 再次尝试运行任务,检查是否有其他等待的任务可以执行
            this.run();
        }
    }

    // 获取当前正在运行的任务数量
    getRunningTasksCount() {
        return this.runningTasks.length;
    }

    // 获取当前等待执行的任务数量
    getPendingTasksCount() {
        return this.pendingQueue.length;
    }

    // 暂停任务池,设置暂停标志为true
    pause() {
        this.isPaused = true;
    }

    // 恢复任务池的运行,设置暂停标志为false,并尝试运行任务
    resume() {
        this.isPaused = false;
        this.run(); 
    }
}

以上代码实现了一个简单的任务并发池,能够控制同时执行的任务数量,支持添加任务、暂停和恢复任务池的运行,并可以获取正在运行和等待执行的任务数量等功能。例如,可以用于在限制并发请求数量的情况下,处理一系列异步任务(如使用 axios 发送多个 HTTP 请求等)。

使用示例:

import axios from 'axios';

// 创建一个ConcurrentPool实例,设置最大并发数为3
const concurrentPool = new ConcurrentPool(3);

// 模拟一些要请求的URL列表
const urls = [
    'page1',
    'page2',
    'page3',
    'page4',
    'page5'
];

// 定义一个函数,用于发送单个HTTP请求并返回结果
const fetchPageContent = async (url) => {
    try {
        const response = await axios.get(url);
        return response.data;
    } catch (error) {
        console.error(`Error fetching ${url}:`, error);
        return null;
    }
};

// 遍历URL列表,向并发池中添加任务
urls.forEach((url) => {
    concurrentPool.add(() => fetchPageContent(url))
      .then((result) => {
            if (result) {
                console.log(`Successfully fetched content from ${url}`);
                // 在这里可以对获取到的内容进行进一步处理,比如解析、存储等
            }
        })
      .catch((error) => {
            console.error(`Failed to fetch content from ${url}:`, error);
        });
});

// 假设在某些情况下,我们想要暂停任务池一段时间,然后再恢复
setTimeout(() => {
    concurrentPool.pause();
    console.log('Task pool paused.');

    setTimeout(() => {
        concurrentPool.resume();
        console.log('Task pool resumed.');
    }, 5000); // 暂停5秒后恢复
}, 3000); // 运行3秒后暂停

在这个示例中:

  1. 首先创建了一个 ConcurrentPool 实例,设置最大并发数为 3,这意味着同时最多会有 3 个 HTTP 请求在发送。
  2. 定义了一个 fetchPageContent 函数,用于使用 axios 发送单个 HTTP 请求并获取网页内容,如果请求出错则返回 null
  3. 遍历要请求的 urls 列表,针对每个 url,向并发池中添加一个任务,任务就是调用 fetchPageContent 函数获取对应网页内容。添加任务时返回的 Promise 用于处理任务完成后的结果(成功或失败)。
  4. 使用 setTimeout 设置了在程序运行 3 秒后暂停任务池,暂停 5 秒后再恢复任务池的操作,以展示 pauseresume 功能的使用。

这样就可以在控制并发数量的情况下,有效地处理多个异步的 HTTP 请求任务,并能根据需要暂停和恢复任务池的运行。

以上就是使用promise控制并发请求的全部内容,如有问题欢迎指正!