【问题标题】:JavaScript event handler working with an async data store API causing race condition使用异步数据存储 API 的 JavaScript 事件处理程序导致竞争条件
【发布时间】:2020-07-12 11:32:10
【问题描述】:

每次触发某些浏览器事件时(例如,当浏览器选项卡关闭时),我都需要更新一些数据:

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 中,但我认为无论哪种方式都是异步操作?

【问题讨论】:

  • 是的,这是一个经典的比赛条件。使用信号量或队列。
  • 请告诉我们getDatasetData 究竟做了什么。也许有一个特定于该 API 的解决方案 - 例如数据库中的事务。
  • 谢谢@Bergi,我在上面做了更新。
  • @winniethemu 谢谢。那就看看Best way to prevent race condition in multiple chrome.storage API calls?吧(虽然我不确定有没有好的解决方案)
  • 将回调放到队列中,这并不复杂,只需观察一个数组然后触发推送函数。在我的测试中,它异步但同步处理.. 见 sn-p:playcode.io/634360 我赞成但不会回答,欢迎您

标签: javascript asynchronous events promise


【解决方案1】:

对于具有异步 API 的数据存储,您有一个相当经典的竞争条件,如果您在数据处理中使用异步操作(在 getData()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
}));

【讨论】:

  • "您可能还可以使用较新的原子来管理访问。" - 不,这是针对共享内存(数组缓冲区)。只要您的代码都在同一个领域/事件循环中,全局同步对象(如您的queue)就是要走的路。
  • @Bergi - 好的,我删除了那行。
  • 这是一个干净的解决方案。谢谢!
【解决方案2】:

JS ascync/await 并没有真正使 JS 代码同步。

你要做的是使用 Promisse.all 阻止 getData 上的事件循环。

所以,

chrome.tabs.onRemoved.addListener(async (tabId) => {
   ...                         // turns in a composition 

  await setData(Promise.all([getData])[0]); // async composition
});

您在事件循环上使用一个块进行异步组合,当事件被触发时,VM 将有一个包含事件的列表,以及一个等待 getData 上的块。

实际上不存在异步组合,只是VM阻止事件循环并等待操作结果的技巧,导致VM将其作为列表处理并且列表不等待。

在使用合成时,请注意您的代码以变得可读。

【讨论】:

  • 这不是竞争条件” - 不。 “你可以阻止事件循环” - 不。
  • 解释负 1 标志,不是竞争条件,因为。它不是并行代码,只是内部代码。如果您考虑到这一点,异步函数组合就是一种响应。
  • 竞争条件也可能出现在正常的并发代码中。你不需要多线程或硬件并行。
  • 谢谢。但这是在队列中处理的,添加事件循环就可以了。所以,是的,是一个竞争条件。谢谢。
  • 任何时候你有await.then(),在你等待这些承诺得到解决时,其他事件处理程序就有机会运行。这允许这些事件中的另一个开始并导致竞争条件。这是 Javascript 中由异步操作引起的经典竞态条件。您无法像尝试那样绕过它。那不会有帮助的。此外,await 不会阻塞事件循环。它只会阻止当前函数的执行。事件循环继续运行其他事件。
猜你喜欢
  • 2020-07-12
  • 2012-01-26
  • 2010-11-27
  • 2014-09-23
  • 2021-06-04
  • 2014-02-23
  • 1970-01-01
  • 2023-03-11
  • 2022-01-05
相关资源
最近更新 更多