并发请求怎么做?且能暂停?又能超时处理?还能监听过程?

479 阅读9分钟

宝子们👋,今天来给大家分享任务队列执行的那些事儿🧐。

任务队列在异步任务处理中就像一个贴心的小管家,能让我们的程序运行得更有序、高效💪

下面就跟着我一起来看看吧😎!

一、什么是任务队列✨

简单来说,任务队列本质上是一种数据结构。

队列用于存储待执行的任务(通常是函数),并按照特定的规则依次执行它们。

这些任务可以是各种异步操作,如网络请求、文件读取、定时器回调等,甚至也可以是同步函数,但在任务队列的框架下,它们都被视为需要排队处理的任务单元。

简单任务队列的思维导图: export.jpeg

二、任务队列执行的好处✨

1. 资源利用大优化🎯

任务队列就像是交通指挥员🚦,能控制同时 “行驶” 的任务数量,避免资源 “交通堵塞”。

比如,当我们有一堆网络请求任务时,如果同时发起太多,可能会把网络带宽占满,导致其他任务 “饿死”。但有了任务队列,我们可以合理设置并发数,让资源分配更均衡🤗。

2. 任务顺序随心控🌈

有些任务就像多米诺骨牌🧩,必须按顺序执行才能保证结果正确。

任务队列就能确保这些任务依次倒下,不会乱套。比如说数据处理流程中,前一个任务的数据处理结果是后一个任务的输入,任务队列可以保证这种依赖关系稳稳地被处理好👍。

3. 系统稳定有保障🛡️

想象一下,如果没有任务队列,大量异步任务像一群无头苍蝇乱撞😵。

而任务队列通过限制并发、处理超时和错误等操作,就像给系统穿上了一层坚固的铠甲,让它能从容应对各种复杂情况,稳定性和可靠性大幅提升👏。

4. 任务管理超轻松🤝

接下来会介绍任务管理的解析🛠️,搭配成熟队列执行库操作更丝滑。

我们可以轻松地添加、暂停、继续、掌控任务,而且还能实时监控任务状态,比如任务开始了、完成了还是出错了,就像给每个任务都装上了小监控摄像头📹。

三、手写 SimpleQueue 初体验✨

我们先上手简单手写一个队列 “小助手” SimpleQueue🧑‍✈️。

它是一个简单的任务队列实现,同样能提供一系列实用的方法来管理任务队列的执行。

image.png

1. 朴实无华的代码实现🚶‍♂️🚶‍♀

class SimpleQueue {
  constructor() {
    this.list = [];        // 存储任务的数组
    this.index = 0;        // 当前任务执行下标
    this.isStop = false;   // 暂停标识
  }

  next(arg) { 
    // 若已经执行完最后一个或当前是暂停状态则不执行
    if (this.index >= this.list.length - 1 || this.isStop) return;
    const cur = this.list[++this.index];
    cur(this.next, arg);
  }

  add(fn) {  
    // 支持添加一个或多个任务
    if (Array.isArray(fn)) {
      this.list.push(...fn);
    } else {
      this.list.push(fn);
    }
  }

  run() {  // 按序执行队列里的任务
    const cur = this.list[this.index];
    if (typeof cur === 'function') {
      cur(this.next);
    }
  }

  stop() {   // 暂停
    this.isStop = true;
  }
  
  retry() {  // 继续
    this.isStop = false;
    this.run();
  }
  
  goOn() {  // 跳过,执行下一个
    this.isStop = false;
    this.next();
  }

  repeat() {  // 重复
    this.index = -1;
    this.next();
  }
  
  removeAll() {  // 清空队列
    this.list.length = 0;
    this.index = 0;
  }
}

2. 让我们添加任务运行起来🎯

通过 const queue = new SimpleQueue();,我们轻松搭建起任务队列的框架🚪。

const queue = new SimpleQueue();
const task1 = (next) => {
  console.log('任务 1 同步执行');
  setTimeout(() => {
    console.log('任务 2 异步完成');
    next();
  }, 1000);
};
const task2 = (next) => {
  console.log('任务 3 完成');
  setTimeout(() => {
    console.log('任务 4 异步完成');
    next();
  }, 1500);
};

queue.add(task1);
queue.add(task2);
queue.run();  
// 任务将会按任务1-4依次完成

队列控制:灵活掌控任务流程

  • 暂停与继续queue.stop() 和 queue.retry() 为我们提供了对队列执行节奏的精准控制。当需要暂时中断任务执行时,queue.stop() 就像按下了暂停键⏸️,让队列静止在当前状态;而 queue.retry() 则如同再次按下播放键▶️,使队列从暂停的地方恢复执行,确保任务流程的连贯性。
  • 跳过任务queue.goOn() 赋予我们快速跳过当前任务的能力,如同在播放列表中直接跳到下一首歌曲⏩,让队列直接进入下一个任务的执行阶段,适用于某些情况下需要紧急处理后续任务的场景。
  • 重复队列queue.repeat() 则像是一个神奇的重置按钮,它将队列的任务索引归零,让整个队列重新开始执行,就像重新播放一段精彩的视频🎬,提供多次执行相同任务序列的便利。

image.png

三、进阶之选: p-queue的奇妙世界🌟

1. p-queue 初印象🌈

当我们的任务需求变得更复杂时,在github上总能找到属于你的曙光🦸‍♂️ !

拥有更强大的功能,能更好地应对各种复杂的异步任务场景的 p-queue 可能帮到你。

github地址可以点击链接自行获取哦💪

p-queue 是强大的任务队列库:

有并发控制,合理分配资源;支持任务优先级,精确调度;

有超时处理,防任务阻塞。还有丰富事件通知,可实时监控。

能灵活添加管理任务、支持队列操作,且扩展性和兼容性强,助力异步任务处理。

image.png

2. 话不多说,使用示例请看VCR🎬

import PQueue from 'p-queue';

// 创建一个p-queue实例,设置并发数为2,每个任务超时时间为2秒
const queue = new PQueue({concurrency: 2, timeout: 2000});

// 定义默认优先级任务
const mediumPriorityTask = async (taskId) => {
  console.log(`普通任务 ${taskId} 执行开始`);
  
  // 该异步执行可能会出现超过2秒的情况
  const randomTime = Math.floor(Math.random() * 1000) + 1500
  await new Promise((resolve) => setTimeout(resolve, randomTime)
  
  console.log(`普通任务 ${taskId} 执行完毕`);
  
  return `普通任务 ${taskId} 结果`;
};


// 🎯添加单个默认优先级任务1,并处理其结果或错误
queue.add(() => mediumPriorityTask(1))
   .then((result) => console.log('当前任务返回结果', result))
   .catch((error) => console.error('当前任务的报错', error));

// 📦添加任务2和3,并处理整体结果或错误,这是一组小包裹,一起处理它们的情况😏
queue.addAll([
    () => mediumPriorityTask(2),
    () => mediumPriorityTask(3)
]).then((results) => console.log('两个任务完成后返回结果:', results))
   .catch((error) => console.error('两个任务中里有执行错误:', error));

// 🚀添加高优先级任务4,设置优先级为99,这可是超级VIP任务。
queue.add({
  task: () => mediumPriorityTask(4),
  priority: 99    
});

// 🚦手动启动队列
queue.start();

// 暂停队列
setTimeout(() => {
  queue.pause();
  console.log('⏸️队列刹车了');
}, 1200);

// 一段时间后继续队列
setTimeout(() => {
  queue.start();
  console.log('🚗队列继续发车');
}, 2500);

// 监听队列空事件
queue.onEmpty().then(() => console.log('队列执行完啦'));

// 监听队列空闲事件
queue.onIdle().then(() => console.log('队列目前在摸鱼'));

// 🗑️清空队列
setTimeout(() => {
  queue.clear();
  console.log('队列任务被哔掉了');
}, 5000);

// 📊获取队列大小相关信息
console.log('队列中等待执行的任务数量:', queue.size);
console.log('正在执行的任务数量:', queue.pending);
console.log('队列是否暂停:', queue.isPaused);

在这个示例中:

  1. 首先创建了一个p-queue实例,设置了并发数和超时时间。
  2. 定义需要执行的任务函数。
  3. 使用add方法添加单个任务,并通过thencatch处理当前添加的任务回调处理。
  4. 使用addAll方法添加多个任务,并处理它们的整体结果和错误。
  5. 通过setTimeout在特定时间暂停和继续队列。
  6. 监听onEmptyonIdle事件。
  7. 在一定时间后清空队列。
  8. 最后获取并打印队列数量和队列暂停状态等信息。

四、 爱之再判断

虽说 SimpleQueue 能实现基本的任务排队,但跟人家 p-queue 相对比:

屁股上挂暖壶——有一腚的水瓶(平)😅!

(1)并发控制
  • p-queue:能通过 concurrency 精准设置同时执行的任务数量,合理控制任务流。
  • SimpleQueue:一旦遇到耗时任务,就像堵住的水管🚿,响应超慢。
(2)任务优先级
  • p-queue:每个任务都能被赋予优先级数值,数字越大越优先执行。
  • SimpleQueue:所有任务平等,按照先来后到的顺序排队,一视同仁没有特殊待遇。
(3)超时处理
  • p - queue:通过 timeout 为任务设定最长执行时间,考试时间到了🕙,没做完也得停笔,这样能防止任务一直霸占资源,保证队列高效运行。
  • SimpleQueue:任务执行起来就像脱缰的野马🐎,一旦卡住或耗时过长,整个队列都得跟着遭殃。
(4)事件通知
  • p-queue:提供各种任务执行状态的事件通知,如任务开始、完成、出错、队列空和空闲等。
  • SimpleQueue:相对 “安静” 许多,仅靠任务函数内部手动调用 next 来推进任务。
(5)代码维护与扩展
  • p-queue:模块化设计,丰富的配置选项和灵活接口,让开发者能轻松扩展和定制,比如定制特殊队列存储或调度策略,适应各种场景,维护起来也得心应手😎。
  • SimpleQueue:实现简单直接,代码逻辑单一,遇到复杂需求,可能会出现小马拉大车。

(6)错误处理

  • p-queue:能捕获任务执行中的错误,还能根据配置处理超时错误等。通过 add 和 addAll 返回的 Promise统一处理任务错误。
  • SimpleQueue:没有统一的错误处理机制,错误处理分散在任务函数内部。
image.png

对比总结:爱之终抉择

手写的SimpleQueue

  • 优点:代码简单易懂,适合新手,基本功能齐全,能满足简单场景下的任务排队、暂停、继续等操作,轻量级,占用资源少。
  • 缺点:缺乏并发控制、任务优先级、超时处理,事件通知不完善,可维护和扩展性差,错误处理分散。

p-queue

  • 优点:有并发控制、任务优先级、超时处理,事件通知丰富,可灵活添加和管理任务,扩展性和兼容性强,能统一处理错误。
  • 缺点:对于超简单场景,使用它可能有些 “大材小用”,学习成本也还好。

🎉探索任务队列的奇妙之旅就到这里啦!

宝子们,今天我们一起体验了 p-queue 的各种强大功能,是不是感觉我上我也能实现 😎?

下一篇笔记我会带着大家深入剖析 p-queue 的源码,看看它内部到底是怎么运作的,让我们可以更好地掌控这个强大的工具。

image.png

感谢大家的阅读,我们下一篇笔记见啦!💕