使用Promise控制请求并发数的有效方法
引言
在现代Web开发中,与服务器进行异步通信是常见的需求。然而,当需要同时发送多个请求时,如果不加以控制,可能会导致服务器负载过高,影响应用性能。本文将介绍如何使用Promise来控制请求的并发数,以确保请求能够有序地发送和处理,提高应用的效率和稳定性。
并发控制的意义
首先,异步编程是为了解决程序中耗时操作的执行问题。举个例子,假设你需要同时发送多个请求给服务器,等待它们都返回结果后再继续处理,但又不希望这些请求一个接一个地按顺序执行,因为这样会浪费时间。这时,我们可以利用Promise来控制请求的并发数,以提高效率。
Promise可以看作是一种表示异步操作结果的对象。它有两个重要的状态:进行中(pending)和已完成(fulfilled)或已拒绝(rejected)。当我们发送一个请求时,会创建一个对应的Promise对象,这个对象可以用来监听请求的结果。我们可以通过调用Promise的then方法,传入一个回调函数,在请求完成后执行相应的处理逻辑。
为了控制请求的并发数,我们可以设置一个计数器来记录当前正在进行的请求数量。当计数器小于并发数限制时,我们可以发送新的请求,计数器加1。当计数器达到并发数限制时,我们暂停发送新的请求,等待其中之一的请求完成,然后再继续发送下一个请求。这样就能保证同时发送的请求数量不超过设定的限制。
通过这种方式,我们可以合理地控制请求的并发数,避免对服务器造成过大的负荷,同时提高程序的执行效率。
总结起来,掌握Promise和异步编程的关键在于理解如何使用Promise对象来管理异步操作,利用计数器来控制并发数。这样我们就能更好地处理多个请求,提高应用的性能和稳定性。
深入理解并发, 并行,并发控制, 切片控制
并发(Concurrency)和并行(Parallelism)是计算机领域中常用的两个概念。
并发是指多个任务在同一个时间段内交替执行,它强调任务之间的交替执行和共享资源的竞争。在并发执行中,多个任务交替执行的顺序是不确定的,每个任务执行一段时间后,切换到下一个任务,通过快速的切换使得任务之间产生了一种同时进行的错觉。并发可以提高系统的吞吐量和响应性,但并不一定能够加速单个任务的执行速度。
并行是指多个任务同时进行,每个任务在不同的处理单元(如多个CPU核心)上执行。并行执行的任务之间相互独立,彼此之间不会产生竞争或依赖关系。通过并行执行,可以提高整体任务的处理能力,加速任务的执行速度。
并发控制(Concurrency Control)是指在并发执行的情况下,保证多个任务对共享资源的访问是正确和有序的。在并发环境中,多个任务可能同时访问和修改共享资源,如果不进行适当的控制,就会产生数据不一致、竞争条件和死锁等问题。并发控制的目标是通过采用合适的技术和策略,保证在多个任务同时执行时,对共享资源的访问和修改是安全和可靠的。
切片控制(Scheduling Control)是一种并发控制的策略,用于控制并发执行的任务按照一定的顺序和时间片长度进行切换。在多任务并发执行的情况下,切片控制决定了每个任务执行的时间片长度和切换的时机。通过合理的切片控制,可以平衡多个任务的执行,避免某个任务长时间占用资源而导致其他任务无法执行的情况。
切片控制通常涉及调度算法和调度器的设计和实现。调度算法决定了任务切换的策略,如先来先服务(First-Come, First-Served)、轮转(Round-Robin)、优先级调度等。调度器负责根据调度算法的策略,实现任务的切换和执行管理。
综上所述,并发和并行是不同的概念,而并发控制和切片控制则是在并发执行环境中保证任务执行的安全性和顺序性的技术手段。了解并理解这些概念对于处理多任务并发执行的场景非常重要。
引出问题
// 设计一个函数,可以限制请求的并发,同时请求结束之后,调用callback函数
// sendRequest(requestList:,limits,callback)
sendRequest(
[()=>request('1'),
()=>request('2'),
()=>request('3'),
()=>request('4')],
3, //并发数
(res)=>{
console.log(res)
})
// 其中request 可以是:
function request (url,time=1){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('请求结束:'+url);
if(Math.random() > 0.5){
resolve('成功')
}else{
reject('错误;')
}
},time*1e3)
})
}
实现思路
为了实现请求的并发数控制,我们可以利用Promise和JavaScript的异步特性。下面是实现的步骤:
- 创建一个请求队列,用于存储待发送的请求。
- 设置一个并发数限制,即同时处理的最大请求数量。
- 使用一个计数器来记录当前正在处理的请求数量,初始值为0。
- 遍历请求队列,对每个请求执行以下步骤: a. 如果计数器小于并发数限制,将计数器加1,并发送该请求。 b. 如果计数器等于并发数限制,使用Promise.race方法等待任一请求完成。 c. 当有请求完成时,将计数器减1,并从请求队列中移除该请求。
- 重复步骤4,直到所有请求完成。
实现代码( 基础版 )
async function sendRequest(requestList, limits, callback) {
const processRequest = async (request) => {
// 执行请求的逻辑,这里使用console.log语句作为示例
console.log("Sending request:", request);
// 模拟请求的异步执行
await new Promise((resolve) => setTimeout(resolve, 1000));
// 请求完成后调用回调函数
callback(request);
};
// 创建一个计数器,用于限制并发请求数量
let counter = 0;
// 并发控制函数
const controlConcurrency = async () => {
while (counter < limits && requestList.length > 0) {
const request = requestList.shift(); // 取出下一个请求
counter++;
await processRequest(request); // 执行请求
counter--;
}
// 检查是否还有未完成的请求
if (counter === 0 && requestList.length === 0) {
callback(); // 所有请求完成后调用回调函数
}
};
// 启动并发控制
for (let i = 0; i < limits; i++) {
controlConcurrency();
}
}
// 请求列表
const requestList = ["request1", "request2", "request3", "request4", "request5"];
// 并发限制数
const limits = 2;
// 回调函数
const callback = (request) => {
console.log("Request completed:", request);
};
// 发送请求
sendRequest(requestList, limits, callback);
优化方案
- 使用
Promise.race
方法:与Promise.all
相反,Promise.race
方法可以在一组请求中任何一个请求完成时就触发回调函数,这可以用于实现更实时的响应。可以在控制并发请求的循环中使用Promise.race
来等待最快完成的请求,并处理其结果。 - 采用连接池:创建一个连接池来管理请求的连接资源,而不是为每个请求都创建新的连接。这可以减少连接的创建和销毁开销,提高请求的效率。连接池可以维护一个可用连接的队列,并在需要时分配连接给请求。
- 实现请求队列:将请求添加到队列中,并按照一定的策略(如先进先出)进行处理。在控制并发请求的过程中,从队列中取出请求并执行,以保持请求的顺序性。这样可以避免同时发送大量请求,减少服务器负载。
- 使用缓存:对于重复的请求,可以使用缓存来存储已经获取的结果,以减少不必要的请求。在发起新请求之前,先检查缓存中是否存在相应的结果,如果存在,则直接使用缓存的结果,而不需要进行实际的网络请求。
- 并发级别调优:根据实际情况,调整并发请求数量的限制。如果网络环境较差或服务器端对并发请求数量有限制,可以适当降低并发级别,避免造成请求失败或被服务器拒绝的情况。
请注意,每个优化方式的适用性取决于具体的使用场景和要求。根据实际情况选择适合的优化方式,可以有效提升代码的性能和用户体验。
1.采用Promise.race方法优化
function sendRequest(requestList, limits, callback) {
const processRequest = async (request) => {
console.log("Sending request:", request);
await new Promise((resolve) => setTimeout(resolve, 1000));
return request;
};
const requests = requestList.map((request) => processRequest(request));
const sendLimitedRequests = async () => {
while (requests.length > 0) {
const limitedRequests = requests.splice(0, limits);
const fastestPromise = Promise.race(limitedRequests);
const result = await fastestPromise;
callback(result); // 请求完成后调用回调函数
}
callback(); // 所有请求完成后调用回调函数
};
sendLimitedRequests().catch((error) => {
console.error("Error occurred:", error);
});
}
代码解释:
- 定义了一个异步函数
processRequest
,用于执行单个请求的逻辑。在这个示例中,使用console.log
来模拟实际的请求,然后使用setTimeout
来模拟异步请求的延迟,并返回请求本身作为结果。 - 将所有请求通过
map
方法映射为执行请求的 Promise,并将这些 Promise 存储在requests
数组中。 - 定义了一个异步函数
sendLimitedRequests
,用于控制并发请求的数量。 - 在一个循环中,检查
requests
数组是否还有待处理的请求。 - 使用
splice
方法从requests
数组中截取limits
个请求,形成一个新的数组limitedRequests
,表示当前限制的请求数量。 - 使用
Promise.race
方法,传入limitedRequests
数组,返回最先完成的 Promise(即最快完成的请求)。 - 使用
await
等待最快完成的请求的结果,并将结果保存在result
变量中。 - 调用回调函数
callback
,将请求的结果传递给回调函数。 - 重复步骤 4 到步骤 8,直到
requests
数组中的所有请求都被处理完毕。 - 当所有请求完成后,调用回调函数
callback
,此时传递的参数为空,表示所有请求都已完成。 - 在调用
sendLimitedRequests
函数时,使用catch
方法捕获可能发生的错误,并将错误信息打印到控制台。
总结来说,这段代码通过限制并发请求数量,并利用 Promise.race
方法等待最快完成的请求,实现了对一组请求的控制和处理,并在请求完成后调用回调函数进行进一步的处理
2.采用连接池
// 假设已有一个连接池对象 connectionPool,包含了可用连接的队列
function sendRequest(requestList, limits, callback) {
const processRequest = async (request) => {
console.log("Sending request:", request);
await new Promise((resolve) => setTimeout(resolve, 1000));
return request;
};
const sendLimitedRequests = async () => {
while (requestList.length > 0) {
const limitedRequests = requestList.splice(0, limits);
const promises = limitedRequests.map((request) => processRequest(request));
const results = await Promise.all(promises);
results.forEach((result) => {
callback(result); // 请求完成后调用回调函数
});
}
callback(); // 所有请求完成后调用回调函数
};
sendLimitedRequests().catch((error) => {
console.error("Error occurred:", error);
});
}
代码解释:
- 定义了一个异步函数
processRequest
,用于执行单个请求的逻辑。在这个示例中,使用console.log
来模拟实际的请求,然后使用setTimeout
来模拟异步请求的延迟,并返回请求本身作为结果。 - 定义了一个异步函数
sendLimitedRequests
,用于控制并发请求的数量。 - 在一个循环中,检查
requestList
数组是否还有待处理的请求。 - 使用
splice
方法从requestList
数组中截取limits
个请求,形成一个新的数组limitedRequests
,表示当前限制的请求数量。 - 使用
map
方法遍历limitedRequests
数组,将每个请求传入processRequest
函数中执行,返回一个 Promise 数组promises
。 - 使用
Promise.all
方法,传入promises
数组,等待所有请求的 Promise 都完成,并将它们的结果保存在results
数组中。 - 使用
forEach
方法遍历results
数组,对每个请求的结果调用回调函数callback
。 - 重复步骤 3 到步骤 7,直到
requestList
数组中的所有请求都被处理完毕。 - 当所有请求完成后,调用回调函数
callback
,此时传递的参数为空,表示所有请求都已完成。 - 在调用
sendLimitedRequests
函数时,使用catch
方法捕获可能发生的错误,并将错误信息打印到控制台。
3.实现请求队列
function sendRequest(requestList, limits, callback) {
const processRequest = async (request) => {
console.log("Sending request:", request);
await new Promise((resolve) => setTimeout(resolve, 1000));
return request;
};
const requestQueue = [...requestList];
const sendNextRequest = async () => {
if (requestQueue.length === 0) {
callback(); // 所有请求完成后调用回调函数
return;
}
const request = requestQueue.shift();
try {
const result = await processRequest(request);
callback(result); // 请求完成后调用回调函数
} catch (error) {
console.error("Error occurred:", error);
} finally {
sendNextRequest(); // 处理下一个请求
}
};
for (let i = 0; i < limits; i++) {
sendNextRequest(); // 启动请求队列处理
}
}
代码解释:
- 定义了一个异步函数
processRequest
,用于执行单个请求的逻辑。在这个示例中,使用console.log
来模拟实际的请求,然后使用setTimeout
来模拟异步请求的延迟,并返回请求本身作为结果。 - 创建了一个请求队列
requestQueue
,使用展开运算符...
将requestList
数组复制到请求队列中。 - 定义了一个异步函数
sendNextRequest
,用于处理下一个请求。 - 检查
requestQueue
数组的长度,如果长度为 0,则表示所有请求已完成,调用回调函数callback
,并返回。 - 使用
shift
方法从requestQueue
中取出下一个请求。 - 尝试执行请求的逻辑,并使用
await
等待请求的完成。 - 如果请求成功完成,调用回调函数
callback
,将请求结果传递给回调函数。 - 如果请求过程中发生错误,使用
catch
捕获错误并打印错误信息。 - 无论请求成功还是失败,使用
finally
关键字确保sendNextRequest
函数递归调用,处理下一个请求。 - 使用一个
for
循环,根据limits
参数的值,多次调用sendNextRequest
函数,启动请求队列的处理。
总结来说,这段代码通过维护一个请求队列和递归调用的方式,实现了限制并发请求数量,并在请求完成后调用回调函数进行进一步的处理。它将请求的处理过程分散在多个异步函数中,并使用递归调用来处理下一个请求,直到所有请求都完成。
4.使用缓存
const requestCache = new Map();
function sendRequest(requestList, limits, callback) {
const processRequest = async (request) => {
console.log("Sending request:", request);
// 检查缓存中是否存在请求结果
if (requestCache.has(request)) {
console.log("Using cached result for request:", request);
return requestCache.get(request);
}
// 执行请求的逻辑
const result = await new Promise((resolve) => setTimeout(() => {
resolve(request);
}, 1000));
// 将请求结果存入缓存
requestCache.set(request, result);
return result;
};
const sendLimitedRequests = async () => {
while (requestList.length > 0) {
const limitedRequests = requestList.splice(0, limits);
const promises = limitedRequests.map(processRequest);
const results = await Promise.all(promises);
results.forEach((result) => {
callback(result); // 请求完成后调用回调函数
});
}
callback(); // 所有请求完成后调用回调函数
};
sendLimitedRequests().catch((error) => {
console.error("Error occurred:", error);
});
}
代码解释:
- 创建了一个
Map
对象requestCache
用于缓存请求的结果。 - 定义了一个异步函数
processRequest
,用于执行单个请求的逻辑。 - 在
processRequest
函数中,首先检查缓存中是否已存在请求结果。如果缓存中存在请求结果,则直接返回缓存中的结果。 - 如果缓存中不存在请求结果,则执行实际的请求逻辑。在这个示例中,使用
setTimeout
模拟异步请求,并在 1 秒后将请求本身作为结果返回。 - 将请求结果存入缓存
requestCache
中,使用请求作为键,结果作为值。 - 在主函数
sendRequest
中,继续使用sendLimitedRequests
函数来控制并发请求数量。 - 在循环中,先检查
requestList
数组是否还有待处理的请求。 - 使用
splice
方法从requestList
数组中截取limits
个请求,形成一个新的数组limitedRequests
,表示当前限制的请求数量。 - 使用
map
方法遍历limitedRequests
数组,将每个请求传入processRequest
函数中执行,返回一个 Promise 数组promises
。 - 使用
Promise.all
方法,传入promises
数组,等待所有请求的 Promise 都完成,并将它们的结果保存在results
数组中。 - 使用
forEach
方法遍历results
数组,对每个请求的结果调用回调函数callback
。 - 重复步骤 7 到步骤 11,直到
requestList
数组中的所有请求都被处理完毕。 - 当所有请求完成后,调用回调函数
callback
,此时传递的参数为空,表示所有请求都已完成。 - 在调用
sendLimitedRequests
函数时,使用catch
方法捕获可能发生的错误,并将错误信息打印到控制台。
总结来说,这段代码在前面的基础上添加了请求结果的缓存功能,以减少重复请求,并在请求开始前先检查缓存中是否存在结果。这样可以提高性能和效率,特别是在请求重复发送的场景下。
转载自:https://juejin.cn/post/7230745439125012541