一、引言

在 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 应用开发中,尤其是涉及到复杂的异步操作和资源管理场景时,深入理解和熟练运用这种请求队列与锁的技术是非常必要的,可以帮助我们构建出更加健壮和高效的应用程序。



















