并发控制在async函数中的核心目的是避免资源耗尽、接口限流或服务崩溃,通过限制同时运行的异步任务数量来维持系统稳定。1. 基于计数器和队列的自定义实现通过维护任务队列和执行计数器动态管理任务执行;2. 使用promise.all结合分块处理适用于固定任务列表的场景,将任务分为小批次串行执行;3. 借助第三方库如p-limit、bottleneck可提供更完善的功能和更高的开发效率。选择策略时需根据任务性质、资源敏感度及开发维护成本综合考量,动态任务适合自定义实现或成熟库,而简单批处理可用分块方法,资源敏感场景需严格控制并发数,复杂需求推荐使用优化过的第三方库以提升健壮性与效率。

在async函数中谈到并发执行控制,核心目的就是为了避免一次性发起过多操作,导致系统资源耗尽、接口限流,或者服务直接崩溃。简单来说,它就是给你的异步任务流加个“闸门”,控制同时运行的任务数量。

要实现async函数中的并发控制,最直接的思路就是维护一个任务队列和一个正在执行任务的计数器。当有新任务进来时,如果当前执行的任务数未达到上限,就立即执行;如果达到上限,就让新任务排队等待。等有任务完成时,再从队列中取出等待的任务执行。这有点像高速公路的收费站,或者说,更像是一个固定数量的工人同时处理订单。
说实话,这玩意儿真不是可有可无的,很多时候是救命稻草。我曾经就遇到过一个场景,需要从第三方API拉取大量数据,每一条数据都需要一个独立的请求。最初,代码写得很“奔放”,直接一个Promise.all把几千个请求一股脑儿地扔出去。结果呢?不是API直接返回429(请求过多),就是我自己的服务器内存飙升,直接OOM(内存溢出)崩溃。这不光是资源浪费,更是业务中断。

你想想看,如果你同时打开几百个文件句柄,或者同时创建几千个网络连接,操作系统能不崩溃吗?浏览器里几十个fetch请求同时跑,页面卡顿是轻的,直接内存溢出也不是没可能。所以,并发控制就是为了避免这些灾难性后果,它让你的系统在处理大量异步任务时,既能保持效率,又能维持稳定。这就像给高速公路限速,或者给水库泄洪口限流,为了整体的顺畅和安全,牺牲一点点瞬时速度是完全值得的。
实现并发控制,方法其实挺多的,各有各的适用场景。

1. 基于计数器和队列的自定义实现
这是最基础也最灵活的方式。你可以自己写一个简单的类或函数,来管理并发数。
class AsyncPool {
constructor(limit) {
this.limit = limit; // 最大并发数
this.running = 0; // 当前正在运行的任务数
this.queue = []; // 等待执行的任务队列
}
async run(taskFn) {
if (this.running < this.limit) {
// 还没达到上限,直接运行
this.running++;
try {
const result = await taskFn();
return result;
} finally {
this.running--;
this.next(); // 任务完成,尝试执行下一个
}
} else {
// 达到上限,加入队列等待
return new Promise(resolve => {
this.queue.push({ taskFn, resolve });
});
}
}
next() {
if (this.queue.length > 0 && this.running < this.limit) {
const { taskFn, resolve } = this.queue.shift();
this.running++;
taskFn().then(result => {
resolve(result);
}).finally(() => {
this.running--;
this.next();
});
}
}
}
// 示例用法
async function fetchData(id) {
console.log(`开始请求数据: ${id}`);
await new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 500)); // 模拟网络请求
console.log(`数据请求完成: ${id}`);
return `数据 ${id}`;
}
async function main() {
const pool = new AsyncPool(3); // 最大并发数设置为3
const tasks = [];
for (let i = 0; i < 10; i++) {
tasks.push(pool.run(() => fetchData(i)));
}
const results = await Promise.all(tasks);
console.log("所有任务完成:", results);
}
// main();这种方式的优点是完全可控,你可以根据业务需求调整细节,比如错误重试机制、任务优先级等。
2. 使用Promise.all结合分块处理 (Chunking)
如果你有一个固定数量的任务列表,并且这些任务可以被分成小批次执行,那么Promise.all结合分块处理是一个简单有效的办法。
async function processInBatches(tasks, batchSize) {
const results = [];
for (let i = 0; i < tasks.length; i += batchSize) {
const batch = tasks.slice(i, i + batchSize);
console.log(`处理批次 ${i/batchSize + 1}, 任务数: ${batch.length}`);
const batchResults = await Promise.all(batch.map(taskFn => taskFn()));
results.push(...batchResults);
}
return results;
}
// 示例用法
// const allTasks = Array.from({ length: 15 }, (_, i) => () => fetchData(i));
// processInBatches(allTasks, 5).then(results => {
// console.log("所有分批任务完成:", results);
// });这种方法简单粗暴,但缺点也很明显:它不是一个动态的并发控制器。它只是把大任务切成小块,每一块内部是全并发的,块与块之间是串行的。如果你的任务是流式的,或者数量不确定,这种方式就不太适用。
3. 借助成熟的第三方库
在实际项目中,我们通常不会自己从头造轮子。社区已经有很多非常优秀的库来解决这个问题,比如p-limit、bottleneck等。
以p-limit为例,它的API非常简洁直观:
// npm install p-limit
import pLimit from 'p-limit';
async function mainWithPLimit() {
const limit = pLimit(3); // 设置最大并发数为3
const input = Array.from({ length: 10 }, (_, i) => i);
const tasks = input.map(id => limit(() => fetchData(id))); // 将每个任务包装起来
const result = await Promise.all(tasks);
console.log("所有任务通过p-limit完成:", result);
}
// mainWithPLimit();使用这些库的好处是它们经过了充分的测试和优化,通常包含了更完善的错误处理、取消机制等高级功能,能大大提高开发效率和代码健壮性。
这就像选工具,没有万能的。得看你手头是什么活儿,以及你对性能、稳定性和开发效率的侧重。
1. 任务的性质和数量
Promise.all结合分块处理可能就足够了。它实现起来最简单,代码也最直观。p-limit)是更好的选择。它们能动态地管理并发,避免一次性加载所有任务到内存中。2. 对资源消耗的敏感度
3. 开发效率与维护成本
p-limit、bottleneck这样的成熟库会大大提高开发效率,并减少潜在的bug。它们通常已经考虑到了各种边界情况。最终,选择哪种策略,往往是在“够用就好”和“追求极致稳定和性能”之间做权衡。没有一劳永逸的方案,理解每种方式的优缺点,根据项目实际需求灵活运用,才是关键。
以上就是async函数中的并发执行控制的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号