JS 中如何实现并发控制?

一、并发控制简介

在日常开发过程中,你可能会遇到并发控制的场景,比如控制请求并发数。那么在 JavaScript 中如何实现并发控制呢?在回答这个问题之前,我们来简单介绍一下并发控制。

假设有 6 个待办任务要执行,而我们希望限制同时执行的任务个数,即最多只有 2 个任务能同时执行。当 正在执行任务列表 中的任何 1 个任务完成后,程序会自动从 待办任务列表 中获取新的待办任务并把该任务添加到 正在执行任务列表 中。为了让大家能够更直观地理解上述的过程,小码哥特意画了以下 3 张图:

1.1 阶段一

1.2 阶段二

1.2 阶段三

好的,介绍完并发控制之后。接下来带大家剖析异步任务并发控制具体实现方法。

二、并发控制的实现

async-pool 这个库提供了 ES7 和 ES6 两种不同版本的实现,在分析其具体实现之前,我们来看一下它如何使用

2.1 asyncPool 的使用

const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
await asyncPool(2, [1000, 5000, 3000, 2000], timeout);

在以上代码中,我们使用 async-pool 这个库提供的 asyncPool 函数来实现异步任务的并发控制。asyncPool 函数的签名如下所示:

function asyncPool(poolLimit, array, iteratorFn){ ... }

该函数接收 3 个参数:

  • poolLimit(数字类型):表示限制的并发数;

  • array(数组类型):表示任务数组;

  • iteratorFn(函数类型):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。

对于以上示例来说,在使用了 asyncPool 函数之后,对应的执行过程如下所示:

const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
// Call iterator (i = 1000)
// Call iterator (i = 5000)
// Pool limit of 2 reached, wait for the quicker one to complete...
// 1000 finishes
// Call iterator (i = 3000)
// Pool limit of 2 reached, wait for the quicker one to complete...
// 3000 finishes
// Call iterator (i = 2000)
// Itaration is complete, wait until running ones complete...
// 5000 finishes
// 2000 finishes
// Resolves, results are passed in given array order `[1000, 5000, 3000, 2000]`.

通过观察以上的注释信息,我们可以大致地了解 asyncPool 函数内部的控制流程。下面我们先来分析 asyncPool 函数的 ES7 实现。

2.2 asyncPool ES7 实现

async function asyncPool(poolLimit, array, iteratorFn) {
  const ret = []; // 存储所有的异步任务
  const executing = []; // 存储正在执行的异步任务
  for (const item of array) {
    // 调用iteratorFn函数创建异步任务
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    ret.push(p); // 保存新的异步任务

    // 当poolLimit值小于或等于总任务个数时,进行并发控制
    if (poolLimit <= array.length) {
      // 当任务完成后,从正在执行的任务数组中移除已完成的任务
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e); // 保存正在执行的异步任务
      if (executing.length >= poolLimit) {
        await Promise.race(executing); // 等待较快的任务执行完成
      }
    }
  }
  return Promise.all(ret);
}

在以上代码中,充分利用了 Promise.all 和 Promise.race 函数特点,再结合 ES7 中提供的 async await 特性,最终实现了并发控制的功能。利用 await Promise.race(executing); 这行语句,我们会等待 正在执行任务列表 中较快的任务执行完成之后,才会继续执行下一次循环。

asyncPool ES7 实现相对比较简单,接下来我们来看一下不使用 async await 特性要如何实现同样的功能。

2.3 asyncPool ES6 实现

function asyncPool(poolLimit, array, iteratorFn) {
  let i = 0;
  const ret = []; // 存储所有的异步任务
  const executing = []; // 存储正在执行的异步任务
  const enqueue = function () {
    if (i === array.length) {
      return Promise.resolve();
    }
    const item = array[i++]; // 获取新的任务项
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    ret.push(p);

    let r = Promise.resolve();

    // 当poolLimit值小于或等于总任务个数时,进行并发控制
    if (poolLimit <= array.length) {
      // 当任务完成后,从正在执行的任务数组中移除已完成的任务
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= poolLimit) {
        r = Promise.race(executing); 
      }
    }
 
    // 正在执行任务列表 中较快的任务执行完成之后,才会从array数组中获取新的待办任务
    return r.then(() => enqueue());
  };
  return enqueue().then(() => Promise.all(ret));
}

在 ES6 的实现版本中,通过内部封装的 enqueue 函数来实现核心的控制逻辑。当 Promise.race(executing) 返回的 Promise 对象变成已完成状态时,才会调用 enqueue 函数,从 array 数组中获取新的待办任务。

总结:对于异步任务并发控制的具体实现核心方法es7中 async await 还是es6中的 enqueue 方法最核心用的方法都是是 Promise 中的Promise.all和Promise.race两个方法。

对于这个两个方法也是经常出现在面试题上的。对于如何手写Promise.all和Promise.race这来个方法的具体实现过程可点击关注收藏作者,在后面会更新对于这两个方法的实现原理的相关技术文档。

链接:https://pan.baidu.com/s/1_4PIUb-Yl68aTW9Bw95iJA 
提取码:tnav 

<think>好的,用户的问题是关于在多个浏览器环境中实现异步操作的并发控制的最佳实践。我需要结合提供的引用内容,尤其是引用3、4、5,因为它们涉及异步处理、并发控制和消息队列的相关知识。首先,我需要理解用户的具体需求:他们可能在进行跨浏览器的测试或者任务,需要同时管理多个浏览器的异步操作,比如爬虫、自动化测试等。这时候并发控制是关键,避免资源竞争和保证数据一致性。 首先,根据引用3,提到了使用Promise.allSettled和async/await来处理并发,这可能适用于单个浏览器标签或页面内的异步操作,但在多浏览器实例的情况下,可能需要更复杂的机制。引用4讨论了锁机制,比如lock、Mutex和Semaphore,这些在C#中用于线程安全,但用户可能是在JavaScript环境下,所以需要考虑Node.js或浏览器环境中的类似机制。引用5提到了MQ(消息队列)用于异步通信和解耦,这可能适用于分布式系统,但如果是同一台机器上的多个浏览器实例,可能需要更轻量级的解决方案。 接下来,我需要整理出几个最佳实践。首先是任务队列与并发池,限制同时运行的浏览器实例数量,防止资源耗尽。可以使用Promise和async/await结合,比如设置一个最大并发数,然后通过循环控制任务的执行。其次是使用锁机制,比如在JavaScript中可以用Mutex或Semaphore库来管理共享资源。第三个是异步状态管理,确保每个浏览器的操作结果被正确处理,避免竞争条件。然后是错误处理与重试机制,保证某个实例失败不会影响整体。分布式任务调度如果涉及多机器,可能需要用到消息队列,如RabbitMQ或Redis。最后是监控和日志,帮助调试和优化。 需要检查是否有遗漏点,比如是否考虑不同浏览器的特性,或者是否需要使用Headless模式节省资源。另外,引用3提到的Promise.allSettled可以在处理多个异步操作时收集结果,无论成功与否,这在并发控制中很有用。可能还需要提到浏览器实例的生命周期管理,及时关闭释放资源。 总结步骤应该是分点详细说明每个实践,结合代码示例,比如使用async库的queue,或者自定义的并发控制函数。同时,引用相关的引用内容作为支持,比如引用3和4的方法论。确保回答结构清晰,用户能逐步理解和实施这些实践。</think>在多浏览器环境下实现异步并发控制时,需结合任务调度、资源隔离和状态同步三个核心方向。以下是分步骤的最佳实践: --- ### 1. **任务队列与并发池** 使用任务队列限制同时运行的浏览器实例数量,防止资源耗尽。通过`Promise`和`async/await`实现并发池: ```javascript async function runConcurrentBrowsers(tasks, maxConcurrency) { const pool = []; for (const task of tasks) { const browserTask = launchBrowser().then(task); // 启动浏览器实例并执行任务 pool.push(browserTask); if (pool.length >= maxConcurrency) { await Promise.race(pool); // 等待任意任务完成 } } return Promise.all(pool); // 等待剩余任务完成 } ``` 此方法通过`Promise.race`动态控制并发数[^3]。 --- ### 2. **浏览器实例锁机制** 通过信号量(Semaphore)或互斥锁(Mutex)实现资源隔离: ```javascript class BrowserLock { constructor(max) { this.semaphore = max; this.queue = []; } async acquire() { if (this.semaphore > 0) { this.semaphore--; return Promise.resolve(); } return new Promise(resolve => this.queue.push(resolve)); } release() { this.semaphore++; if (this.queue.length > 0) { const next = this.queue.shift(); next(); } } } // 使用示例 const lock = new BrowserLock(5); // 最多5个浏览器并行 async function safeOperation() { await lock.acquire(); try { await performBrowserTask(); // 执行需要隔离的操作 } finally { lock.release(); } } ``` 该模式可防止多个异步操作同时修改共享资源(如配置文件)[^4]。 --- ### 3. **异步状态共享与协调** 通过消息队列(如RabbitMQ或Redis)解耦浏览器任务: - **生产者**:生成需要跨浏览器处理的任务(如登录、数据采集) - **消费者**:监听队列并分配任务到不同浏览器实例 ```javascript // 使用Redis实现简单任务队列 const redis = require("redis"); const client = redis.createClient(); async function dispatchTask(task) { await client.lPush("browser_tasks", JSON.stringify(task)); // 生产者推送任务 } async function consumeTasks() { while (true) { const task = await client.rPop("browser_tasks"); // 消费者获取任务 if (task) { const browser = await puppeteer.launch(); await processTask(browser, JSON.parse(task)); // 在独立浏览器实例中处理 } } } ``` 消息队列可实现流量削峰和系统解耦[^5]。 --- ### 4. **错误恢复与监控** - **自动重试**:为异步操作添加指数退避重试逻辑 - **心跳检测**:定期检查浏览器实例健康状态 ```javascript async function resilientOperation(task, retries = 3) { try { return await task(); } catch (error) { if (retries <= 0) throw error; await new Promise(r => setTimeout(r, 2 ** (4 - retries) * 1000)); // 退避等待 return resilientOperation(task, retries - 1); } } ``` --- ### 5. **性能优化实践** - **Headless模式**:使用无头浏览器减少资源消耗 - **复用实例**:通过`browser.connect()`复用已有实例而非频繁启动 - **内存隔离**:为每个任务创建独立上下文(Context) ```javascript const browser = await puppeteer.launch({ headless: "new" }); const context = await browser.createIncognitoBrowserContext(); // 隔离环境 const page = await context.newPage(); ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值