← 中文资源

简体中文版本

带 FIFO 等待队列的异步互斥锁

按先进先出顺序串行化临界区,支持获取超时、可重入保护与安全的重复释放。

阅读英文原文

功能说明

该示例用等待队列实现进程内异步互斥锁。acquire 在空闲时立即拿锁并返回释放函数,被占用时把 resolve 与可选定时器放入队列,释放时唤醒队首,从而保证 FIFO 公平性。runExclusive 把获取、执行与 finally 释放封装成一次调用,适合保护写文件、更新共享缓存或调用不支持并发的后端接口。

关键代码

createRelease 使用闭包内的 released 标记,重复调用释放函数不会重复唤醒后续等待者,也不会把锁状态改乱。超时定时器触发时先在队列中查找并移除自己,再拒绝 Promise,避免已超时的等待者之后被误唤醒。释放时若队列非空则清除该等待者的定时器并把新的释放函数交给它,锁保持占用;只有队列为空才把 locked 置回 false。

使用注意

该锁不可重入:持有锁的代码再次 acquire 会永久等待,需要重入语义时应记录持有者或改用计数信号量。timeoutMs 只限制排队时间,不限制任务执行时间,长任务仍需单独的截止时间控制。它是单进程内锁,不能替代数据库行锁或分布式锁。acquire 的参数校验发生在 async 函数内,因此非法参数表现为被拒绝的 Promise 而不是同步抛错。

验证方式

启动多个 runExclusive 任务并记录并发计数,断言最大值始终为一;按调用顺序观察进入临界区的次序,验证 FIFO;让一个等待者超时,确认其 Promise 拒绝且后续释放不会唤醒它;连续调用两次 release,确认只有一个等待者被唤醒;最后测试非法 timeoutMs 会拒绝。

代码示例

export class AsyncMutex {
  #locked = false;
  #waiters = [];

  get locked() {
    return this.#locked;
  }

  get queued() {
    return this.#waiters.length;
  }

  async acquire(options = {}) {
    const { timeoutMs } = options;
    if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
      throw new RangeError("timeoutMs must be a positive finite number");
    }
    if (!this.#locked) {
      this.#locked = true;
      return this.#createRelease();
    }
    return new Promise((resolve, reject) => {
      const waiter = { resolve, reject, timer: undefined };
      if (timeoutMs !== undefined) {
        waiter.timer = setTimeout(() => {
          const index = this.#waiters.indexOf(waiter);
          if (index === -1) return;
          this.#waiters.splice(index, 1);
          reject(new Error("timed out waiting for mutex after " + timeoutMs + "ms"));
        }, timeoutMs);
      }
      this.#waiters.push(waiter);
    });
  }

  #createRelease() {
    let released = false;
    return () => {
      if (released) return;
      released = true;
      const next = this.#waiters.shift();
      if (next) {
        if (next.timer !== undefined) clearTimeout(next.timer);
        next.resolve(this.#createRelease());
      } else {
        this.#locked = false;
      }
    };
  }

  async runExclusive(task, options) {
    const release = await this.acquire(options);
    try {
      return await task();
    } finally {
      release();
    }
  }
}