如何在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ı
相关产品推荐
相关产品推荐

