Node.js 实现异步任务的取消与超时控制
Node.js 异步任务概述
在深入探讨 Node.js 中异步任务的取消与超时控制之前,我们先来回顾一下 Node.js 的异步编程模型。Node.js 基于事件驱动和非阻塞 I/O 模型构建,这使得它在处理大量并发 I/O 操作时表现出色。在 Node.js 中,许多操作,如文件系统 I/O、网络请求等,都是异步执行的。
异步任务的实现方式
- 回调函数:这是 Node.js 早期处理异步操作最常用的方式。例如,读取文件的操作:
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
在这个例子中,readFile
是一个异步函数,它接受文件名、编码格式以及一个回调函数作为参数。当文件读取操作完成后,会调用这个回调函数,并将可能出现的错误 err
和读取到的数据 data
作为参数传递进去。
- Promise:ES6 引入的 Promise 为异步操作提供了一种更优雅的处理方式。通过将异步操作封装成 Promise 对象,可以使用
.then()
方法来处理成功的结果,使用.catch()
方法来处理错误。以下是用 Promise 重写上述文件读取操作:
const fs = require('fs').promises;
fs.readFile('example.txt', 'utf8')
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
});
Promise 对象有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。一旦状态改变,就不会再变,这使得异步操作的状态管理更加清晰。
- Async/await:这是基于 Promise 的一种语法糖,它让异步代码看起来更像同步代码,大大提高了代码的可读性。同样是文件读取操作:
const fs = require('fs').promises;
async function readFileAsync() {
try {
const data = await fs.readFile('example.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
}
readFileAsync();
async
关键字用于定义一个异步函数,该函数总是返回一个 Promise。await
关键字只能在 async
函数内部使用,它会暂停函数的执行,直到 Promise 被解决(resolved)或被拒绝(rejected),然后返回解决的值或抛出拒绝的原因。
异步任务的取消
在某些情况下,我们可能需要取消正在进行的异步任务。例如,用户在网页上发起了一个长时间运行的网络请求,但在请求完成之前,用户改变了主意并取消了操作。在 Node.js 中实现异步任务的取消并非易事,因为 JavaScript 本身并没有原生支持取消异步操作的机制。然而,我们可以通过一些技巧来模拟取消功能。
使用 AbortController
从 Node.js v15.0.0 开始,引入了对 AbortController
的支持,这使得取消异步任务变得更加容易。AbortController
是 Web API 的一部分,它允许你在需要时中止一个或多个 DOM 操作、fetch 请求等。在 Node.js 中,我们可以利用它来取消异步任务。
- 基本使用:
const { AbortController } = require('node:abort - controller');
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort();
}, 2000);
async function asyncTask() {
try {
await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
resolve('Task completed');
}, 5000);
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error('Task aborted'));
});
});
} catch (err) {
console.error(err.message);
}
}
asyncTask();
在这个例子中,我们创建了一个 AbortController
实例 controller
,并从中获取 signal
。然后,我们设置一个定时器,两秒后调用 controller.abort()
来发出取消信号。在异步任务 asyncTask
中,我们创建了一个 Promise,该 Promise 模拟一个需要五秒才能完成的任务。同时,我们为 signal
添加了一个 abort
事件监听器,当接收到取消信号时,清除定时器并拒绝 Promise,抛出错误。
- 结合文件系统操作:
const { AbortController } = require('node:abort - controller');
const fs = require('fs').promises;
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort();
}, 2000);
async function readFileWithAbort() {
try {
const data = await fs.readFile('largeFile.txt', { signal, encoding: 'utf8' });
console.log(data);
} catch (err) {
if (err.name === 'AbortError') {
console.error('File read aborted');
} else {
console.error(err);
}
}
}
readFileWithAbort();
这里,我们在调用 fs.readFile
时,传入了 signal
选项。如果在文件读取过程中发出了取消信号,fs.readFile
会抛出一个 AbortError
,我们可以在 catch
块中捕获并处理这个错误。
手动实现取消机制
在 Node.js 早期版本或者某些不支持 AbortController
的环境中,我们可以手动实现取消机制。这通常涉及到在异步任务中定期检查一个标志变量,以决定是否需要取消任务。
- 基于回调函数的手动取消:
let shouldCancel = false;
function asyncTaskWithCancel(callback) {
let progress = 0;
const intervalId = setInterval(() => {
if (shouldCancel) {
clearInterval(intervalId);
callback(new Error('Task cancelled'));
return;
}
progress += 1;
if (progress >= 10) {
clearInterval(intervalId);
callback(null, 'Task completed');
}
}, 1000);
}
asyncTaskWithCancel((err, result) => {
if (err) {
console.error(err.message);
} else {
console.log(result);
}
});
setTimeout(() => {
shouldCancel = true;
}, 3000);
在这个例子中,我们定义了一个全局变量 shouldCancel
作为取消标志。asyncTaskWithCancel
函数模拟了一个异步任务,通过 setInterval
每隔一秒检查一次 shouldCancel
。如果标志为 true
,则清除定时器并调用回调函数,传递取消错误。如果任务正常完成,则在进度达到 10 时清除定时器并调用回调函数,传递成功结果。
- 基于 Promise 的手动取消:
function asyncTaskWithCancelPromise() {
return new Promise((resolve, reject) => {
let shouldCancel = false;
let progress = 0;
const intervalId = setInterval(() => {
if (shouldCancel) {
clearInterval(intervalId);
reject(new Error('Task cancelled'));
return;
}
progress += 1;
if (progress >= 10) {
clearInterval(intervalId);
resolve('Task completed');
}
}, 1000);
setTimeout(() => {
shouldCancel = true;
}, 3000);
});
}
asyncTaskWithCancelPromise()
.then(result => {
console.log(result);
})
.catch(err => {
console.error(err.message);
});
这里使用 Promise 封装了异步任务,同样通过检查 shouldCancel
标志来决定是否取消任务。如果取消,拒绝 Promise 并抛出错误;如果任务正常完成,解决 Promise 并传递成功结果。
异步任务的超时控制
超时控制是异步任务管理中另一个重要的方面。当一个异步任务运行时间过长时,我们可能希望自动终止它,以避免程序长时间无响应。
使用 setTimeout 实现超时
- 简单的 setTimeout 超时处理:
function asyncTask() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task completed');
}, 5000);
});
}
const timeoutId = setTimeout(() => {
console.error('Task timed out');
}, 3000);
asyncTask()
.then(result => {
clearTimeout(timeoutId);
console.log(result);
})
.catch(err => {
console.error(err);
});
在这个例子中,asyncTask
模拟了一个需要五秒才能完成的异步任务。我们设置了一个三秒的定时器 timeoutId
,如果 asyncTask
在三秒内没有完成,定时器就会触发,打印出 “Task timed out”。如果 asyncTask
正常完成,我们会清除定时器并打印任务结果。
- 封装超时函数:
function withTimeout(promise, ms) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Task timed out'));
}, ms);
promise
.then(result => {
clearTimeout(timeoutId);
resolve(result);
})
.catch(err => {
clearTimeout(timeoutId);
reject(err);
});
});
}
function asyncTask() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task completed');
}, 5000);
});
}
withTimeout(asyncTask(), 3000)
.then(result => {
console.log(result);
})
.catch(err => {
console.error(err.message);
});
这里我们封装了一个 withTimeout
函数,它接受一个 Promise 和一个超时时间 ms
。在 withTimeout
函数内部,我们设置了一个定时器,当定时器触发时,拒绝传入的 Promise 并抛出超时错误。如果传入的 Promise 正常完成,我们清除定时器并解决 withTimeout
返回的 Promise。
使用 AbortController 实现超时
结合 AbortController
,我们可以更优雅地实现超时控制。
- 基本实现:
const { AbortController } = require('node:abort - controller');
function asyncTask() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task completed');
}, 5000);
});
}
const controller = new AbortController();
const { signal } = controller;
const timeoutId = setTimeout(() => {
controller.abort();
}, 3000);
asyncTask()
.then(result => {
clearTimeout(timeoutId);
console.log(result);
})
.catch(err => {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
console.error('Task timed out');
} else {
console.error(err);
}
});
在这个例子中,我们使用 AbortController
来实现超时控制。创建一个 AbortController
实例 controller
,并获取其 signal
。设置一个三秒的定时器,当定时器触发时,调用 controller.abort()
发出取消信号。在 asyncTask
的 catch
块中,我们检查错误是否为 AbortError
,如果是,则表示任务超时。
- 封装超时函数:
const { AbortController } = require('node:abort - controller');
function withTimeoutAbort(promise, ms) {
return new Promise((resolve, reject) => {
const controller = new AbortController();
const { signal } = controller;
const timeoutId = setTimeout(() => {
controller.abort();
}, ms);
promise
.then(result => {
clearTimeout(timeoutId);
resolve(result);
})
.catch(err => {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
reject(new Error('Task timed out'));
} else {
reject(err);
}
});
});
}
function asyncTask() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task completed');
}, 5000);
});
}
withTimeoutAbort(asyncTask(), 3000)
.then(result => {
console.log(result);
})
.catch(err => {
console.error(err.message);
});
这里我们封装了一个基于 AbortController
的超时函数 withTimeoutAbort
。它接受一个 Promise 和一个超时时间 ms
。在函数内部,创建一个 AbortController
,设置定时器在超时时发出取消信号。如果 Promise 正常完成,清除定时器并解决返回的 Promise;如果捕获到 AbortError
,则拒绝返回的 Promise 并抛出超时错误。
复杂场景下的取消与超时控制
在实际应用中,异步任务往往不是孤立存在的,可能涉及到多个异步任务的嵌套、并发执行等复杂场景。下面我们来看一些复杂场景下如何进行取消与超时控制。
并发任务的取消与超时
- 使用 Promise.all 并发执行任务:
const { AbortController } = require('node:abort - controller');
function asyncTask1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task 1 completed');
}, 4000);
});
}
function asyncTask2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task 2 completed');
}, 3000);
});
}
const controller = new AbortController();
const signal = controller.signal;
const tasks = [asyncTask1(), asyncTask2()];
const timeoutId = setTimeout(() => {
controller.abort();
}, 2500);
Promise.all(tasks.map(task => {
return new Promise((resolve, reject) => {
const taskWithSignal = () => task.then(resolve).catch(reject);
signal.addEventListener('abort', () => {
reject(new Error('Tasks aborted'));
});
taskWithSignal();
});
}))
.then(results => {
clearTimeout(timeoutId);
console.log(results);
})
.catch(err => {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
console.error('Tasks timed out or aborted');
} else {
console.error(err);
}
});
在这个例子中,我们使用 Promise.all
并发执行 asyncTask1
和 asyncTask2
。同时,我们设置了一个 AbortController
并添加了一个两秒半的超时定时器。每个任务都添加了对 signal
的 abort
事件监听器,当接收到取消信号时,拒绝相应的 Promise。如果任务正常完成,打印结果;如果超时或取消,捕获错误并处理。
- 使用 Promise.race 实现任务竞争并设置超时:
const { AbortController } = require('node:abort - controller');
function asyncTask1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task 1 completed');
}, 4000);
});
}
function asyncTask2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Task 2 completed');
}, 3000);
});
}
const controller = new AbortController();
const signal = controller.signal;
const timeoutId = setTimeout(() => {
controller.abort();
}, 2500);
Promise.race([asyncTask1(), asyncTask2()].map(task => {
return new Promise((resolve, reject) => {
const taskWithSignal = () => task.then(resolve).catch(reject);
signal.addEventListener('abort', () => {
reject(new Error('Tasks aborted'));
});
taskWithSignal();
});
}))
.then(result => {
clearTimeout(timeoutId);
console.log(result);
})
.catch(err => {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
console.error('Tasks timed out or aborted');
} else {
console.error(err);
}
});
这里使用 Promise.race
,它会返回最先完成的任务的结果。同样设置了 AbortController
和超时定时器,当任何一个任务完成或者接收到取消信号时,相应地处理结果或错误。
嵌套异步任务的取消与超时
- 嵌套回调函数的情况:
let shouldCancel = false;
function innerAsyncTask(callback) {
let progress = 0;
const intervalId = setInterval(() => {
if (shouldCancel) {
clearInterval(intervalId);
callback(new Error('Inner task cancelled'));
return;
}
progress += 1;
if (progress >= 5) {
clearInterval(intervalId);
callback(null, 'Inner task completed');
}
}, 1000);
}
function outerAsyncTask(callback) {
setTimeout(() => {
innerAsyncTask((innerErr, innerResult) => {
if (innerErr) {
callback(innerErr);
return;
}
console.log(innerResult);
callback(null, 'Outer task completed');
});
}, 2000);
}
outerAsyncTask((err, result) => {
if (err) {
console.error(err.message);
} else {
console.log(result);
}
});
setTimeout(() => {
shouldCancel = true;
}, 3000);
在这个例子中,outerAsyncTask
内部调用了 innerAsyncTask
。我们通过全局变量 shouldCancel
来控制任务的取消。innerAsyncTask
每隔一秒检查一次 shouldCancel
,如果为 true
则取消任务。outerAsyncTask
在两秒后启动 innerAsyncTask
,如果 innerAsyncTask
取消或完成,outerAsyncTask
相应地处理结果或错误。
- 嵌套 Promise 的情况:
function innerAsyncTask() {
return new Promise((resolve, reject) => {
let shouldCancel = false;
let progress = 0;
const intervalId = setInterval(() => {
if (shouldCancel) {
clearInterval(intervalId);
reject(new Error('Inner task cancelled'));
return;
}
progress += 1;
if (progress >= 5) {
clearInterval(intervalId);
resolve('Inner task completed');
}
}, 1000);
setTimeout(() => {
shouldCancel = true;
}, 3000);
});
}
function outerAsyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
innerAsyncTask()
.then(innerResult => {
console.log(innerResult);
resolve('Outer task completed');
})
.catch(innerErr => {
reject(innerErr);
});
}, 2000);
});
}
outerAsyncTask()
.then(result => {
console.log(result);
})
.catch(err => {
console.error(err.message);
});
这里使用 Promise 嵌套实现了类似的功能。innerAsyncTask
封装为 Promise,通过内部的 shouldCancel
标志控制取消。outerAsyncTask
在两秒后启动 innerAsyncTask
,并根据 innerAsyncTask
的结果解决或拒绝自身的 Promise。
性能与资源管理
在实现异步任务的取消与超时控制时,性能与资源管理是不可忽视的方面。
资源释放
- 定时器资源:无论是在实现取消还是超时控制时,我们经常会使用
setTimeout
或setInterval
创建定时器。在任务完成或取消时,一定要记得清除这些定时器,以避免内存泄漏和不必要的资源消耗。例如:
const timeoutId = setTimeout(() => {
console.log('Timeout');
}, 3000);
// 任务完成或取消时
clearTimeout(timeoutId);
- 文件描述符等资源:在涉及文件系统操作时,如果任务取消或超时,要确保正确关闭文件描述符,释放相关资源。例如:
const fs = require('fs');
const fd = fs.openSync('example.txt', 'r');
try {
// 异步文件操作逻辑
} catch (err) {
// 取消或超时处理
fs.closeSync(fd);
}
性能优化
- 减少不必要的检查:在手动实现取消机制时,虽然定期检查取消标志是必要的,但过于频繁的检查会影响性能。应该根据任务的性质和预期运行时间,合理设置检查频率。例如,对于一个可能运行较长时间的任务,可以适当延长检查间隔:
let shouldCancel = false;
function asyncTask() {
let progress = 0;
const intervalId = setInterval(() => {
if (shouldCancel) {
clearInterval(intervalId);
// 处理取消逻辑
return;
}
progress += 1;
// 任务逻辑
}, 500); // 适当延长检查间隔
return new Promise((resolve) => {
// 任务完成逻辑
});
}
- 合理使用并发与并行:在处理多个异步任务时,要根据任务的特点和系统资源情况,合理选择并发或并行执行方式。例如,对于 I/O 密集型任务,并发执行可以充分利用系统资源,提高整体效率;而对于 CPU 密集型任务,并行执行可能会导致系统资源过度消耗,反而降低性能。可以使用
cluster
模块在 Node.js 中实现多进程并行处理 CPU 密集型任务,同时结合AbortController
等机制进行任务的取消与超时控制。
通过合理的资源释放和性能优化,可以确保在实现异步任务的取消与超时控制时,程序既能满足功能需求,又能保持良好的性能和资源利用率。
在 Node.js 开发中,熟练掌握异步任务的取消与超时控制,对于构建高效、稳定的应用程序至关重要。无论是简单的单任务场景,还是复杂的并发、嵌套任务场景,都可以通过合适的方法实现灵活的控制。同时,注重性能与资源管理,能够进一步提升应用程序的质量。希望通过本文的介绍,你对 Node.js 中异步任务的取消与超时控制有了更深入的理解和掌握。