You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在JavaScript中防止race condition?Resource Manager原子操作实现求助

用DOM存储信号量,搞定Resource Manager的竞态问题

嘿,我来帮你实现这个基于DOM的信号量方案,刚好能解决你Resource Manager里的原子性任务需求!

一、为啥选DOM存信号量?

浏览器里的DOM属性操作是原子性的——同一时间只有一个线程能修改DOM元素的属性,这天然适合用来模拟锁的状态。而且DOM是全局可访问的,不管你的Resource Manager模块在哪,都能拿到锁的状态。

二、核心Semaphore实现

先写一个可复用的Semaphore类,我们会创建一个隐藏的DOM元素来存储锁的计数:

class DOMSemaphore {
  constructor(maxConcurrency = 1, domElementId = 'resource-semaphore') {
    // 找现成的DOM元素,没有就创建一个隐藏的div
    this.semaphoreElement = document.getElementById(domElementId);
    if (!this.semaphoreElement) {
      this.semaphoreElement = document.createElement('div');
      this.semaphoreElement.id = domElementId;
      this.semaphoreElement.style.display = 'none'; // 藏起来不影响页面
      document.body.appendChild(this.semaphoreElement);
    }
    // 用dataset存信号量计数(规范的自定义属性方式)
    this.semaphoreElement.dataset.count = maxConcurrency.toString();
    this.maxConcurrency = maxConcurrency;
    this.timeoutMs = 5000; // 默认5秒超时,你可以按需修改
  }

  // 拿锁:直到拿到或超时
  async acquire() {
    const startTime = Date.now();
    return new Promise((resolve, reject) => {
      const checkLock = () => {
        const currentCount = parseInt(this.semaphoreElement.dataset.count);
        if (currentCount > 0) {
          // 原子性减1,成功拿到锁
          this.semaphoreElement.dataset.count = (currentCount - 1).toString();
          resolve(true);
          return;
        }
        // 超时就抛错,避免无限等待
        if (Date.now() - startTime >= this.timeoutMs) {
          reject(new Error('获取锁超时'));
          return;
        }
        // 没拿到锁就继续轮询,用requestAnimationFrame不卡主线程
        requestAnimationFrame(checkLock);
      };
      checkLock();
    });
  }

  // 放锁:用完一定要释放,避免死锁!
  release() {
    const currentCount = parseInt(this.semaphoreElement.dataset.count);
    if (currentCount < this.maxConcurrency) {
      this.semaphoreElement.dataset.count = (currentCount + 1).toString();
    } else {
      console.warn('警告:释放锁的次数超过获取次数!');
    }
  }
}

三、把retrievePartners改成原子性方法

假设你的retrievePartners是个异步方法,容易引发竞态,我们用上面的Semaphore来包裹它:

// 初始化一个互斥锁(同一时间只允许1个任务执行)
const resourceLock = new DOMSemaphore(1);

async function retrievePartners() {
  let gotLock = false;
  try {
    // 先获取锁,超时则抛出错误
    await resourceLock.acquire();
    gotLock = true;

    // --- 这里写你原来的retrievePartners核心逻辑 ---
    console.log('开始执行retrievePartners,当前无资源竞争');
    // 比如调用接口、操作共享数据等
    const partners = await fetch('/api/partners').then(res => res.json());
    // ...你的其他业务代码

    return partners;
  } catch (err) {
    console.error('获取锁失败或任务执行出错:', err);
    throw err; // 向上抛出错误让调用者处理
  } finally {
    // 无论成功失败,都要释放锁!不然锁会被永久占用
    if (gotLock) {
      resourceLock.release();
      console.log('锁已释放,允许其他任务执行');
    }
  }
}

四、关键细节唠一唠

  • DOM变量怎么添加的?:在Semaphore的构造函数里,我们要么查找已有ID的元素,要么新建一个隐藏div添加到body中,用它的dataset.count属性存储信号量剩余名额,完全不会影响页面显示。
  • 轮询为啥用requestAnimationFrame?:比setInterval更友好,会跟着浏览器的刷新节奏执行,不会在页面卡顿的时候无效轮询,减少性能消耗。
  • 超时机制很重要:如果某个任务意外卡死,超时能避免其他任务无限等待,你可以根据业务场景调整timeoutMs参数。
  • finally块一定要释放锁:哪怕任务执行出错抛出异常,锁也必须释放,否则整个Resource Manager会陷入死锁状态。

五、拓展:支持多并发场景

要是你想允许有限个任务同时执行(比如最多3个),初始化Semaphore时修改参数即可:

const resourceSemaphore = new DOMSemaphore(3);

这样就能同时运行3个任务,超出的任务会排队等待锁释放~

内容的提问来源于stack exchange,提问作者Rasim Avcı

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:04:24