各位同仁,各位开发者,大家好!
在现代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页)尝试同时修改同一个数据或执行同一个操作时,就会出现问题:
- 竞态条件(Race Conditions):操作的最终结果取决于执行的精确时序。
- 数据不一致(Data Inconsistency):一个上下文的修改被另一个上下文意外覆盖,或者读取到过期的数据。
- 重复操作(Duplicate Operations):例如,多个Tab页同时向服务器发送相同的初始化请求,造成不必要的负载或错误。
过去,开发者们尝试了多种方法来模拟互斥锁的行为:
1. 基于 localStorage 的简易锁
最常见也最直观的方法是利用 localStorage。localStorage 是跨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 对象,其中包含锁的 name 和 mode 信息。 |
浏览器支持:
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() 方法。它允许您请求一个带有特定名称和模式的锁,并在成功获取锁后执行一段代码。
该方法接收两个主要参数:
name: 锁的名称,一个字符串。所有请求相同名称锁的上下文都会相互协调。options或callback:- 如果只传递一个函数,它会被作为回调函数在获取锁后执行。
- 如果传递一个对象,它是一个
LockOptions对象,可以包含mode、ifAvailable、steal、signal等选项,以及一个callback函数。
navigator.locks.request() 返回一个 Promise。当锁被成功获取并且回调函数执行完毕后(或者回调函数返回的 Promise settled),这个 Promise 会 resolve。如果锁请求失败(例如,ifAvailable 为 true 但锁不可用),或者被取消,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: 非阻塞锁请求
- 用途:当你希望尝试获取锁,但如果锁已被占用,则不等待,而是立即执行备用逻辑或放弃操作时使用。
- 行为:如果设置为
true,request()会尝试立即获取锁。- 如果锁可用,它会像往常一样获取锁并执行回调。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拒绝。 - 如果锁已经成功获取,
signal的abort不会影响已获取的锁,锁仍会在回调完成后自动释放。
- 如果
- 示例:用户在等待锁时关闭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 会以DOMException的AbortError拒绝。务必捕获并处理此错误,以区分是正常的取消还是其他问题。- 其他错误:在回调函数内部执行的代码也可能抛出错误。由于回调函数是异步的,这些错误会通过
navigator.locks.request()返回的 Promise 链传递。因此,try...catch块应包裹整个await navigator.locks.request(...)调用。
2. 最佳实践
- 选择合适的锁名称:锁名称应清晰、具体,反映其保护的资源。避免使用过于泛泛的名称,以免不小心锁定不相关的资源。例如,
'user_profile_edit'比'data_lock'更好。 - 最小化锁的持有时间:只在真正需要互斥访问共享资源的代码块中持有锁。一旦完成受保护的操作,就应该尽快释放锁。Web Locks API 的回调函数机制已经自动处理了释放,但这意味着你的回调函数本身不应该包含不必要的耗时操作。
- 避免死锁:虽然 Web Locks API 自动释放机制极大地减少了传统死锁的风险(例如,一个持有锁的上下文崩溃),但在复杂场景中,如果一个上下文需要同时获取多个锁,并且以不同的顺序获取,仍然可能发生死锁。
- 统一锁顺序:如果需要获取多个锁,所有上下文都应以相同的固定顺序请求这些锁。
- 考虑超时:对于需要长时间等待的锁,可以结合
signal和setTimeout来实现超时机制。
- 优雅降级:尽管浏览器支持度很高,但对于不支持 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 方便和强大。Atomics和SharedArrayBuffer提供了非常低级的内存共享和原子操作,主要用于高性能的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应用在并发环境下更加游刃有余。