javascript - 使用异步数据存储 API 的 JavaScript 事件处理程序导致竞争条件
问题描述
每次触发某些浏览器事件时(例如,当浏览器选项卡关闭时),我都需要更新一些数据:
chrome.tabs.onRemoved.addListener(async (tabId) => {
let data = await getData(); // async operation
... // modify data
await setData(data); // async operation
});
问题是,当多个此类事件触发器快速连续时,异步getData()
可能会在事件处理程序的后续调用中返回陈旧的结果,然后setData()
才有机会在较早的事件中完成,从而导致结果不一致。
如果事件处理程序可以同步执行,则不会发生此问题,但getData()
两者setData()
都是异步操作。
这是比赛条件吗?处理此类逻辑的推荐模式是什么?
- - 更新 - -
为了提供更多上下文,getData()
并且setData()
只是一些 Chrome 存储 API 的承诺版本:
async function getData() {
return new Promise(resolve => {
chrome.storage.local.get(key, function(data) => {
// callback
});
});
}
async function setData() {
return new Promise(resolve => {
chrome.storage.local.set({ key: value }, function() => {
// callback
});
});
}
出于可读性目的,我将 API 调用包装在 Promise 中,但我认为无论哪种方式都是异步操作?
解决方案
getData()
对于具有异步 API 的数据存储,您有一个相当经典的竞争条件,如果您在数据处理中使用异步操作(在和之间),竞争条件会更糟setData()
。异步操作允许另一个事件在中间运行你的处理,破坏了你的事件序列的原子性。
以下是如何将传入的 tabId 放入队列并确保您一次只处理其中一个事件的想法:
const queue = [];
chrome.tabs.onRemoved.addListener(async (newTabId) => {
queue.push(newTabId);
if (queue.length > 1) {
// already in the middle of processing one of these events
// just leave the id in the queue, it will get processed later
return;
}
async function run() {
// we will only ever have one of these "in-flight" at the same time
try {
let tabId = queue[0];
let data = await getData(); // async operation
... // modify data
await setData(data); // async operation
} finally {
queue.shift(); // remove this one from the queue
}
}
while (queue.length) {
try {
await run();
} catch(e) {
console.log(e);
// decide what to do if you get an error
}
}
});
这可以变得更通用,因此可以在多个地方(每个都有自己的队列)重复使用,如下所示:
function enqueue(fn) {
const queue = [];
return async function(...args) {
queue.push(args); // add to end of queue
if (queue.length > 1) {
// already processing an item in the queue,
// leave this new one for later
return;
}
async function run() {
try {
const nextArgs = queue[0]; // get oldest item from the queue
await fn(...nextArgs); // process this queued item
} finally {
queue.shift(); // remove the one we just processed from the queue
}
}
// process all items in the queue
while (queue.length) {
try {
await run();
} catch(e) {
console.log(e);
// decide what to do if you get an error
}
}
}
}
chrome.tabs.onRemoved.addListener(enqueue(async function(tabId) {
let data = await getData(); // async operation
... // modify data
await setData(data); // async operation
}));
推荐阅读
- reactjs - React Relay Uncaught RelayNetwork:没有返回数据进行操作
- c# - 如何使用 EntityFramework 有效地计算表中存在的项目数?
- python - 如何在 tkinter 的 OptionMenu 中创建边框
- javascript - 屏幕共享浏览器(安卓 <-> PC)
- node.js - 使用 Nodejs / Express 路由路径的问题
- c# - 如何从 Api 填充列表中的服务器列表并在 WPF .Net Core 3.1 的 ListView 中显示
- django - 持久保存多个客户端数据的安全方法
- vim - 添加新的 .vimrc 文件时,vim 会覆盖默认设置
- python - 线性回归损失增加
- ios - Flutter - iOS 的电话验证验证 ID 有时会变为空