0

0

async函数中的并发执行控制

畫卷琴夢

畫卷琴夢

发布时间:2025-07-13 16:30:02

|

404人浏览过

|

来源于php中文网

原创

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

async函数中的并发执行控制

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

async函数中的并发执行控制

解决方案

要实现async函数中的并发控制,最直接的思路就是维护一个任务队列和一个正在执行任务的计数器。当有新任务进来时,如果当前执行的任务数未达到上限,就立即执行;如果达到上限,就让新任务排队等待。等有任务完成时,再从队列中取出等待的任务执行。这有点像高速公路的收费站,或者说,更像是一个固定数量的工人同时处理订单。

为什么并发控制在异步操作中如此关键?

说实话,这玩意儿真不是可有可无的,很多时候是救命稻草。我曾经就遇到过一个场景,需要从第三方API拉取大量数据,每一条数据都需要一个独立的请求。最初,代码写得很“奔放”,直接一个Promise.all把几千个请求一股脑儿地扔出去。结果呢?不是API直接返回429(请求过多),就是我自己的服务器内存飙升,直接OOM(内存溢出)崩溃。这不光是资源浪费,更是业务中断。

async函数中的并发执行控制

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

几种常见的async并发控制实现方式

实现并发控制,方法其实挺多的,各有各的适用场景。

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(开源)权限定制管理系统(以下简称“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-limitbottleneck等。

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-limitbottleneck这样的成熟库会大大提高开发效率,并减少潜在的bug。它们通常已经考虑到了各种边界情况。

最终,选择哪种策略,往往是在“够用就好”和“追求极致稳定和性能”之间做权衡。没有一劳永逸的方案,理解每种方式的优缺点,根据项目实际需求灵活运用,才是关键。

相关专题

更多
硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1049

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

86

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

455

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

11

2026.01.19

promise的用法
promise的用法

“promise” 是一种用于处理异步操作的编程概念,它可以用来表示一个异步操作的最终结果。Promise 对象有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。Promise的用法主要包括构造函数、实例方法(then、catch、finally)和状态转换。

301

2023.10.12

html文本框类型介绍
html文本框类型介绍

html文本框类型有单行文本框、密码文本框、数字文本框、日期文本框、时间文本框、文件上传文本框、多行文本框等等。详细介绍:1、单行文本框是最常见的文本框类型,用于接受单行文本输入,用户可以在文本框中输入任意文本,例如用户名、密码、电子邮件地址等;2、密码文本框用于接受密码输入,用户在输入密码时,文本框中的内容会被隐藏,以保护用户的隐私;3、数字文本框等等。

401

2023.10.12

数据库三范式
数据库三范式

数据库三范式是一种设计规范,用于规范化关系型数据库中的数据结构,它通过消除冗余数据、提高数据库性能和数据一致性,提供了一种有效的数据库设计方法。本专题提供数据库三范式相关的文章、下载和课程。

352

2023.06.29

如何删除数据库
如何删除数据库

删除数据库是指在MySQL中完全移除一个数据库及其所包含的所有数据和结构,作用包括:1、释放存储空间;2、确保数据的安全性;3、提高数据库的整体性能,加速查询和操作的执行速度。尽管删除数据库具有一些好处,但在执行任何删除操作之前,务必谨慎操作,并备份重要的数据。删除数据库将永久性地删除所有相关数据和结构,无法回滚。

2075

2023.08.14

AO3中文版入口地址大全
AO3中文版入口地址大全

本专题整合了AO3中文版入口地址大全,阅读专题下面的的文章了解更多详细内容。

1

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4.7万人学习

PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.5万人学习

Git 教程
Git 教程

共21课时 | 2.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号