本文首发同名微信公众号:前端徐徐
大家好,我是徐徐。今天我们讲讲如何在 Electron 中实现任务队列管理。
前言
我相信很多小伙伴都会在 Electron 中 或者 Node 应用中遇到定时任务之类的功能,如果定时任务比较简单,数量比较少的话,随处定义 setInterval
就能解决了,但是如果遇到定时任务非常多,任务中执行的方法并不简单的话,简单的随处定义 setInterval
已经不能满足复杂的需求了,这个时候我们可能就需要考虑使用任务队列来管理整个应用的定时任务了。
任务队列的好处和缺点
在做任务队列之前,我们需要探讨一下任务队列的好处和缺点,因为这样的话在设计的时候才能发挥它的优势,然后尽量处理一些他的弊端,这样设计出来的程序才会更加好用。
好处
- 集中管理: 使用任务队列可以将所有的定时任务集中管理,避免了在不同地方随意定义定时器或调用
setInterval
,导致一些任务的同时并发,这样能更清晰地掌控任务的执行顺序、任务的数量及其执行时机。 - 控制任务执行顺序: 任务队列可以按照一定的优先级或顺序执行任务,这对于复杂的业务逻辑非常重要。
- 便于任务调度与取消: 使用队列后,可以方便地对任务进行暂停、恢复、取消等操作。如果任务数量非常多,或者存在临时需要暂停的任务,直接操作队列比操作多个
setInterval
更加方便。 - 减少资源浪费:
setInterval
可能会造成不必要的 CPU 占用,尤其是任务间隔过短时。通过任务队列,可以在需要时按需触发任务,减少不必要的资源浪费。 - 任务重试与失败处理: 任务队列可以添加任务重试机制,并且在任务执行失败时,能够自动处理异常,避免任务因单次失败而影响后续任务。
缺点
- 复杂度增加: 使用任务队列管理任务,代码结构会相应复杂。特别是当队列中任务很多,且有不同优先级、重复执行的需求时,队列的管理就需要额外的精力和设计。
- 性能瓶颈: 虽然任务队列可以减少不必要的
setInterval
使用,但如果任务队列的处理逻辑不高效,也可能导致性能瓶颈,影响应用响应。 - 延迟问题: 如果任务队列中的任务排队过多,可能会出现任务执行的延迟问题,尤其是在任务本身耗时较长的情况下,可能导致前一个任务未执行完,后续任务就被阻塞,造成整体任务的积压。
知名的任务队列库
- node-schedule
https://www.npmjs.com/package/node-schedule
- node-cron
GitHub - node-cron/node-cron: A simple cron-like job scheduler for Node.js
这两个库都是非常优秀的库,他们的任务都是基于 cron 表达式来定义的任务的时间,但是都仅仅支持到秒的级别,如果对时间精度要求不高,没有特别的定制化功能可以无脑选择他们,但是如果要自己实现一个快捷方便且轻量的任务队列该如何实现呢,毕竟自己实现出来的扩展性更强,更加适合特定的业务场景,下面我们就来看看如何实现一个任务队列吧。
任务队列的核心功能点
根据上面的一些分析,我们现在来看看需要实现那些核心功能,以便更好的发挥任务队列的优势,避免其劣势
- 任务注册
注册一个定时任务,指定任务名称、任务函数和执行间隔时间。
- 任务执行
启动一个已注册的定时任务,并将其加入任务队列。
- 取消任务
取消一个已注册的定时任务,并将其从任务队列中移除。
- 任务状态获取
检查指定任务是否正在运行。
- 获取任务实例
获取指定任务的详细配置和实例信息。
- 任务最大并发数
管理任务队列的最大任务数量和超出最大队列大小的任务数量。
- 任务执行白名单
定义一个任务白名单,确保白名单中的任务可以优先执行,即使队列已满。
- 队列核心
核心队列处理逻辑,依次从任务队列中取出任务并执行,同时处理任务超时和异常情况。
- 超过最大队列任务上传告警
当任务队列或超出最大队列的任务数量超过限制时,记录日志并上传告警信息。
- 添加队列前置判断
在添加任务到任务队列之前进行检查,确保任务未在队列中且未超过最大任务数量。
- 添加超过最大任务队列前置判断
在添加任务到超出最大队列的任务队列之前进行检查,确保任务未在队列中且未超过超出最大队列的任务数量。
核心代码如下所示:
- src/common/schedule/index.ts
import { Log4 as log } from "@/common/log";
interface ScheduleOption {
taskName: string;
taskFunction: any;
interval: number;
}
interface TaskOption {
taskFunction: any;
interval: number;
taskInstance: any;
isRun: boolean;
}
interface TaskList {
[taskName: string]: TaskOption | null;
}
class TaskQueueManager {
private static instance: TaskQueueManager;
private taskList: TaskList = {};
private taskQueue: any[] = [];
private overMaxQueueSizeTaskQueue: any[] = [];
private readonly MAX_QUEUE_SIZE = 10;
private readonly OVER_MAX_QUEUE_SIZE = 10;
private readonly WHITE_LIST: string[] = ["initTask"];
// 私有构造函数,确保外部不能直接实例化
private constructor() {
this.startQueueProcessing();
}
// 获取唯一的实例
public static getInstance(): TaskQueueManager {
if (!TaskQueueManager.instance) {
TaskQueueManager.instance = new TaskQueueManager();
}
return TaskQueueManager.instance;
}
// 任务注册
registerSchedule(scheduleOption: ScheduleOption): void {
const { taskName, taskFunction, interval } = scheduleOption;
if (this.taskList[taskName]) {
this.taskList[taskName] = null;
}
this.taskList[taskName] = {
taskFunction,
interval,
isRun: false,
taskInstance: null,
};
}
// 告警上传
private logAndUploadTaskSizeTooLarge(
type: "task_queue_size" | "over_max_queue_size",
): void {
const data: any = {};
let taskQueue: any[] = [];
if (type === "task_queue_size") {
data.taskQueue = taskQueue.map((item) => item.name);
taskQueue = taskQueue.map((item) => item.name);
} else {
data.overMaxQueue = this.overMaxQueueSizeTaskQueue.map(
(item) => item.name,
);
taskQueue = this.overMaxQueueSizeTaskQueue.map((item) => item.name);
}
try {
// todo 上传任务队日志
log.info(type, JSON.stringify(taskQueue));
} catch (err) {
log.info("task_queue_size_too_large err", err);
}
}
// 添加队列前置判断
private shouldAddTaskToQueue(taskName: string): boolean {
if (this.taskQueue.find((task) => task.name === taskName)) {
return false;
}
if (this.WHITE_LIST.includes(taskName)) {
return true;
}
if (this.taskQueue.length >= this.MAX_QUEUE_SIZE) {
this.logAndUploadTaskSizeTooLarge("task_queue_size");
return false;
}
return true;
}
// 添加超过最大任务队列前置判断
private shouldAddOverMaxQueueSizeTaskQueue(taskName: string): boolean {
if (this.overMaxQueueSizeTaskQueue.find((task) => task.name === taskName)) {
return false;
} else {
if (this.overMaxQueueSizeTaskQueue.length >= this.OVER_MAX_QUEUE_SIZE) {
this.logAndUploadTaskSizeTooLarge("over_max_queue_size");
return false;
} else {
return true;
}
}
}
// 任务执行
runTask(taskName: string, initRun = true): void {
const options: TaskOption | null = this.taskList[taskName];
if (!options) {
return;
}
const { taskFunction, interval, taskInstance, isRun } = options;
if (isRun && taskInstance && taskInstance.clear) {
taskInstance && taskInstance.clear();
} else {
if (!this.taskList[taskName]) {
return;
}
this.taskList[taskName].isRun = true;
if (initRun) {
if (this.shouldAddTaskToQueue(taskName)) {
this.taskQueue.push({
name: taskName,
callback: taskFunction,
});
} else {
if (this.shouldAddOverMaxQueueSizeTaskQueue(taskName)) {
this.overMaxQueueSizeTaskQueue.push({
name: taskName,
callback: taskFunction,
});
}
}
}
}
if (!this.taskList[taskName]) {
return;
}
this.taskList[taskName].taskInstance = setInterval(() => {
if (this.shouldAddTaskToQueue(taskName)) {
this.taskQueue.push({
name: taskName,
callback: taskFunction,
});
} else {
if (this.shouldAddOverMaxQueueSizeTaskQueue(taskName)) {
this.overMaxQueueSizeTaskQueue.push({
name: taskName,
callback: taskFunction,
});
}
}
}, interval);
}
// 取消任务
cancelTask(taskName: string): void {
// 判断任务实例是否存在
if (!this.taskList[taskName]) {
return;
}
const { taskInstance } = this.taskList[taskName];
// 取消任务实例
if (taskInstance) {
if (taskInstance.clear) {
taskInstance.clear();
} else {
clearInterval(taskInstance);
}
}
// 重置任务状态
this.taskList[taskName].isRun = false;
this.taskList[taskName].taskInstance = null;
// 从任务队列中移除该任务
this.taskQueue = this.taskQueue.filter((item) => item.name !== taskName);
delete this.taskList[taskName];
}
// 根据关键词批量取消任务
cancelTaskByKeyWords(taskKeyWords: string): void {
Object.keys(this.taskList).forEach((o: string) => {
if (o.toLowerCase().indexOf(taskKeyWords.toLowerCase()) !== -1) {
this.cancelTask(o);
}
});
}
// 查找任务是否运行
isTaskRun(taskName: string): boolean {
const options: TaskOption | null = this.taskList[taskName];
if (!options) {
return false;
}
return options.isRun;
}
// 任务是否注册
isRegisterTask(taskName: string): boolean {
if (this.taskList[taskName]) {
return true;
}
return false;
}
// 获取任务实例
getTaskOption(taskName: string): TaskOption | false {
if (this.taskList[taskName]) {
return this.taskList[taskName];
}
return false;
}
// 任务时间是否变化
taskTimeHasChange(taskName: string, time: number): boolean {
const taskOption = this.getTaskOption(taskName);
if (
taskOption &&
taskOption.interval &&
taskOption.interval === time * 1000
) {
return true;
} else {
return false;
}
}
// 判断是否可以执行任务
private canDoTask(): boolean {
if (import.meta.env.VITE_CURRENT_RUN_MODE === "main") {
const { net } = require("electron");
if (net.isOnline()) {
return true;
}
return false;
} else {
if (!navigator.onLine) {
return false;
}
return true;
}
}
// 任务队列核心
private async processQueue(): Promise<void> {
if (this.taskQueue.length || this.overMaxQueueSizeTaskQueue.length) {
let taskItem = null;
if (this.taskQueue.length) {
taskItem = this.taskQueue.shift();
}
if (this.overMaxQueueSizeTaskQueue.length) {
taskItem = this.overMaxQueueSizeTaskQueue.shift();
}
if (!taskItem) {
return;
}
if (this.isTaskRun(taskItem.name) && this.canDoTask()) {
const timeout = 5000; // 超时时间,单位毫秒
try {
const taskPromise = new Promise((resolve, reject) => {
const result = taskItem.callback();
if (result && typeof result.then === "function") {
result
.then((res: any) => {
resolve(res);
})
.catch((err: Error) => {
reject(err);
});
} else {
resolve("sync function");
}
});
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Task timed out")), timeout),
);
Promise.race([taskPromise, timeoutPromise])
.then(() => {})
.catch((err) => {
log.info(
`${taskItem.name} failed with error or timeout`,
import.meta.env.VITE_CURRENT_RUN_MODE,
err,
);
});
} catch (err) {
log.info(`${taskItem.name} failed with error`, err);
}
} else {
return;
}
} else {
return;
}
}
// 启动任务队列处理
startQueueProcessing(): void {
let intervalId: any = setInterval(() => {
this.processQueue();
if (
this.taskQueue.length === 0 ||
this.overMaxQueueSizeTaskQueue.length === 0
) {
clearInterval(intervalId);
intervalId = null;
setTimeout(() => this.startQueueProcessing(), 100);
}
}, 2024);
}
}
export default TaskQueueManager;
整个 TaskQueueManager
类是一个单例模式的任务队列管理器,用于注册、运行和取消定时任务。它管理任务队列和超出最大队列大小的任务队列,并提供任务执行、取消、查找、注册和队列处理等功能,同时实现了任务队列的动态处理和日志记录。
外部使用如下,非常简单。
- src/common/schedule/testTask.ts
import TaskQueueManager from "./index";
export const initTestTask = () => {
const testTask = () => {
console.log("testTask");
}
const taskQueueManager = TaskQueueManager.getInstance();
taskQueueManager.registerSchedule({
taskName: "testTask",
taskFunction: testTask,
interval: 3000,
});
taskQueueManager.runTask("testTask");
}
结语
在日常的开发中,有很多场景可能都需要用到任务队列,上面任务队列的实现思路也不一定只适用于客户端,服务端设计大致的思路也差不多。
通过实现一个功能完善的任务队列管理系统,我们能够更加高效地管理定时任务,确保任务的顺序执行、资源的合理利用以及任务的灵活调度。尽管任务队列管理增加了一定的代码复杂度,但其带来的集中管理、任务调度和性能优化的好处是显而易见的。
当然,整体来说这个任务队列的管理实现其实是一个比较基础的任务队列,在此基础之上我们还可以探索更多的功能,比如任务依赖管理,任务优先级等,然后进一步优化它。