我是否需要关注异步 Javascript 的竞争条件?

IT技术 javascript
2021-02-16 16:27:08

假设我加载了一些 Flash 电影,我知道在未来的某个时候会调用window.flashReady并设置window.flashReadyTriggered = true.

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

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

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

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

5个回答

所有 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. 单击事件处理程序将焦点更改为另一个字段
  3. 其他字段具有用于 onfocus 的事件处理程序
  4. 浏览器立即调用 onfocus 事件处理程序
  5. onfocus 事件处理程序运行
  6. 点击事件处理程序的其余部分运行(在 .focus() 调用之后)

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

@Vanuan - 另外,你为什么要从 6 年前开始挖掘这个?
2021-04-18 16:27:08
您需要定义什么是竞争条件。竞争条件与线程无关。
2021-04-30 16:27:08
@Vanuan - 这是竞争条件一个定义(显然要注意软件中有关竞争条件的部分)。而且,还有什么是竞争条件
2021-05-09 16:27:08
@Vanuan - 因为在任何给定时间只有一段 Javascript 运行,所以没有任何典型的线程竞争条件,其中两个线程尝试访问相同的变量,或者一个线程访问它的时间与另一个线程相比。完全不可预测。由于一次只运行一段 Javascript,因此这种竞争条件根本不会发生。如果您想询问不同类型的竞态条件,请解释您所询问的内容,因为这个问题中的其他人显然没有问过这个问题。
2021-05-10 16:27:08
“任何单独的执行线程”<--- 那是什么?它是单个事件循环迭代吗?或者它是单个事件处理程序执行?
2021-05-12 16:27:08

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

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

阅读node.js的设计思路,您将对 Javascript 的单线程方法有更好的理解

进一步阅读: 为什么 JavaScript 不支持多线程?

@Ben,不是。1) 一个工人一次只执行一个表达式。2) 不存在非自愿屈服。3) 您不能修改其他工人的数据。→ 所以没有数据竞争。
2021-04-24 16:27:08
怎么样这个竞争条件
2021-04-25 16:27:08
这个答案不是完整的故事。请阅读下面我的回答:stackoverflow.com/a/12799287/1040124
2021-04-28 16:27:08
@Jens,传入事件不会抢占当前代码,但是您是对的,接下来会触发回调的竞争。
2021-05-04 16:27:08
@DanDascalescu,标准说首先评估增强分配的 LHS,然后是 RHS。因为 RHS 包含一个await表达式,当前的“线程”产生了。这可以称为数据竞赛,但我更愿意称其为对标准的误解。只需使用一个临时变量,一切都会正常工作。
2021-05-15 16:27:08

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

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

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

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

<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 次它将返回新数据,但第三次我们不知道要返回的响应的形状。

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

是的,当然 Javascript 中存在竞争条件。它基于事件循环模型,因此表现出异步计算的竞争条件。下面的程序将或者登录1016取决于是否incHeadsqrHead先完成:

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));