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

如何在访问用户脚本存储时实现原子性(及更通用的并发安全性)?

如何在访问用户脚本存储时实现原子性(及更通用的并发安全性)?

太懂你这种头疼的情况了——就像你那个计数器脚本,开两个标签页跑START,结果最后拿到64而不是预期的100,完全是异步存储操作的竞态搞的鬼。现代用户脚本管理器的GM存储API都是异步的,多个脚本实例(或同脚本多标签页)并发读写时,很容易打破我们默认的原子性假设。下面我给你几个实用的解决方案,覆盖你提到的CAS、重试更新和临界区需求,优先用Greasemonkey原生API,也会提其他管理器的专属方案。

一、实现Compare-and-Swap(CAS)原语

CAS是很多并发控制的基础,核心逻辑是:只有当存储的当前值和我们预期的值一致时,才写入新值。我们可以用Greasemonkey的GM.getValue和GM.setValue组合实现这个逻辑:

/**
 * 基于GM API的CAS操作
 * @param {string} key 存储键名
 * @param {any} expectedValue 预期的当前值
 * @param {any} newValue 要写入的新值
 * @returns {Promise<boolean>} 是否成功完成CAS操作
 */
async function gmCompareAndSwap(key, expectedValue, newValue) {
    // GM.getValue本身是原子性的,先获取当前值
    const currentValue = await GM.getValue(key);
    // 用JSON.stringify处理引用类型的内容对比,基础类型可直接用===
    if (JSON.stringify(currentValue) === JSON.stringify(expectedValue)) {
        await GM.setValue(key, newValue);
        return true;
    }
    return false;
}

二、带重试的“弱更新”原语

有了CAS,我们可以实现自动重试的更新逻辑——当检测到竞态(CAS失败)时,重新获取当前值再尝试更新,直到成功或达到重试上限:

/**
 * 带重试的原子更新操作
 * @param {string} key 存储键名
 * @param {function(any): any} updateFn 接收当前值,返回新值的函数
 * @param {number} maxRetries 最大重试次数,默认10
 * @returns {Promise<any>} 更新后的最终值
 */
async function gmAtomicUpdate(key, updateFn, maxRetries = 10) {
    let retries = 0;
    while (retries < maxRetries) {
        const currentValue = await GM.getValue(key) ?? 0;
        const newValue = updateFn(currentValue);
        const success = await gmCompareAndSwap(key, currentValue, newValue);
        if (success) {
            return newValue;
        }
        retries++;
        // 加微小延迟避免频繁重试
        await new Promise(resolve => setTimeout(resolve, 10));
    }
    throw new Error(`原子更新失败,已达到最大重试次数${maxRetries}`);
}

用这个方法改造你的计数器逻辑,就能彻底避免竞态:

const KEY = 'counter';
GM.registerMenuCommand("[COUNTER] START", () => {
    (async () => {
        const x0 = await GM.getValue(KEY) ?? 0;
        const iters = 50;
        for (let i = 0; i < iters; ++i) {
            await gmAtomicUpdate(KEY, (current) => {
                console.info(`[COUNTER] iteration ${i}: ${current}`);
                return current + 1;
            });
        }
        const x1 = await GM.getValue(KEY);
        alert(`[COUNTER] ran from ${x0} to ${x1} in ${iters} iterations`);
    })().catch(err => {
        console.error(`[COUNTER]`, err);
    });
});

三、临界区(Mutex/互斥锁)原语

如果你需要一整块代码的互斥执行(而非单个更新操作),可以基于CAS实现跨标签页生效的互斥锁:

const LOCK_KEY = "__gm_mutex_lock__";

/**
 * 获取互斥锁
 * @param {number} timeout 超时时间(毫秒),默认5000
 * @returns {Promise<function()>} 释放锁的函数
 */
async function gmLock(timeout = 5000) {
    const startTime = Date.now();
    while (Date.now() - startTime < timeout) {
        // 尝试获取锁:仅当LOCK_KEY未被占用时设置为true
        const locked = await gmCompareAndSwap(LOCK_KEY, undefined, true);
        if (locked) {
            // 返回释放锁的函数,确保锁能被正确释放
            return async () => {
                await GM.setValue(LOCK_KEY, undefined);
            };
        }
        // 未拿到锁则等待后重试
        await new Promise(resolve => setTimeout(resolve, 50));
    }
    throw new Error("获取锁超时");
}

使用时一定要用try/finally确保锁被释放(即使代码出错):

GM.registerMenuCommand("[COUNTER] START WITH LOCK", () => {
    (async () => {
        const releaseLock = await gmLock();
        try {
            const x0 = await GM.getValue(KEY) ?? 0;
            const iters = 50;
            for (let i = 0; i < iters; ++i) {
                const x = await GM.getValue(KEY) ?? 0;
                console.info(`[COUNTER] iteration ${i}: ${x}`);
                await GM.setValue(KEY, 1 + x);
            }
            const x1 = await GM.getValue(KEY);
            alert(`[COUNTER] ran from ${x0} to ${x1} in ${iters} iterations`);
        } finally {
            // 无论成功失败都释放锁
            await releaseLock();
        }
    })().catch(err => {
        console.error(`[COUNTER]`, err);
    });
});

四、其他用户脚本管理器的专属方案

如果你用的是Tampermonkey或Violentmonkey,有更直接的原生支持:

  • Tampermonkey:提供GM.addValueChangeListener监听存储变化,部分版本支持GM.transaction实现原子读写事务。
  • Violentmonkey:默认保证GM.setValue的原子性,还提供GM.scriptHandler管理脚本实例并发。

关键注意事项

  • 所有方案都考虑了脚本突然终止的情况:用try/finally确保锁释放,CAS和重试逻辑本身是幂等的,中途中断不会破坏数据一致性。
  • GM存储是跨域的,这些方案天然支持跨域名的并发控制,无需额外处理同源问题。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 06:49:50