WebLocks API:在浏览器中实现跨 Tab 页/Worker 的互斥锁(Mutex)

各位同仁,各位开发者,大家好!

在现代Web应用中,我们常常面临一个挑战:如何在同一个用户代理(浏览器)中,跨越不同的Tab页、iframe,甚至是Web Worker,来协调和同步资源访问?想象一下,用户可能同时打开了您的应用的多个页面,或者您的应用正在后台使用Service Worker或Dedicated Worker处理任务。在这种分布式环境中,如果不对共享资源进行适当的控制,就可能导致数据不一致、重复操作、竞态条件乃至应用崩溃。

今天,我们将深入探讨一个专门为解决这类问题而设计的Web标准API——Web Locks API。它提供了一种在浏览器环境中实现互斥锁(Mutex)的机制,使得跨Tab页和Worker的资源同步变得前所未止的简便和可靠。

浏览器环境下的并发困境

在深入了解Web Locks API之前,我们首先需要理解为什么它如此重要,以及在它出现之前,开发者们是如何尝试解决这些并发问题的,以及这些方案的局限性。

Web浏览器是一个多进程、多线程的环境。每个Tab页通常运行在独立的渲染进程中,但它们共享一些全局资源,比如本地存储(localStorage)。Web Worker,包括Dedicated Worker、Shared Worker和Service Worker,也在不同的线程中执行,但它们同样可能需要访问共享数据或执行全局唯一的任务。

如果没有一个可靠的同步机制,当多个执行上下文(例如,两个不同的Tab页)尝试同时修改同一个数据或执行同一个操作时,就会出现问题:

  1. 竞态条件(Race Conditions):操作的最终结果取决于执行的精确时序。
  2. 数据不一致(Data Inconsistency):一个上下文的修改被另一个上下文意外覆盖,或者读取到过期的数据。
  3. 重复操作(Duplicate Operations):例如,多个Tab页同时向服务器发送相同的初始化请求,造成不必要的负载或错误。

过去,开发者们尝试了多种方法来模拟互斥锁的行为:

1. 基于 localStorage 的简易锁

最常见也最直观的方法是利用 localStoragelocalStorage 是跨Tab页共享的,并且其写操作是原子性的(即整个键值对要么完全写入,要么不写入,不会出现部分写入的状态)。

基本思路是:一个Tab页尝试设置一个特定的 localStorage 键作为锁,如果设置成功(即该键之前不存在),则认为自己获得了锁;否则,就等待或重试。

// 简易的 localStorage 锁实现 (存在严重缺陷)
async function acquireLocalStorageLock(lockName, timeout = 5000) {
    const lockKey = `lock_${lockName}`;
    const expiryKey = `lock_expiry_${lockName}`;
    const lockDuration = 2000; // 锁持有时间,防止死锁

    const startTime = Date.now();

    while (Date.now() - startTime < timeout) {
        const now = Date.now();
        const existingExpiry = parseInt(localStorage.getItem(expiryKey) || '0', 10);

        // 如果锁已过期,或者锁不存在,尝试获取
        if (now > existingExpiry || !localStorage.getItem(lockKey)) {
            // 尝试设置锁,并记录过期时间
            // 这里的操作不是原子的,存在竞态条件!
            localStorage.setItem(lockKey, 'locked');
            localStorage.setItem(expiryKey, String(now + lockDuration));

            // 再次检查,确保自己是唯一成功设置锁的
            // 仍然不能完全避免竞态
            if (localStorage.getItem(lockKey) === 'locked' &&
                parseInt(localStorage.getItem(expiryKey) || '0', 10) === (now + lockDuration)) {
                console.log(`Tab ${window.name || 'Unknown'} acquired lock: ${lockName}`);
                return true;
            }
        }

        await new Promise(resolve => setTimeout(resolve, 50)); // 短暂等待后重试
    }

    console.log(`Tab ${window.name || 'Unknown'} failed to acquire lock: ${lockName}`);
    return false;
}

function releaseLocalStorageLock(lockName) {
    const lockKey = `lock_${lockName}`;
    const expiryKey = `lock_expiry_${lockName}`;
    localStorage.removeItem(lockKey);
    localStorage.removeItem(expiryKey);
    console.log(`Tab ${window.name || 'Unknown'} released lock: ${lockName}`);
}

// 示例使用
async function fetchDataWithLocalStorageLock() {
    const LOCK_NAME = 'my_fetch_data_lock';
    if (await acquireLocalStorageLock(LOCK_NAME)) {
        try {
            console.log(`Tab ${window.name || 'Unknown'} is fetching data...`);
            // 模拟API请求
            await new Promise(resolve => setTimeout(resolve, 1500));
            console.log(`Tab ${window.name || 'Unknown'} finished fetching data.`);
        } finally {
            releaseLocalStorageLock(LOCK_NAME);
        }
    } else {
        console.log(`Tab ${window.name || 'Unknown'} skipped fetching data, another tab has the lock.`);
    }
}

// 每个tab页都尝试执行
// fetchDataWithLocalStorageLock();

localStorage 锁的局限性:

  • 竞态条件localStorage.setItem 虽然原子,但“检查-然后-设置”的整个逻辑并非原子。两个Tab页可能同时检查到锁不存在或已过期,然后都尝试设置,导致都认为自己获得了锁。
  • 轮询(Polling):为了等待锁的释放,需要不断地检查 localStorage,这会消耗CPU资源。
  • 死锁风险:如果持有锁的Tab页崩溃或意外关闭,而没有及时释放锁,其他Tab页将永远无法获取到锁(尽管可以通过设置过期时间来缓解,但这增加了复杂性)。
  • 缺乏通知机制:没有办法知道锁何时被释放,只能通过轮询或 storage 事件(它只在不同Tab页修改时触发,无法用于等待同一个Tab页的锁)。

2. BroadcastChannel 进行消息协调

BroadcastChannel 允许同一源(origin)下的所有浏览上下文(Tab页、iframe、Worker)进行双向通信。它可以用于通知其他Tab页某个操作已经开始或结束。

// 使用 BroadcastChannel 协调
const channel = new BroadcastChannel('my_app_channel');
let isProcessing = false;

channel.onmessage = (event) => {
    if (event.data === 'start_processing') {
        isProcessing = true;
        console.log(`Tab ${window.name || 'Unknown'} received start_processing.`);
    } else if (event.data === 'end_processing') {
        isProcessing = false;
        console.log(`Tab ${window.name || 'Unknown'} received end_processing.`);
    }
};

async function fetchDataWithBroadcastChannel() {
    if (isProcessing) {
        console.log(`Tab ${window.name || 'Unknown'} skipped fetching data, another tab is processing.`);
        return;
    }

    // 这里仍然存在竞态条件:
    // 两个tab可能同时检查 isProcessing 为 false,然后都尝试设置 isProcessing = true 并发送消息。
    isProcessing = true;
    channel.postMessage('start_processing');

    try {
        console.log(`Tab ${window.name || 'Unknown'} is fetching data...`);
        await new Promise(resolve => setTimeout(resolve, 1500));
        console.log(`Tab ${window.name || 'Unknown'} finished fetching data.`);
    } finally {
        isProcessing = false;
        channel.postMessage('end_processing');
    }
}

// fetchDataWithBroadcastChannel();

BroadcastChannel 的局限性:

  • 仅用于消息传递:它本身不提供互斥锁语义。虽然可以用来通知状态,但“谁先收到消息”、“谁有权执行”等问题仍然需要额外的逻辑来解决,且同样面临竞态条件。
  • 无法保证原子性:一个Tab页发送“我开始处理了”的消息,在消息到达其他所有Tab页之前,另一个Tab页可能已经开始处理了。
  • 状态管理复杂:需要手动管理所有Tab页的状态,容易出错。

3. IndexedDB 实现复杂锁

IndexedDB 提供了事务机制,可以在一定程度上实现更可靠的锁。通过在一个对象存储中创建一个“锁”记录,并利用 add 操作的唯一性约束(如果记录已存在则失败),可以模拟锁的获取。事务的原子性可以保证“检查-然后-设置”的原子性。

// 概念性的 IndexedDB 锁 (实际实现会非常复杂)
async function acquireIndexedDBLock(lockName, dbName = 'my_locks_db') {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(dbName, 1);

        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            db.createObjectStore('locks', { keyPath: 'name' });
        };

        request.onsuccess = (event) => {
            const db = event.target.result;
            const transaction = db.transaction('locks', 'readwrite');
            const store = transaction.objectStore('locks');

            try {
                // 尝试添加锁记录。如果记录已存在,则会抛出 ConstraintError
                const addRequest = store.add({ name: lockName, timestamp: Date.now() });

                addRequest.onsuccess = () => {
                    console.log(`Tab ${window.name || 'Unknown'} acquired IndexedDB lock: ${lockName}`);
                    resolve(true);
                };

                addRequest.onerror = (e) => {
                    // 如果是 ConstraintError,说明锁已被其他Tab页持有
                    if (e.target.error.name === 'ConstraintError') {
                        console.log(`Tab ${window.name || 'Unknown'} failed to acquire IndexedDB lock: ${lockName}`);
                        resolve(false);
                    } else {
                        console.log(`Tab ${window.name || 'Unknown'} IndexedDB error:`, e.target.error);
                        reject(e.target.error);
                    }
                };

                transaction.oncomplete = () => {
                    db.close();
                };

                transaction.onerror = (e) => {
                    console.error('IndexedDB transaction error:', e.target.error);
                    db.close();
                    reject(e.target.error);
                };

            } catch (e) {
                // 捕获同步错误,如事务已关闭
                console.error('IndexedDB lock acquisition error:', e);
                db.close();
                reject(e);
            }
        };

        request.onerror = (event) => {
            console.error('IndexedDB open error:', event.target.errorCode);
            reject(event.target.error);
        };
    });
}

async function releaseIndexedDBLock(lockName, dbName = 'my_locks_db') {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(dbName, 1);

        request.onsuccess = (event) => {
            const db = event.target.result;
            const transaction = db.transaction('locks', 'readwrite');
            const store = transaction.objectStore('locks');
            const deleteRequest = store.delete(lockName);

            deleteRequest.onsuccess = () => {
                console.log(`Tab ${window.name || 'Unknown'} released IndexedDB lock: ${lockName}`);
                resolve(true);
            };

            deleteRequest.onerror = (e) => {
                console.error('IndexedDB delete error:', e.target.error);
                reject(e.target.error);
            };

            transaction.oncomplete = () => {
                db.close();
            };

            transaction.onerror = (e) => {
                console.error('IndexedDB transaction error:', e.target.error);
                db.close();
                reject(e.target.error);
            };
        };

        request.onerror = (event) => {
            console.error('IndexedDB open error:', event.target.errorCode);
            reject(event.target.error);
        };
    });
}

// 示例使用
async function fetchDataWithIndexedDBLock() {
    const LOCK_NAME = 'my_indexeddb_fetch_data_lock';
    if (await acquireIndexedDBLock(LOCK_NAME)) {
        try {
            console.log(`Tab ${window.name || 'Unknown'} is fetching data via IndexedDB lock...`);
            await new Promise(resolve => setTimeout(resolve, 1500));
            console.log(`Tab ${window.name || 'Unknown'} finished fetching data via IndexedDB lock.`);
        } finally {
            await releaseIndexedDBLock(LOCK_NAME);
        }
    } else {
        console.log(`Tab ${window.name || 'Unknown'} skipped fetching data, another tab has the IndexedDB lock.`);
    }
}

// fetchDataWithIndexedDBLock();

IndexedDB 锁的局限性:

  • 实现复杂:虽然解决了原子性问题,但代码量大,错误处理繁琐,需要处理数据库的打开、升级、事务管理、错误回调等。
  • 不提供等待机制:如果锁被占用,当前Tab页只能得到一个失败结果,无法像传统互斥锁那样“等待”锁的释放。需要自己实现轮询或基于 BroadcastChannel 的通知机制,这又回到了之前的复杂性。
  • 死锁管理:同样需要考虑崩溃后的死锁问题,需要额外的逻辑来清理陈旧的锁记录。

这些传统方法要么不够健壮,要么实现过于复杂,且都缺乏一个标准的、浏览器原生支持的等待队列和自动释放机制。这就是 Web Locks API 应运而生并大放异彩的原因。

引入 Web Locks API:浏览器原生的互斥锁

Web Locks API 提供了一个标准化的、浏览器原生的方式来在同一源(origin)下的所有浏览上下文(包括Tab页、iframe 和 Web Worker)之间协调对资源的访问。它解决了上述所有传统方法的痛点,提供了一个可靠、简单且高效的互斥锁机制。

核心概念:

概念 描述
锁的名称 一个字符串,用于唯一标识要保护的资源。所有尝试获取相同名称锁的上下文都会相互协调。
锁模式 定义了锁的类型,影响其与其他锁的兼容性。主要有两种:"exclusive"(排他锁)和 "shared"(共享锁)。
排他锁 ("exclusive"):最常见的互斥锁。一旦被一个上下文持有,其他任何上下文都无法获取同名锁(无论是排他还是共享),直到当前锁被释放。适用于写操作,确保数据一致性。
共享锁 ("shared"):允许多个上下文同时持有同名锁,但前提是没有排他锁被持有。适用于读操作,允许多个读者并发访问。一旦有排他锁请求,所有共享锁都会被阻塞或等待。
请求锁 通过 navigator.locks.request() 方法发起。它是一个异步操作,返回一个 Promise。当 Promise resolve 时,表示成功获取了锁。
回调函数 navigator.locks.request() 的第二个参数(或 options 对象中的 callback 属性)是一个函数,当锁被成功获取时,该函数会被执行。在这个函数内部执行受保护的代码。
自动释放 当回调函数执行完毕(无论是正常返回、抛出错误,还是返回的 Promise settled),或者当持有锁的浏览上下文关闭时,锁会自动释放。这极大地简化了错误处理和死锁预防。
ifAvailable 选项 一个布尔值,如果设置为 true,则尝试立即获取锁。如果无法获取(因为锁已被占用),则 Promise 会立即 resolve 为 undefined,而不是等待。这适用于非阻塞场景。
steal 选项 一个布尔值,如果设置为 true,则尝试强制获取锁。如果锁已被其他上下文持有,steal 会导致现有锁被释放,然后当前上下文获取锁。这是一个强大的功能,但必须谨慎使用,因为它可能中断正在进行的操作。
signal 选项 一个 AbortSignal 对象。可以通过它来取消一个正在等待锁的请求。当 AbortSignal 被触发时,request() 返回的 Promise 会以 AbortError 拒绝。
Lock 对象 当锁被成功获取并执行回调函数时,会向回调函数传入一个 Lock 对象,其中包含锁的 namemode 信息。

浏览器支持:

Web Locks API 得到了主流浏览器的广泛支持,包括 Chrome (87+), Firefox (99+), Edge (87+), Safari (15.4+)。这意味着您可以在绝大多数现代Web应用中放心使用它。

Web Locks API 的基本用法:navigator.locks.request()

Web Locks API 的核心是 navigator.locks.request() 方法。它允许您请求一个带有特定名称和模式的锁,并在成功获取锁后执行一段代码。

该方法接收两个主要参数:

  1. name: 锁的名称,一个字符串。所有请求相同名称锁的上下文都会相互协调。
  2. optionscallback:
    • 如果只传递一个函数,它会被作为回调函数在获取锁后执行。
    • 如果传递一个对象,它是一个 LockOptions 对象,可以包含 modeifAvailablestealsignal 等选项,以及一个 callback 函数。

navigator.locks.request() 返回一个 Promise。当锁被成功获取并且回调函数执行完毕后(或者回调函数返回的 Promise settled),这个 Promise 会 resolve。如果锁请求失败(例如,ifAvailabletrue 但锁不可用),或者被取消,Promise 会 reject 或 resolve 为 undefined

// 基本的排他锁请求
async function performExclusiveTask(taskId) {
    const LOCK_NAME = 'my_exclusive_resource';

    console.log(`Tab ${window.name || 'Unknown'} - Task ${taskId}: Attempting to acquire exclusive lock "${LOCK_NAME}"...`);

    try {
        // request() 返回一个 Promise,当锁被获取且回调执行完毕后 resolve
        await navigator.locks.request(LOCK_NAME, async (lock) => {
            // lock 参数是一个 Lock 对象,包含 name 和 mode
            console.log(`Tab ${window.name || 'Unknown'} - Task ${taskId}: Acquired exclusive lock "${lock.name}" (mode: ${lock.mode}).`);

            // 在这里执行需要互斥保护的代码
            // 模拟一个耗时操作,例如写入数据
            await new Promise(resolve => setTimeout(resolve, 2000));

            console.log(`Tab ${window.name || 'Unknown'} - Task ${taskId}: Finished exclusive operation.`);
        });
        console.log(`Tab ${window.name || 'Unknown'} - Task ${taskId}: Lock "${LOCK_NAME}" released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'} - Task ${taskId}: Failed to acquire or process with lock "${LOCK_NAME}". Error:`, error);
    }
}

// 示例:在两个不同的Tab页中调用
// Tab A: performExclusiveTask(1);
// Tab B: performExclusiveTask(2);

// 预期行为:
// Tab A 请求锁,成功获取并执行。
// Tab B 请求锁,由于 Tab A 持有排他锁,Tab B 会等待。
// Tab A 释放锁后,Tab B 立即获取锁并执行。

关键点:

  • 异步性request() 是异步的,它会等待锁可用。
  • 回调函数:所有受保护的代码都应该放在回调函数内部。
  • 自动释放:一旦回调函数执行完毕(无论是正常完成、抛出错误,还是回调函数返回的 Promise settled),或者当前Tab页/Worker关闭,Web Locks API 会自动释放锁。这极大地简化了资源管理,避免了死锁的风险。
  • Lock 对象:回调函数会接收一个 Lock 对象作为参数,可以用来获取当前锁的名称和模式。

锁模式:排他(Exclusive) vs. 共享(Shared)

Web Locks API 提供了两种基本的锁模式,它们定义了锁在并发访问时的行为。理解这两种模式对于正确设计并发策略至关重要。

1. 排他锁 ("exclusive")

这是默认模式,也是最常见的互斥锁。

  • 行为:当一个上下文持有排他锁时,其他任何上下文都无法获取同名的锁,无论是排他锁还是共享锁。
  • 用途:适用于需要修改共享资源的操作,例如写入数据、更新状态、执行一次性任务等。它确保了在任何给定时间只有一个上下文可以修改资源,从而避免了竞态条件和数据不一致。
  • 指定方式
    • navigator.locks.request(name, callback) (默认就是排他锁)
    • navigator.locks.request(name, { mode: 'exclusive', callback })

排他锁示例:

async function updateSharedCounter(incrementValue) {
    const LOCK_NAME = 'shared_counter_lock';
    const COUNTER_KEY = 'global_counter';

    console.log(`Tab ${window.name || 'Unknown'}: Attempting to update counter by ${incrementValue}.`);

    try {
        await navigator.locks.request(LOCK_NAME, { mode: 'exclusive' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired exclusive lock for counter.`);

            // 模拟读取和修改操作
            let currentCounter = parseInt(localStorage.getItem(COUNTER_KEY) || '0', 10);
            console.log(`Tab ${window.name || 'Unknown'}: Current counter value before update: ${currentCounter}`);

            currentCounter += incrementValue;
            await new Promise(resolve => setTimeout(resolve, 500)); // 模拟计算耗时
            localStorage.setItem(COUNTER_KEY, String(currentCounter));

            console.log(`Tab ${window.name || 'Unknown'}: Counter updated to: ${currentCounter}`);
        });
        console.log(`Tab ${window.name || 'Unknown'}: Exclusive lock for counter released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Failed to update counter. Error:`, error);
    }
}

// 在多个Tab页中并发调用
// updateSharedCounter(1); // Tab 1
// updateSharedCounter(1); // Tab 2
// updateSharedCounter(1); // Tab 3

// 预期结果:即使并发调用,由于排他锁的存在,计数器会按顺序正确递增,不会出现丢失更新的情况。
// 最终 localStorage.global_counter 会是 3。

2. 共享锁 ("shared")

共享锁允许多个上下文同时持有同名锁,但前提是没有排他锁被持有。

  • 行为
    • 多个上下文可以同时持有共享锁。
    • 如果一个上下文持有排他锁,其他任何上下文都无法获取共享锁(必须等待排他锁释放)。
    • 如果多个上下文持有共享锁,任何尝试获取排他锁的上下文都必须等待所有共享锁释放。
  • 用途:适用于读取共享资源的操作。允许多个“读者”同时访问资源,提高了并发性,同时又确保了在“写入者”存在时,所有“读者”都会被阻塞,以保证读取到最新且一致的数据。
  • 指定方式navigator.locks.request(name, { mode: 'shared', callback })

共享锁与排他锁的交互示例:

// 共享锁:读取数据
async function readSharedData() {
    const LOCK_NAME = 'shared_data_lock';
    const DATA_KEY = 'my_shared_data';

    console.log(`Tab ${window.name || 'Unknown'}: Attempting to read shared data.`);

    try {
        await navigator.locks.request(LOCK_NAME, { mode: 'shared' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired shared lock for data.`);

            const data = localStorage.getItem(DATA_KEY) || 'No data';
            await new Promise(resolve => setTimeout(resolve, 800)); // 模拟读取耗时
            console.log(`Tab ${window.name || 'Unknown'}: Read data: "${data}"`);
        });
        console.log(`Tab ${window.name || 'Unknown'}: Shared lock for data released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Failed to read data. Error:`, error);
    }
}

// 排他锁:写入数据
async function writeSharedData(newData) {
    const LOCK_NAME = 'shared_data_lock';
    const DATA_KEY = 'my_shared_data';

    console.log(`Tab ${window.name || 'Unknown'}: Attempting to write shared data: "${newData}".`);

    try {
        await navigator.locks.request(LOCK_NAME, { mode: 'exclusive' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired exclusive lock for data.`);

            await new Promise(resolve => setTimeout(resolve, 1500)); // 模拟写入耗时
            localStorage.setItem(DATA_KEY, newData);
            console.log(`Tab ${window.name || 'Unknown'}: Data written: "${newData}"`);
        });
        console.log(`Tab ${window.name || 'Unknown'}: Exclusive lock for data released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Failed to write data. Error:`, error);
    }
}

// 演示顺序:
// 1. Tab A: readSharedData() -> 立即获取共享锁并开始读取
// 2. Tab B: readSharedData() -> 立即获取共享锁并开始读取 (与 Tab A 并发)
// 3. Tab C: writeSharedData("New Value") -> 请求排他锁,等待 Tab A 和 Tab B 的共享锁释放
// 4. Tab D: readSharedData() -> 请求共享锁,等待 Tab C 的排他锁释放 (Tab C 正在等待 Tab A, B 释放)

// 实际操作:
// Tab A 执行 readSharedData()
// Tab B 执行 readSharedData()
// Tab C 执行 writeSharedData("Value 1")
// Tab D 执行 readSharedData()
// Tab E 执行 writeSharedData("Value 2")

锁模式兼容性矩阵:

理解不同锁模式之间的兼容性是使用 Web Locks API 的关键。

已持有锁模式 / 请求锁模式 exclusive 请求 shared 请求
无锁 允许 允许
exclusive 阻塞 阻塞
shared 阻塞 允许
  • 允许:请求者立即获取锁。
  • 阻塞:请求者进入等待队列,直到当前冲突的锁被释放。

从表格中可以看出:

  • 排他锁是“最强”的锁,它会阻塞所有其他同名锁的请求。
  • 共享锁是“最弱”的锁,它只与排他锁冲突。多个共享锁可以共存。
  • 当有共享锁存在时,排他锁请求会被阻塞,直到所有共享锁都被释放。

高级选项:ifAvailable, steal, signal

navigator.locks.request() 方法的 options 对象提供了几个高级选项,允许开发者更精细地控制锁的请求行为。

1. ifAvailable: 非阻塞锁请求

  • 用途:当你希望尝试获取锁,但如果锁已被占用,则不等待,而是立即执行备用逻辑或放弃操作时使用。
  • 行为:如果设置为 truerequest() 会尝试立即获取锁。
    • 如果锁可用,它会像往常一样获取锁并执行回调。Promise resolve 值为回调函数的结果。
    • 如果锁不可用,它不会等待,而是立即 resolve 为 undefined。回调函数不会被执行。
  • 示例:防止多个Tab页同时显示某个一次性的通知。
async function showOneTimeNotification(message) {
    const LOCK_NAME = 'one_time_notification_lock';

    console.log(`Tab ${window.name || 'Unknown'}: Attempting to show notification.`);

    try {
        const lockResult = await navigator.locks.request(LOCK_NAME, {
            mode: 'exclusive',
            ifAvailable: true, // 关键:如果锁不可用,不等待
            async callback(lock) {
                if (lock) { // 检查是否成功获取了锁
                    console.log(`Tab ${window.name || 'Unknown'}: Acquired lock, showing notification: "${message}"`);
                    // 模拟显示通知
                    await new Promise(resolve => setTimeout(resolve, 3000));
                    console.log(`Tab ${window.name || 'Unknown'}: Notification displayed.`);
                    return true; // 返回一个值,会成为 request() Promise 的 resolve 值
                } else {
                    // 这种情况不会发生,因为 ifAvailable 为 true 时,如果 lock 不可用,callback 不会执行。
                    // 但是为了类型安全和逻辑严谨,可以保留。
                    console.log(`Tab ${window.name || 'Unknown'}: Lock was not available (unexpected path).`);
                    return false;
                }
            }
        });

        if (lockResult === undefined) {
            console.log(`Tab ${window.name || 'Unknown'}: Notification skipped. Another tab is already showing it.`);
        } else if (lockResult === true) {
            console.log(`Tab ${window.name || 'Unknown'}: Notification process completed.`);
        } else {
            console.log(`Tab ${window.name || 'Unknown'}: Notification process finished without lock (unexpected result).`);
        }

    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Error showing notification:`, error);
    }
}

// 多个Tab页同时调用:
// showOneTimeNotification("Welcome to our app!");
// 只有一个Tab页会成功获取锁并显示通知。

2. steal: 强制获取锁

  • 用途:在特定情况下,你可能需要强制获取一个锁,即使它当前已被其他上下文持有。这通常用于恢复机制、优先级任务或当确定当前持有锁的上下文已经“卡住”时。
  • 行为:如果设置为 true,并且锁已被占用,request() 会尝试“窃取”该锁。
    • 如果成功窃取,当前持有锁的上下文的锁会被释放,然后当前请求者获取锁并执行回调。
    • 这会中断其他上下文的操作,因此必须极其谨慎地使用。
  • 示例:一个后台Service Worker需要强制更新缓存,即使一个Tab页正在使用排他锁。
// 注意:steal 选项非常强大,可能导致数据不一致或破坏其他上下文的操作。谨慎使用!
async function forcefulCacheUpdate(cacheVersion) {
    const LOCK_NAME = 'cache_update_lock';

    console.log(`Tab ${window.name || 'Unknown'}: Attempting to forcefully update cache to version ${cacheVersion}.`);

    try {
        await navigator.locks.request(LOCK_NAME, {
            mode: 'exclusive',
            steal: true, // 关键:强制获取锁
            async callback(lock) {
                console.log(`Tab ${window.name || 'Unknown'}: Acquired (possibly stolen) lock for cache update.`);
                // 模拟耗时的缓存更新操作
                await new Promise(resolve => setTimeout(resolve, 4000));
                console.log(`Tab ${window.name || 'Unknown'}: Cache updated to version ${cacheVersion}.`);
            }
        });
        console.log(`Tab ${window.name || 'Unknown'}: Cache update lock released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Error during forceful cache update:`, error);
    }
}

// 假设 Tab A 正在执行一个常规的长时间操作,持有 'cache_update_lock'
// await navigator.locks.request('cache_update_lock', async () => { /* long running task */ });

// Tab B (例如 Service Worker) 调用 forcefulCacheUpdate(2.0)
// Tab B 会中断 Tab A 的锁,并获取锁进行更新。Tab A 的回调会被中断。

3. signal: 取消锁请求

  • 用途:当你需要在锁请求还在等待队列中时,能够取消它。这对于用户交互(例如用户导航离开页面)或超时机制非常有用。
  • 行为signal 选项接受一个 AbortSignal 对象。
    • 如果 signal 在锁被获取之前触发 abort 事件,request() 返回的 Promise 会以 AbortError 拒绝。
    • 如果锁已经成功获取,signalabort 不会影响已获取的锁,锁仍会在回调完成后自动释放。
  • 示例:用户在等待锁时关闭Tab页或执行其他操作。
async function performCancellableTask() {
    const LOCK_NAME = 'cancellable_task_lock';
    const abortController = new AbortController();
    const signal = abortController.signal;

    console.log(`Tab ${window.name || 'Unknown'}: Attempting to acquire lock for cancellable task.`);

    // 模拟用户在等待一段时间后取消操作
    setTimeout(() => {
        if (!signal.aborted) { // 只有在未被取消时才尝试取消
            console.log(`Tab ${window.name || 'Unknown'}: User decided to cancel the task.`);
            abortController.abort();
        }
    }, 3000); // 3秒后尝试取消

    try {
        await navigator.locks.request(LOCK_NAME, {
            mode: 'exclusive',
            signal: signal, // 关键:关联 AbortSignal
            async callback(lock) {
                console.log(`Tab ${window.name || 'Unknown'}: Acquired lock for cancellable task.`);
                // 模拟耗时操作
                await new Promise(resolve => setTimeout(resolve, 5000));
                console.log(`Tab ${window.name || 'Unknown'}: Cancellable task completed.`);
            }
        });
        console.log(`Tab ${window.name || 'Unknown'}: Lock released for cancellable task.`);
    } catch (error) {
        if (error.name === 'AbortError') {
            console.log(`Tab ${window.name || 'Unknown'}: Lock request was aborted:`, error.message);
        } else {
            console.error(`Tab ${window.name || 'Unknown'}: Error during cancellable task:`, error);
        }
    }
}

// 假设 Tab A 正在执行一个长时间任务,持有 'cancellable_task_lock'
// Tab B 调用 performCancellableTask()
// Tab B 会等待锁,但如果在3秒内未获取到锁,它会取消请求,Promise 拒绝 AbortError。

实践应用场景

Web Locks API 在浏览器环境中有很多实用的场景。以下是一些常见且有代表性的例子。

1. 防止重复的 API 请求

这是最经典的用例之一。当用户在多个Tab页中打开同一个应用时,可能会导致重复的初始化数据请求,造成服务器不必要的负载。

问题:多个Tab页同时发起相同的 /api/init-data 请求。
解决方案:使用排他锁,确保只有一个Tab页能够发起并处理该请求。

const CACHE_KEY = 'app_init_data';
const LOCK_NAME = 'init_data_fetch_lock';

async function fetchAndCacheInitData() {
    // 优先从缓存读取
    const cachedData = localStorage.getItem(CACHE_KEY);
    if (cachedData) {
        console.log(`Tab ${window.name || 'Unknown'}: Data found in cache.`);
        return JSON.parse(cachedData);
    }

    console.log(`Tab ${window.name || 'Unknown'}: No cached data, attempting to fetch.`);

    try {
        const data = await navigator.locks.request(LOCK_NAME, { mode: 'exclusive' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired lock to fetch init data.`);

            // 再次检查缓存,因为在等待锁的过程中,其他Tab页可能已经获取并缓存了数据
            const dataDuringLock = localStorage.getItem(CACHE_KEY);
            if (dataDuringLock) {
                console.log(`Tab ${window.name || 'Unknown'}: Data cached by another tab while waiting for lock.`);
                return JSON.parse(dataDuringLock);
            }

            // 模拟 API 请求
            console.log(`Tab ${window.name || 'Unknown'}: Making API call to fetch initial data...`);
            const response = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 替换为你的API
            const result = await response.json();
            await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟和处理时间

            // 缓存数据
            localStorage.setItem(CACHE_KEY, JSON.stringify(result));
            console.log(`Tab ${window.name || 'Unknown'}: Initial data fetched and cached.`);
            return result;
        });

        console.log(`Tab ${window.name || 'Unknown'}: Initial data process completed.`);
        return data;

    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Error fetching initial data:`, error);
        throw error;
    }
}

// 多个Tab页同时调用
// fetchAndCacheInitData().then(data => console.log(data));

2. 同步 UI 状态更新

当一个应用的UI状态需要在所有打开的Tab页中保持一致时,Web Locks API 可以确保状态更新的原子性。

问题:用户在一个Tab页中更改了主题设置,希望其他Tab页也立即同步。
解决方案:使用排他锁来更新全局状态,并通过 BroadcastChannel 通知其他Tab页。

const THEME_KEY = 'app_theme';
const LOCK_NAME_THEME = 'theme_update_lock';
const themeChannel = new BroadcastChannel('theme_channel');

// 初始化时读取主题
function applyTheme(theme) {
    document.body.className = theme;
    console.log(`Tab ${window.name || 'Unknown'}: Applied theme: ${theme}`);
}

// 监听其他Tab页的主题更新通知
themeChannel.onmessage = (event) => {
    if (event.data.type === 'theme_updated') {
        applyTheme(event.data.theme);
    }
};

async function updateTheme(newTheme) {
    console.log(`Tab ${window.name || 'Unknown'}: Attempting to update theme to "${newTheme}".`);

    try {
        await navigator.locks.request(LOCK_NAME_THEME, { mode: 'exclusive' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired lock for theme update.`);

            // 模拟异步操作,例如保存到服务器或长时间计算
            await new Promise(resolve => setTimeout(resolve, 800));
            localStorage.setItem(THEME_KEY, newTheme);
            applyTheme(newTheme); // 当前Tab页立即更新

            // 通知其他Tab页更新主题
            themeChannel.postMessage({ type: 'theme_updated', theme: newTheme });
            console.log(`Tab ${window.name || 'Unknown'}: Theme updated and broadcasted.`);
        });
        console.log(`Tab ${window.name || 'Unknown'}: Theme update lock released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Error updating theme:`, error);
    }
}

// 假设用户点击按钮触发:
// updateTheme('dark');
// updateTheme('light');

// 初始加载时:
// applyTheme(localStorage.getItem(THEME_KEY) || 'light');

3. 管理客户端队列/任务分配

对于需要处理大量数据或后台任务的Web应用,可以将任务分发给不同的Tab页或Worker来处理,Web Locks API 可以确保任务被唯一处理。

问题:有一个待处理任务队列,但不希望多个Tab页处理同一个任务。
解决方案:使用排他锁,每个Tab页尝试获取锁来声明对下一个任务的独占处理权。

const TASK_QUEUE_KEY = 'pending_tasks';
const LOCK_NAME_TASK = 'task_processor_lock';

async function processNextTask() {
    console.log(`Tab ${window.name || 'Unknown'}: Attempting to process next task.`);

    try {
        const taskProcessed = await navigator.locks.request(LOCK_NAME_TASK, { mode: 'exclusive' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired lock for task processing.`);

            let tasks = JSON.parse(localStorage.getItem(TASK_QUEUE_KEY) || '[]');
            if (tasks.length === 0) {
                console.log(`Tab ${window.name || 'Unknown'}: No tasks in queue.`);
                return false;
            }

            const nextTask = tasks.shift(); // 取出第一个任务
            localStorage.setItem(TASK_QUEUE_KEY, JSON.stringify(tasks)); // 更新队列

            console.log(`Tab ${window.name || 'Unknown'}: Processing task:`, nextTask);
            await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟任务处理时间
            console.log(`Tab ${window.name || 'Unknown'}: Finished processing task:`, nextTask);
            return true;
        });

        if (taskProcessed) {
            console.log(`Tab ${window.name || 'Unknown'}: Task processing completed.`);
            // 可以再次尝试处理下一个任务,或者等待外部触发
            // setTimeout(processNextTask, 100);
        } else {
            console.log(`Tab ${window.name || 'Unknown'}: No task processed.`);
        }

    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Error processing task:`, error);
    }
}

// 初始化任务队列 (只在某个Tab页执行一次,或者在应用启动时)
function initializeTasks() {
    if (!localStorage.getItem(TASK_QUEUE_KEY)) {
        localStorage.setItem(TASK_QUEUE_KEY, JSON.stringify([
            { id: 1, type: 'report' },
            { id: 2, type: 'data_sync' },
            { id: 3, type: 'image_process' },
            { id: 4, type: 'notification' }
        ]));
        console.log("Task queue initialized.");
    }
}

// 多个Tab页同时调用:
// initializeTasks(); // 确保队列存在
// setInterval(processNextTask, 500); // 每个Tab页每500ms尝试处理一个任务

4. 资源限制(例如,摄像头/麦克风访问)

某些浏览器资源(如摄像头、麦克风)在同一时间只能被一个Tab页或应用使用。Web Locks API 可以用来协调这些资源的访问。

问题:多个Tab页同时尝试访问摄像头。
解决方案:使用排他锁确保只有一个Tab页能够请求并使用摄像头。

const CAMERA_LOCK_NAME = 'camera_access_lock';
let mediaStream = null;

async function requestCameraAccess() {
    console.log(`Tab ${window.name || 'Unknown'}: Attempting to get camera access.`);

    try {
        await navigator.locks.request(CAMERA_LOCK_NAME, { mode: 'exclusive' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired lock for camera access.`);

            try {
                const stream = await navigator.mediaDevices.getUserMedia({ video: true });
                mediaStream = stream;
                // 将视频流显示在页面上
                const videoElement = document.createElement('video');
                videoElement.srcObject = stream;
                videoElement.play();
                document.body.appendChild(videoElement);
                console.log(`Tab ${window.name || 'Unknown'}: Camera stream started.`);

                // 假设摄像头会使用一段时间,然后释放
                await new Promise(resolve => setTimeout(resolve, 10000)); // 保持10秒
                console.log(`Tab ${window.name || 'Unknown'}: Camera stream active for 10 seconds.`);

            } catch (mediaError) {
                console.error(`Tab ${window.name || 'Unknown'}: Failed to get media stream:`, mediaError);
            } finally {
                if (mediaStream) {
                    mediaStream.getTracks().forEach(track => track.stop());
                    mediaStream = null;
                    console.log(`Tab ${window.name || 'Unknown'}: Camera stream stopped and released.`);
                }
            }
        });
        console.log(`Tab ${window.name || 'Unknown'}: Camera access lock released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Failed to acquire camera lock or error during process:`, error);
    }
}

// 多个Tab页同时调用:
// requestCameraAccess();
// 只有一个Tab页会成功获取锁并使用摄像头。

5. 结合 IndexedDB 实现更健壮的数据同步

当需要对 IndexedDB 中的数据执行复杂的读写操作,并且这些操作需要跨Tab页保持原子性时,Web Locks API 是 IndexedDB 事务的有力补充。IndexedDB 事务保证了单个数据库操作的原子性,而 Web Locks API 则保证了跨多个数据库操作或跨多个Tab页的原子性。

问题:需要执行一个多步骤的数据迁移或复杂的报告生成,涉及 IndexedDB 多个对象的读写,且不能被其他Tab页中断。
解决方案:使用 Web Locks API 获取一个排他锁,然后在锁的回调中执行 IndexedDB 事务。

const DB_NAME = 'my_app_db';
const STORE_NAME_USERS = 'users';
const STORE_NAME_REPORTS = 'reports';
const LOCK_NAME_DATA_MIGRATION = 'data_migration_lock';

async function openDatabase() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, 1);

        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            if (!db.objectStoreNames.contains(STORE_NAME_USERS)) {
                db.createObjectStore(STORE_NAME_USERS, { keyPath: 'id' });
            }
            if (!db.objectStoreNames.contains(STORE_NAME_REPORTS)) {
                db.createObjectStore(STORE_NAME_REPORTS, { keyPath: 'id' });
            }
            console.log(`Tab ${window.name || 'Unknown'}: IndexedDB upgrade needed.`);
        };

        request.onsuccess = (event) => {
            console.log(`Tab ${window.name || 'Unknown'}: IndexedDB opened successfully.`);
            resolve(event.target.result);
        };

        request.onerror = (event) => {
            console.error(`Tab ${window.name || 'Unknown'}: IndexedDB open error:`, event.target.error);
            reject(event.target.error);
        };
    });
}

async function performComplexDataMigration() {
    console.log(`Tab ${window.name || 'Unknown'}: Attempting complex data migration.`);

    try {
        await navigator.locks.request(LOCK_NAME_DATA_MIGRATION, { mode: 'exclusive' }, async (lock) => {
            console.log(`Tab ${window.name || 'Unknown'}: Acquired lock for data migration.`);

            const db = await openDatabase();
            const transaction = db.transaction([STORE_NAME_USERS, STORE_NAME_REPORTS], 'readwrite');
            const userStore = transaction.objectStore(STORE_NAME_USERS);
            const reportStore = transaction.objectStore(STORE_NAME_REPORTS);

            // 步骤 1: 读取所有用户数据
            const users = await new Promise((resolve, reject) => {
                const req = userStore.getAll();
                req.onsuccess = () => resolve(req.result);
                req.onerror = () => reject(req.error);
            });
            console.log(`Tab ${window.name || 'Unknown'}: Read ${users.length} users.`);

            // 步骤 2: 生成报告(模拟一个耗时计算)
            await new Promise(resolve => setTimeout(resolve, 1500));
            const newReport = {
                id: `report-${Date.now()}`,
                generatedBy: window.name || 'Unknown Tab',
                userCount: users.length,
                timestamp: new Date().toISOString()
            };
            console.log(`Tab ${window.name || 'Unknown'}: Generated new report:`, newReport.id);

            // 步骤 3: 将报告写入 IndexedDB
            await new Promise((resolve, reject) => {
                const req = reportStore.add(newReport);
                req.onsuccess = () => resolve();
                req.onerror = () => reject(req.error);
            });

            // 提交 IndexedDB 事务
            await new Promise((resolve, reject) => {
                transaction.oncomplete = () => {
                    console.log(`Tab ${window.name || 'Unknown'}: IndexedDB transaction completed.`);
                    db.close();
                    resolve();
                };
                transaction.onerror = () => {
                    console.error(`Tab ${window.name || 'Unknown'}: IndexedDB transaction error:`, transaction.error);
                    db.close();
                    reject(transaction.error);
                };
            });

            console.log(`Tab ${window.name || 'Unknown'}: Data migration successful.`);
        });
        console.log(`Tab ${window.name || 'Unknown'}: Data migration lock released.`);
    } catch (error) {
        console.error(`Tab ${window.name || 'Unknown'}: Error during data migration:`, error);
    }
}

// 示例:初始化一些用户数据 (只执行一次)
async function initializeUsers() {
    const db = await openDatabase();
    const transaction = db.transaction([STORE_NAME_USERS], 'readwrite');
    const userStore = transaction.objectStore(STORE_NAME_USERS);

    const users = [
        { id: 'u1', name: 'Alice' },
        { id: 'u2', name: 'Bob' }
    ];

    for (const user of users) {
        await new Promise((resolve, reject) => {
            const req = userStore.add(user);
            req.onsuccess = () => resolve();
            req.onerror = (e) => {
                if (e.target.error.name === 'ConstraintError') {
                    console.log(`User ${user.id} already exists.`);
                    resolve(); // 已存在,不算错误
                } else {
                    reject(e.target.error);
                }
            };
        });
    }

    await new Promise(resolve => {
        transaction.oncomplete = () => { db.close(); resolve(); };
        transaction.onerror = () => { db.close(); resolve(); }; // 忽略错误
    });
    console.log(`Tab ${window.name || 'Unknown'}: Initial users added.`);
}

// 多个Tab页同时调用
// initializeUsers().then(() => performComplexDataMigration());

错误处理与最佳实践

1. 错误处理

  • AbortError:当 signal 选项的 AbortSignal 被触发,并且锁请求尚未成功获取时,navigator.locks.request() 返回的 Promise 会以 DOMExceptionAbortError 拒绝。务必捕获并处理此错误,以区分是正常的取消还是其他问题。
  • 其他错误:在回调函数内部执行的代码也可能抛出错误。由于回调函数是异步的,这些错误会通过 navigator.locks.request() 返回的 Promise 链传递。因此,try...catch 块应包裹整个 await navigator.locks.request(...) 调用。

2. 最佳实践

  • 选择合适的锁名称:锁名称应清晰、具体,反映其保护的资源。避免使用过于泛泛的名称,以免不小心锁定不相关的资源。例如,'user_profile_edit''data_lock' 更好。
  • 最小化锁的持有时间:只在真正需要互斥访问共享资源的代码块中持有锁。一旦完成受保护的操作,就应该尽快释放锁。Web Locks API 的回调函数机制已经自动处理了释放,但这意味着你的回调函数本身不应该包含不必要的耗时操作。
  • 避免死锁:虽然 Web Locks API 自动释放机制极大地减少了传统死锁的风险(例如,一个持有锁的上下文崩溃),但在复杂场景中,如果一个上下文需要同时获取多个锁,并且以不同的顺序获取,仍然可能发生死锁。
    • 统一锁顺序:如果需要获取多个锁,所有上下文都应以相同的固定顺序请求这些锁。
    • 考虑超时:对于需要长时间等待的锁,可以结合 signalsetTimeout 来实现超时机制。
  • 优雅降级:尽管浏览器支持度很高,但对于不支持 Web Locks API 的老旧浏览器,应该提供备用方案或禁用相关功能。可以通过检查 navigator.locks 是否存在来判断。
// 优雅降级示例
if ('locks' in navigator) {
    // 使用 Web Locks API
    navigator.locks.request(...).then(...).catch(...);
} else {
    // 提供备用方案,例如:
    // 使用 localStorage 模拟 (但要清楚其局限性)
    // 或者直接不执行需要同步的操作
    console.warn("Web Locks API not supported. Falling back to less robust synchronization or skipping feature.");
    // fallbackToLocalStorageLock();
}

Web Locks API 与 Web Worker

Web Locks API 的一个强大之处在于它不仅适用于Tab页,也完美支持Web Worker(包括Dedicated Worker、Shared Worker和Service Worker)。这意味着您可以将耗时的同步任务卸载到Worker线程,而主线程保持响应。

Worker线程同样可以通过 navigator.locks.request() 来请求和释放锁。

// main.js (主线程)
const worker = new Worker('worker.js');

async function mainThreadTask() {
    console.log(`Main Thread: Attempting to acquire lock from main thread.`);
    try {
        await navigator.locks.request('worker_shared_resource', { mode: 'exclusive' }, async (lock) => {
            console.log(`Main Thread: Acquired lock. Performing main thread task.`);
            await new Promise(resolve => setTimeout(resolve, 3000));
            console.log(`Main Thread: Main thread task completed.`);
        });
        console.log(`Main Thread: Lock released by main thread.`);
    } catch (error) {
        console.error(`Main Thread: Error with lock:`, error);
    }
}

// 模拟主线程和Worker并发请求锁
mainThreadTask();
worker.postMessage('start_worker_task');
// worker.js (Web Worker)
self.onmessage = async (event) => {
    if (event.data === 'start_worker_task') {
        console.log(`Worker: Attempting to acquire lock from worker.`);
        try {
            await navigator.locks.request('worker_shared_resource', { mode: 'exclusive' }, async (lock) => {
                console.log(`Worker: Acquired lock. Performing worker task.`);
                await new Promise(resolve => setTimeout(resolve, 2000));
                console.log(`Worker: Worker task completed.`);
            });
            console.log(`Worker: Lock released by worker.`);
        } catch (error) {
            console.error(`Worker: Error with lock:`, error);
        }
    }
};

运行结果预期:
无论是主线程还是Worker,都会排队等待获取 worker_shared_resource 锁。哪个先获取到,哪个就先执行其受保护的代码块。这确保了跨线程的互斥访问。

与其他浏览器同步机制的比较

Web Locks API 并不是浏览器中唯一的同步机制。理解它与其他机制的异同,有助于在不同场景下选择最合适的工具。

特性/API Web Locks API BroadcastChannel SharedWorker IndexedDB (作为锁) Atomics / SharedArrayBuffer
主要用途 跨浏览上下文的互斥锁(Mutex) 跨浏览上下文的消息传递 跨浏览上下文的共享状态和集中协调 事务性数据存储,可模拟锁 低级内存同步(Worker内部)
互斥保证 强互斥(排他/共享),原生支持等待队列 无互斥保证,仅消息通知 间接实现(通过共享Worker内部逻辑) 事务原子性,但无等待队列 内存级别原子操作,非高级互斥锁
适用范围 Tab页、iframe、所有Worker、Service Worker Tab页、iframe、所有Worker、Service Worker Tab页、iframe、Dedicated Worker(非Service Worker) Tab页、iframe、所有Worker、Service Worker 仅支持 SharedArrayBuffer 的Worker
易用性 ,API设计简洁,自动释放 中,需要手动管理状态和逻辑 中,需要设计Worker内部协调逻辑 低,实现复杂,无等待队列 低,非常底层,易出错
性能开销 低,浏览器原生实现,高效的等待队列 低,基于事件的消息传递 中,额外的Worker线程开销 中,涉及磁盘I/O和事务管理 高,低级操作,但非常高效(如果使用得当)
死锁风险 (自动释放),但多锁场景仍需注意 无死锁概念 需Worker内部逻辑谨慎处理 需手动过期/清理机制 需谨慎设计,否则易死锁
自动释放 ,回调完成或上下文关闭即释放 否,需Worker内部逻辑管理 否,需手动清理
非阻塞请求 (ifAvailable) 否(消息是异步的) 间接实现 间接实现(通过 add 失败)
取消等待 (signal) 间接实现

总结:

  • Web Locks API 是实现跨浏览上下文互斥锁的最直接、最健壮、最推荐的方式。
  • BroadcastChannel 用于一对多或多对多的消息通信,而非互斥访问。
  • SharedWorker 适用于需要一个单一、集中的后台进程来管理共享状态或执行任务的场景,可以在其内部结合Web Locks API。
  • IndexedDB 主要是持久化存储,虽然其事务特性可用于模拟锁,但远不如 Web Locks API 方便和强大。
  • AtomicsSharedArrayBuffer 提供了非常低级的内存共享和原子操作,主要用于高性能的Worker间数据同步,但其API复杂且仅限于 SharedArrayBuffer。对于高级的业务逻辑互斥,Web Locks API 是更好的选择。

Web Locks API 的强大与意义

Web Locks API 填补了Web平台在并发控制方面的一个长期空白。它提供了一个原生的、高效率的、易于使用的互斥锁机制,极大地简化了跨Tab页、iframe 和 Web Worker 的资源协调。

通过 Web Locks API,开发者可以:

  • 提高数据一致性:确保在任何给定时间只有一个上下文修改共享数据。
  • 避免重复操作:防止不必要的网络请求、计算或资源访问。
  • 简化并发逻辑:抽象了底层复杂的同步机制,让开发者专注于业务逻辑。
  • 增强用户体验:通过避免竞态条件和保证一致性,提升应用的稳定性和可靠性。

随着现代Web应用日益复杂,多Tab页、多Worker并行工作的场景越来越普遍,Web Locks API 将成为构建健壮、高性能Web应用不可或缺的工具。掌握并善用它,将使您的Web应用在并发环境下更加游刃有余。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注