【问题标题】:Do I need to be concerned with race conditions with asynchronous Javascript?我需要关注异步 Javascript 的竞争条件吗?
【发布时间】:2011-11-06 12:06:36
【问题描述】:

假设我加载了一些我知道将来某个时间点会调用 window.flashReady 并设置为 window.flashReadyTriggered = true 的 Flash 电影。

现在我有一段代码,我想在 Flash 准备好时执行。如果window.flashReady 已经被调用,我希望它立即执行它,如果它还没有被调用,我想把它作为window.flashReady 中的回调。天真的方法是这样的:

if(window.flashReadyTriggered) {
  block();
} else {
  window.flashReady = block;
}

所以我基于此的担心是if条件中的表达式被评估为false,但是在block()可以执行之前,window.flashReady被外部Flash触发。因此,block 永远不会被调用。

是否有更好的设计模式来实现我所追求的更高层次的目标(例如,手动调用 flashReady 回调)?如果没有,我安全吗,还是我应该做其他事情?

【问题讨论】:

  • 为什么不让flash调用javascript函数呢?

标签: javascript


【解决方案1】:

所有 Javascript 事件处理程序脚本均由一个主事件队列系统处理。这意味着事件处理程序一次运行一个,一个运行直到完成,然后下一个准备好开始运行。因此,在 Javascript 中没有任何典型的竞争条件可以在多线程语言中看到,其中语言的多个线程可以同时运行(或时间切片)并为访问变量创建实时冲突。

javascript 中的任何单个执行线程都将在下一个线程开始之前运行完成。这就是 Javascript 的工作原理。从事件队列中拉出一个事件,然后代码开始运行以处理该事件。该代码自行运行,直到将控制权返回给系统,然后系统将从事件队列中提取下一个事件并运行该代码,直到将控制权返回给系统。

因此,由同时执行的两个线程引起的典型竞争条件在 Javascript 中不会发生。

这包括所有形式的 Javascript 事件,包括:用户事件(鼠标、键等)、计时器事件、网络事件(ajax 回调)等...

您可以在 Javascript 中实际执行多线程的唯一地方是使用 HTML5 Web WorkersWorker Threads(在 node.js 中),但它们与常规 javascript 非常隔离(它们只能通过消息与常规 javascript 通信传递) 并且根本无法操作 DOM 并且必须有自己的脚本和命名空间等...


虽然从技术上讲,我不会将此称为竞争条件,但在 Javascript 中存在一些情况,因为它的一些异步操作可能会同时进行两个或多个异步操作(实际上不是执行 Javascript,而是底层的异步操作同时运行本机代码)并且每个操作相对于其他操作何时完成可能是不可预测的。这会产生时间的不确定性(如果操作的相对时间对您的代码很重要)会产生您必须手动编码的东西。您可能需要对操作进行排序,以便运行一个操作,然后在开始下一个操作之前等待它完成。或者,您可以启动所有三个操作,然后编写一些代码来收集所有三个结果,当它们都准备好时,您的代码就会继续。

在现代 Javascript 中,promise 通常用于管理这些类型的异步操作。

因此,如果您有三个异步操作,每个操作都返回一个 Promise(例如从数据库读取、从另一台服务器获取请求等),您可以手动排序,然后像这样:

a().then(b).then(c).then(result => {
    // result here
}).catch(err => {
    // error here
});

或者,如果您希望它们一起运行(同时在飞行中)并且只知道它们何时完成,您可以这样做:

Promise.all([a(), b(), c()])..then(results => {
    // results here
}).catch(err => {
    // error here
});

虽然我不会将这些竞争条件称为竞争条件,但它们属于设计代码以控制不确定顺序的一般系列。


在浏览器的某些情况下可能会出现一种特殊情况。这并不是真正的竞争条件,但如果您使用大量具有临时状态的全局变量,则可能需要注意。当您自己的代码导致另一个事件发生时,浏览器有时会同步调用该事件处理程序,而不是等到当前执行线程完成。一个例子是:

  1. 点击
  2. click 事件处理程序将焦点更改为另一个字段
  3. 其他字段有 onfocus 的事件处理程序
  4. 浏览器立即调用 onfocus 事件处理程序
  5. onfocus 事件处理程序运行
  6. 单击事件处理程序的其余部分运行(在 .focus() 调用之后)

这在技术上不是竞争条件,因为它 100% 知道 onfocus 事件处理程序何时执行(在 .focus() 调用期间)。但是,它可能会造成一个事件处理程序运行而另一个事件处理程序正在执行的情况。

【讨论】:

  • “任何单独的执行线程”
  • 您需要定义什么是竞态条件。竞争条件与线程无关。
  • @Vanuan - 因为在任何给定时间只有一段 Javascript 正在运行,所以不会出现典型的线程竞争条件,即两个线程尝试访问相同的变量或时间当一个线程访问它时,另一个线程是完全不可预测的。由于一次只运行一个 Javascript,所以这种竞争条件根本不会发生。如果您想询问不同类型的比赛条件,请解释您在问什么,因为这个问题中显然没有其他人在问这个问题。
  • @Vanuan - 这是one definition of a race condition(显然要注意软件中有关竞争条件的部分)。而且,还有What is a Race Condition
  • @Vanuan - 另外,你为什么要从 6 年前开始挖掘这个?
【解决方案2】:

JavaScript 是单线程的。没有竞争条件。

当在当前“指令指针”处没有更多代码要执行时,“线程”“传递接力棒”,排队的window.setTimeout 或事件处理程序可能会执行其代码。

阅读node.js的设计思路,你会更好地理解Javascript的单线程方法。

进一步阅读: Why doesn't JavaScript support multithreading?

【讨论】:

  • 这个答案不是完整的故事。请在下面阅读我的回答:stackoverflow.com/a/12799287/1040124
  • @Jens,传入事件不会抢占当前代码,但你是对的,接下来会触发回调。
  • this race condition 怎么样?
  • @DanDascalescu,标准规定首先评估增强分配的 LHS,然后是 RHS。因为 RHS 包含 await 表达式,所以当前“线程”产生。这可以称为数据竞赛,但我宁愿称其为对标准的误解。只需使用一个临时变量,一切都会正常工作。
  • @Ben,不是。 1) 单个工作者一次只执行表达式。 2)没有非自愿让步。 3)您不能修改其他工人的数据。 → 所以没有数据竞争。
【解决方案3】:

重要的是要注意,如果您这样做,您仍然可能会遇到竞争条件。使用多个异步 XMLHttpRequest。未定义返回响应的顺序(即响应可能不会以发送时的相同顺序返回)。这里的输出取决于其他不可控事件(服务器延迟等)的顺序或时间。这是简而言之的竞争条件

因此,即使使用单个事件队列(如在 JavaScript 中)也不能阻止事件以无法控制的顺序出现,您的代码应该处理好这一点。

【讨论】:

  • 这不是竞争条件,它只是异步代码的工作方式。如果你给鲍勃一个盒子并说“当你打开这个盒子时,把它还给我”,然后立即转向苏并告诉她同样的事情,你先从谁那里取回盒子?这取决于他们打开盒子需要多长时间,并且不在您的控制范围内。同样,AJAX 请求的工作方式相同;你不能假设它们会以任何特定的顺序完成,因为你没有订购它们,你只是说“当请求完成时,运行这个回调函数”。
  • 线程不一定会出现竞争条件,它只是一种表示您有 2 个或更多异步函数竞争相同资源并且您不知道谁会获胜以及最终结果如何的一种方式执行状态是。这就是为什么@Jens 描述的是正确的
【解决方案4】:

当然需要。它一直在发生:

<button onClick=function() {
  const el = document.getElementById("view");
  fetch('/some/api').then((data) => {
    el.innerHTML = JSON.stringify(data);
  })
}>Button 1</button>

<button onClick=function() {
  const el = document.getElementById("view");
  fetch('/some/other/api').then((data) => {
    el.innerHTML = JSON.stringify(data);
  })

}>Button 2</button>

有些人不认为这是一种竞争条件。

但确实如此。

竞争条件被广泛定义为“电子、软件或其他系统的行为,其中输出取决于其他不可控事件的顺序或时间”。

如果用户在短时间内单击这 2 个按钮,则不能保证输出取决于单击的顺序。这取决于哪个 api 请求将更快得到解决。此外,您引用的 DOM 元素可能会被其他一些事件(如更改路线)删除。

您可以通过禁用按钮或在正在进行加载操作时显示一些微调器来缓解这种竞争状况,但这是作弊。您应该在代码级别使用一些互斥量/计数器/信号量来控制异步流程。

要使其适应您的问题,这取决于“block()”是什么。如果是同步函数,则无需担心。但如果是异步的,你就得担心了:

  function block() {
    window.blockInProgress = true;
    // some asynchronous code
    return new Promise(/* window.blockInProgress = false */);
  }

  if(!window.blockInProgress) {
    block();
  } else {
    window.flashReady = block;
  }

此代码对您想要防止块被多次调用是有意义的。但是,如果您不在乎,或者“块”是同步的,则不必担心。如果您担心全局变量值在检查时会发生变化,您不必担心,除非您调用某个异步函数,否则它保证不会发生变化。

一个更实际的例子。假设我们想要缓存 AJAX 请求。

 fetchCached(params) {
   if(!dataInCache()) {
     return fetch(params).then(data => putToCache(data));
   } else {
     return getFromCache();
   }
 }

如果我们多次调用这段代码会发生什么?我们不知道哪些数据会先返回,所以我们也不知道哪些数据会被缓存。前 2 次它会返回新数据,但第 3 次我们不知道要返回的响应的形状。

【讨论】:

  • 我请 A 将一支红笔放在我的桌子上,替换任何可能已经存在的笔。我让 B 人在我的桌子上放一支蓝笔,以代替可能已经存在的笔。两个人都必须先回到办公桌前拿起笔。两个人回来后,我的桌子上会放什么颜色的笔?显而易见的答案是“谁最后回来”,但这只能在事后确定(或在编程术语中,在运行时)。这与创建两个按钮并尝试确定首先单击哪个按钮没有什么不同;你不能控制,用户可以。
  • 不存在竞争条件,因为您无法控制哪支笔最终会放在您的桌子上。这源于一个不正确的前提:你可以保证两个人完成你交给他们的任务的速度。两个人都按照自己的速度完成了任务,彼此独立。同样,如果您发起两个独立运行和独立解析的网络请求,但修改了一个资源,则这不是竞争条件。您假设请求 A 将在请求 B 之前完成,仅仅是因为您首先启动了请求 A。
  • 如果这仍然令人困惑,请这样想:竞争条件是在给定相同的两个输入的情况下输出出乎意料的情况。相反,这一定意味着有一个未满足的期望。如果两个网络请求修改同一个 div 的内容,期望是什么?您是否期望请求 B 的内容会在 div 中,因为您将其称为第二个?如果答案是“我不知道,因为我不知道网络请求何时完成”,那么没有意外的输出,因为你没有最初的期望。
  • 这与打算设置a = 4没有什么不同; a = 3,但我将其设置为 a = 3; a = 4,并将其称为“竞争条件”,因为“事件不会按照程序员预期的顺序发生”。至于期望,这也是错误的。我无法控制任何一个人的行为,因此期望一个人会先于另一个人回到我身边在逻辑上是没有意义的。我只能在两者都回到我身边后做一些事情,例如,选择两者中的一个,或者因为我已经有了第一支铅笔而拒绝第二支铅笔。
  • 您的意思是,将两个人同时送出去却不知道他们何时回来的行为是一种竞争条件。这在逻辑上是毫无意义的。你还说,因为你不知道你的桌子上最后会出现什么,这也是一种竞争条件,但这忽略了这样一个事实,即你知道什么时候有人回来,而你可以对这个事实采取行动(又名回调函数)。如果我告诉两个人给我一支笔,我可以在我手里拿着笔后决定我想做什么。
【解决方案5】:

是的,Javascript 中当然存在竞争条件。它基于事件循环模型,因此展示了异步计算的竞争条件。以下程序将记录1016,具体取决于是先完成incHead 还是sqrHead

const rand = () => Math.round(Math.random() * 100);

const incHead = xs => new Promise((res, rej) =>
  setTimeout(ys => {
    ys[0] = ys[0] + 1;
    res(ys);
  }, rand(), xs));

const sqrHead = xs => new Promise((res, rej) =>
  setTimeout(ys => {
    ys[0] = ys[0] * ys[0];
    res(ys);
  }, rand(), xs))

const state = [3];

const foo = incHead(state);

const bar = sqrHead(state);

Promise.all([foo, bar])
  .then(_ => console.log(state));

【讨论】:

    猜你喜欢
    • 2010-11-27
    • 2020-07-12
    • 2021-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-02
    • 2020-11-22
    相关资源
    最近更新 更多