一、引言
在 JavaScript 开发中,尤其是在涉及到异步操作和对共享资源的并发访问时,有效地控制请求顺序和资源访问权限至关重要。例如,在多个网络请求同时针对一个有限制访问频率的 API 或者多个异步任务竞争同一个文件写入权限的场景下,若不加以妥善处理,可能会导致数据混乱、请求失败或资源冲突等问题。本文将深入探讨如何在 JavaScript 中创建一个请求队列,当锁被占用时,将请求妥善地放入队列中,待锁释放后,再依次处理队列中的请求,从而确保系统的稳定性和正确性。
二、JS 中的异步锁实现
(一)简易异步锁对象
-
构造函数
- 首先,创建一个名为
AsyncLock
的函数构造器,用于生成异步锁对象实例。在构造函数内部,初始化以下几个重要属性:this.queue
:这是一个数组,用于存储被阻塞的请求任务。初始时,它被设置为空数组,因为还没有任何请求被阻塞。this.isLocked
:这是一个布尔值,用于表示锁的当前状态。初始化为false
,意味着锁是空闲的,资源可供使用。this.version
:这是一个字符串类型的属性,用于记录锁的版本信息。初始版本设置为'0'
,每当锁状态发生改变(如被获取或释放)时,版本号可以相应更新,这有助于在复杂的应用场景中跟踪锁的使用历史和状态变化。
- 以下是构造函数的代码示例:
- 首先,创建一个名为
function AsyncLock() {this.queue = [];this.isLocked = false;this.version = '0';
}
-
获取锁方法 -
lock
- 该方法用于尝试获取锁。如果当前锁已经被其他任务占用(即
this.isLocked === true
),则需要将当前请求任务加入到等待队列this.queue
中。 - 为了实现等待队列的功能,使用
await new Promise(resolve => this.queue.push(resolve));
。这里创建了一个新的Promise
,并将其resolve
函数添加到队列中。当锁被释放时,会从队列中取出这个resolve
函数并执行,从而继续执行被阻塞的任务。 - 如果等待队列的长度已经达到了某个设定的上限(例如 10),则表示系统可能存在资源紧张或者请求过多的情况,此时直接返回
false
,表示获取锁失败,避免队列无限增长导致内存溢出等问题。 - 如果成功获取锁(即锁原本是空闲的),则将
this.isLocked
设置为true
,并生成一个新的锁版本号(可以使用Date.now().toString()
等方式生成一个基于时间戳的唯一字符串作为版本号)。同时,为了防止锁被无限期占用,设置一个超时机制。如果在获取锁时没有传入特定的超时时间参数,则默认在 300 毫秒后自动释放锁。可以使用setTimeout
函数来实现超时释放锁的功能,在超时回调函数中调用this.unlock()
方法释放锁。 - 以下是
lock
方法的完整代码示例:
- 该方法用于尝试获取锁。如果当前锁已经被其他任务占用(即
AsyncLock.prototype.lock = async function (timeout = 300) {if (this.isLocked) {if (this.queue.length < 10) {await new Promise(resolve => this.queue.push(resolve));} else {return false;}}this.isLocked = true;this.version = Date.now().toString();const timer = setTimeout(() => {this.unlock();}, timeout);return true;
};
-
释放锁方法 -
unlock
- 该方法用于释放当前持有的锁。如果等待队列
this.queue
中有等待的任务(即this.queue.length > 0
),则从队列中取出第一个任务的resolve
函数并执行,这将唤醒被阻塞的任务,使其继续执行后续操作。通过const resolve = this.queue.shift(); resolve();
实现从队列头部取出并执行resolve
函数。 - 如果等待队列为空,说明没有任务在等待锁,此时将
this.isLocked
设置为false
,表示锁已经被释放,可以被其他任务获取。 - 以下是
unlock
方法的代码示例:
- 该方法用于释放当前持有的锁。如果等待队列
AsyncLock.prototype.unlock = function () {if (this.queue.length > 0) {const resolve = this.queue.shift();resolve();} else {this.isLocked = false;}
};
-
包装锁的获取与释放方法 -
withLock
- 为了更方便地在外部使用锁机制,创建一个
withLock
方法。这个方法是一个async
函数,它内部首先调用this.lock
方法尝试获取锁。如果获取锁成功(返回true
),则执行传入的任务函数(假设为task
),并在任务完成后调用this.unlock
方法释放锁。如果获取锁失败(返回false
),则直接返回一个表示locked
状态的对象或值,以便外部代码进行相应的处理。 - 以下是
withLock
方法的代码示例:
- 为了更方便地在外部使用锁机制,创建一个
AsyncLock.prototype.withLock = async function (task) {const locked = await this.lock();if (locked) {try {return await task();} finally {this.unlock();}} else {return { locked: true };}
};
三、使用示例
1、创建AsyncLock
的实例:
const asyncLock = new AsyncLock();
2、 定义一个异步函数test
来模拟请求任务:
async function test() {const result = await asyncLock.withLock(async () => {console.log('执行任务');// 这里可以添加具体的任务逻辑,例如网络请求、文件操作等return '任务完成';});if (result.locked) {console.log('获取锁失败');} else {console.log(result);}
}
3、 多次调用test
函数来测试锁机制:
test();
test();
test();
在上述示例中,当第一个test
函数调用时,如果锁空闲,它将获取锁并执行任务,打印 “执行任务”,然后释放锁。当第二个和第三个test
函数调用时,如果第一个test
函数还未释放锁,它们将被加入等待队列。一旦锁被释放,等待队列中的任务将依次被执行,从而实现了请求队列与锁机制的协同工作。
四、总结
在 JavaScript 中,通过精心设计和实现请求队列与异步锁的结合,可以有效地应对并发访问共享资源时可能出现的各种问题。这种机制不仅能够确保资源在同一时间只有一个任务能够访问,避免了数据竞争和冲突,而且通过合理的等待队列管理,能够有条不紊地处理多个请求,提高了系统的整体稳定性和可靠性。在实际的大型 JavaScript 应用开发中,尤其是涉及到复杂的异步操作和资源管理场景时,深入理解和熟练运用这种请求队列与锁的技术是非常必要的,可以帮助我们构建出更加健壮和高效的应用程序。