在开发中,我们会遇到处理大量的异步操作,例如发送网络请求、读取文件等。当并发请求数量过多时,可能会导致浏览器或服务器性能下降,甚至出现崩溃的情况,并且会导致一些请求挂起。本文将介绍如何使用 Promise 来实现并发池来控制请求数量,从而提高的性能和稳定性。
我们先聊聊什么是并发:
并发(Concurrency)是指在同一时间段内处理多个任务的能力。这并不意味着这些任务在同一时刻被执行,而是指它们在时间上重叠,交替进行,从而给人一种同时执行的感觉。
举个生活中的例子:你在做饭的同时,可能会接听电话,或者回复短信。虽然你不能同时进行这两项操作,但你可以在短时间内快速切换,从而在一段时间内同时处理这两件事情,这就是并发。
而JavaScript是单线程语言在处理并发一种基于事件循环(Event Loop)的单线程并发模型
哪我们实践开发中会遇到一些并发问题:
在某些场景下,我们需要同时发起大量的异步请求。例如:
- 大文件分片上传:将一个大文件分割成多个小片段,然后并发上传这些片段。
- 图片批量加载:在一个页面中加载大量的图片。
- 数据批量处理:同时向服务器发送多个请求以获取或更新数据。
当并发请求数量过多时,可能会出现以下问题:
- 浏览器或服务器资源耗尽:每个请求都会占用一定的内存、CPU 和网络带宽。过多的并发请求可能导致资源耗尽,从而影响页面响应速度甚至导致浏览器崩溃。
- 服务器过载:服务器同时处理过多的请求可能会导致负载过高,从而影响服务器的性能和稳定性。
- 请求超时:由于资源限制或服务器过载,某些请求可能会超时,从而导致操作失败。
由于 JavaScript 是单线程语言,对高并发处理并不友好。因此,我们需要一种机制来控制并发请求的数量,以避免上述问题的发生。
这里使用并发池控制并发
并发池是一种常用的控制并发请求数量的技术。它的基本思想是:
- 维护一个请求队列和一个正在执行的请求数组。
- 设置一个最大并发数。
- 当有新的请求时,将其添加到请求队列中。
- 如果正在执行的请求数量小于最大并发数,则从请求队列中取出一个请求并执行。
- 当一个请求完成后,将其从正在执行的请求数组中移除,并从请求队列中取出一个新的请求执行。
为实现并发池,这里我的选择是使用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秒后暂停
在这个示例中:
- 首先创建了一个
ConcurrentPool实例,设置最大并发数为3,这意味着同时最多会有3个 HTTP 请求在发送。 - 定义了一个
fetchPageContent函数,用于使用axios发送单个 HTTP 请求并获取网页内容,如果请求出错则返回null。 - 遍历要请求的
urls列表,针对每个url,向并发池中添加一个任务,任务就是调用fetchPageContent函数获取对应网页内容。添加任务时返回的Promise用于处理任务完成后的结果(成功或失败)。 - 使用
setTimeout设置了在程序运行3秒后暂停任务池,暂停5秒后再恢复任务池的操作,以展示pause和resume功能的使用。
这样就可以在控制并发数量的情况下,有效地处理多个异步的 HTTP 请求任务,并能根据需要暂停和恢复任务池的运行。
以上就是使用promise控制并发请求的全部内容,如有问题欢迎指正!