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

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

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

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

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结合分块处理是一个简单有效的办法。
TURF(开源)权限定制管理系统(以下简称“TURF系统”),是蓝水工作室推出的一套基于软件边界设计理念研发的具有可定制性的权限管理系统。TURF系统充分考虑了易用性,将配置、设定等操作进行了图形化设计,完全在web界面实现,程序员只需在所要控制的程序中简单调用一个函数,即可实现严格的程序权限管控,管控力度除可达到文件级别外,还可达到代码级别,即可精确控制到
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. 任务的性质和数量
-
固定且数量可控的任务批处理: 如果你有一批固定的任务(比如从数据库导出1000条记录),并且可以接受分批执行的模式,那么
Promise.all结合分块处理可能就足够了。它实现起来最简单,代码也最直观。 -
动态、流式或数量巨大的任务: 当任务源源不断地产生,或者任务总数非常庞大时,基于计数器和队列的自定义实现或第三方库(如
p-limit)是更好的选择。它们能动态地管理并发,避免一次性加载所有任务到内存中。
2. 对资源消耗的敏感度
- API调用: 如果你的任务是调用外部API,那几乎肯定需要并发控制,而且限制通常由对方的限流策略决定。你需要根据对方的QPS(每秒查询数)或并发连接数来设定你的上限。
-
CPU密集型 vs. I/O密集型:
- I/O密集型任务(如网络请求、文件读写): 这种任务大部分时间在等待外部响应,CPU占用率不高。你可以设置相对较高的并发数,因为它们不会过度占用CPU,更多是受限于网络带宽或文件系统性能。
- CPU密集型任务(如复杂计算、图像处理): 这种任务会大量占用CPU。并发数最好不要超过CPU核心数,否则过多的上下文切换反而会降低效率。
- 内存消耗: 如果每个任务都会占用大量内存(例如处理大文件),即使是I/O密集型,也需要严格控制并发数,防止内存溢出。
3. 开发效率与维护成本
- 简单场景: 如果需求简单,自己写一个轻量级的计数器就够了,代码量不大,也方便理解。
-
复杂场景或追求健壮性: 当你需要更完善的功能,比如任务优先级、取消、超时、重试机制等,或者希望代码更健壮、少出错,那么使用像
p-limit、bottleneck这样的成熟库会大大提高开发效率,并减少潜在的bug。它们通常已经考虑到了各种边界情况。
最终,选择哪种策略,往往是在“够用就好”和“追求极致稳定和性能”之间做权衡。没有一劳永逸的方案,理解每种方式的优缺点,根据项目实际需求灵活运用,才是关键。









