ChatGPT解决这个技术问题 Extra ChatGPT

JavaScript 是否保证是单线程的?

众所周知,JavaScript 在所有现代浏览器实现中都是单线程的,但这是在任何标准中指定的还是只是传统上的?假设 JavaScript 始终是单线程的是否完全安全?

在浏览器的上下文中,可能。但是有些程序允许您将 JS 视为顶级语言并为其他 C++ 库提供绑定。例如,flusspferd (C++ bindings for JS - AWESOME BTW) 正在用多线程 JS 做一些事情。这取决于上下文。
@RickyA 你已经创建了一个循环引用!那篇文章链接到这个问题,该问题链接到……

f
falinsky

这是个好问题。我很想说“是”。我不能。

JavaScript 通常被认为具有对脚本(*)可见的单个执行线程,因此当您输入内联脚本、事件侦听器或超时时,您可以完全控制,直到您从块或函数的末尾返回。

(*:忽略浏览器是否真的使用一个 OS 线程实现其 JS 引擎,或者 WebWorkers 是否引入了其他有限的执行线程的问题。)

然而,实际上这并不完全正确,以一种偷偷摸摸的讨厌的方式。

最常见的情况是即时事件。当您的代码执行某些操作时,浏览器会立即触发它们:

var l= document.getElementById('log'); var i= document.getElementById('inp'); i.onblur= function() { l.value+= 'blur\n'; }; setTimeout(function() { l.value+= '登录\n'; l.focus(); l.value+= '注销\n'; }, 100); i.focus();

在除 IE 之外的所有设备上都出现 log in, blur, log out。这些事件不仅会因为您直接调用 focus() 而触发,它们可能会因为您调用 alert()、打开弹出窗口或其他任何移动焦点而发生。

这也可能导致其他事件。例如,添加一个 i.onchange 侦听器并在 focus() 调用取消焦点之前在输入中键入一些内容,并且日志顺序是 log in, change, blur, log out,除了在 Opera 中它是 log in, blur, log out, change 和它在 IE 中(更难解释){ 5}。

同样,对提供它的元素调用 click() 会立即在所有浏览器中调用 onclick 处理程序(至少这是一致的!)。

(我在这里使用了直接的 on... 事件处理程序属性,但 addEventListenerattachEvent 也是如此。)

尽管您没有做任何事情来激发它,但还有很多情况下,当您的代码被线程化时,事件可能会触发。一个例子:

var l= document.getElementById('log'); document.getElementById('act').onclick= function() { l.value+= 'alert in\n';警报(“警报!”); l.value+= '警报输出\n'; }; window.onresize= function() { l.value+= 'resize\n'; };

点击 alert,您将看到一个模态对话框。在您关闭该对话之前,不会再执行脚本,是吗?没有。调整主窗口的大小,您将在 textarea 中获得 alert in, resize, alert out

您可能认为在模式对话框打开时调整窗口大小是不可能的,但事实并非如此:在 Linux 中,您可以随意调整窗口大小;在 Windows 上,这并不容易,但您可以通过将屏幕分辨率从较大的屏幕分辨率更改为较小的屏幕分辨率,从而调整窗口大小。

您可能会想,因为脚本是线程化的,所以当用户没有与浏览器进行主动交互时,只有 resize(可能还有一些类似 scroll)可以触发。对于单个窗口,您可能是对的。但是,一旦您执行跨窗口脚本,这一切都将付诸东流。对于除 Safari 之外的所有浏览器,当它们中的任何一个忙时会阻止所有窗口/选项卡/框架,您可以从另一个文档的代码与文档交互,在单独的执行线程中运行并导致任何相关的事件处理程序火。

在脚本仍然线程化的情况下,可以引发您可以生成的事件的地方:

在除 Opera 之外的所有浏览器中打开模式弹出窗口(警报、确认、提示)时;

在支持它的浏览器上的 showModalDialog 期间;

“此页面上的脚本可能正忙...”对话框,即使您选择让脚本继续运行,也允许触发和处理调整大小和模糊等事件,即使脚本处于繁忙的循环,除了在 Opera 中。

不久前,在带有 Sun Java 插件的 IE 中,调用 applet 上的任何方法都可以触发事件并重新输入脚本。这一直是一个对时间敏感的错误,Sun 可能已经修复了它(我当然希望如此)。

可能更多。自从我对此进行测试已经有一段时间了,从那以后浏览器变得越来越复杂。

总之,在大多数用户看来,JavaScript 在大多数情况下都具有严格的事件驱动单线程执行。实际上,它没有这样的东西。目前尚不清楚其中有多少只是一个错误,有多少是经过深思熟虑的设计,但如果你正在编写复杂的应用程序,尤其是跨窗口/框架脚本的应用程序,它很有可能会咬你——而且间歇性地,难以调试的方法。

如果最坏的情况出现在最坏的情况下,您可以通过间接所有事件响应来解决并发问题。当事件进入时,将其放入队列中,稍后在 setInterval 函数中按顺序处理队列。如果您正在编写一个打算供复杂应用程序使用的框架,那么这样做可能是一个不错的举措。 postMessage 也有望在未来缓解跨文档脚本的痛苦。


@JP:就我个人而言,我不想马上知道,因为这意味着我必须小心我的代码是可重入的,调用我的模糊代码不会影响某些外部代码所依赖的状态。有太多情况下,模糊是一种意想不到的副作用,必须抓住每一个。不幸的是,即使您确实想要它,它也不可靠! IE 触发 blur 您的代码将控制权返回给浏览器。
Javascript是单线程的。在 alert() 上停止执行并不意味着事件线程停止发送事件。只是意味着您的脚本在屏幕上显示警报时正在休眠,但它必须保持抽水事件才能绘制屏幕。当警报出现时,事件泵正在运行,这意味着继续发送事件是完全正确的。充其量,这演示了一个可以在 javascript 中发生的协作线程,但是所有这些行为都可以通过一个函数来解释,该函数只是将一个事件附加到事件泵以稍后处理而不是现在进行处理。
但是,请记住协作线程仍然是单线程的。两件事不能同时发生,这就是多线程允许并注入非确定性的原因。所描述的所有内容都是确定性的,这很好地提醒了这些类型的问题。分析工作做得很好@bobince
Chubbard 是对的:JavaScript 是单线程的。这不是多线程的示例,而是在单个线程中同步消息分派。是的,可以暂停堆栈并让事件派发继续(例如 alert()),但是在真正的多线程环境中发生的访问问题根本不可能发生;例如,在测试和随后的分配之间,您永远不会有变量更改值,因为您的线程不能被任意中断。我担心这种反应只会引起混乱。
是的,但是考虑到等待用户输入的阻塞函数可能发生在任何两个语句之间,您可能会遇到操作系统级线程给您带来的所有一致性问题。 JavaScript 引擎是否真的在多个操作系统线程中运行无关紧要。
M
Már Örlygsson

我会说是的 - 因为如果浏览器的 javascript 引擎异步运行它,几乎所有现有的(至少所有非平凡的)javascript 代码都会中断。

除此之外,HTML5 already specifies Web Workers(用于多线程 javascript 代码的显式标准化 API)将多线程引入基本 Javascript 几乎毫无意义。

其他评论者请注意:尽管 setTimeout/setInterval、HTTP 请求加载事件 (XHR) 和 UI 事件(点击、焦点等)提供了多线程的粗略印象 - 它们是仍然沿着一个时间线执行——一次一个——所以即使我们事先不知道它们的执行顺序,也不必担心在事件处理程序、定时函数或 XHR 回调执行期间外部条件的变化。)


我同意。如果多线程被添加到浏览器中的 Javascript 中,它将通过一些显式的 API(例如 Web Workers),就像它与所有命令式语言一样。这是唯一有意义的方法。
注意有单。主 JS 线程,但有些东西在浏览器中并行运行。这不仅仅是多线程的印象。请求实际上是并行运行的。您在 JS 中定义的侦听器是一个接一个运行的,但请求是真正并行的。
s
spender

是的,尽管在使用任何异步 API(例如 setInterval 和 xmlhttp 回调)时,您仍然会遇到一些并发编程问题(主要是竞争条件)。


C
ChessWhiz

是的,尽管 Internet Explorer 9 会在一个单独的线程上编译您的 Javascript,以准备在主线程上执行。不过,这不会改变你作为程序员的任何东西。


C
Community

我想说,规范并没有阻止某人创建在多个线程上运行 javascript 的引擎,要求代码执行同步以访问共享对象状态。

我认为单线程非阻塞范例源于需要在 ui 永远不会阻塞的浏览器中运行 javascript。

Nodejs 遵循了浏览器的方法。

Rhino 引擎,支持在不同线程中运行 js 代码。执行不能共享上下文,但它们可以共享范围。对于这种特定情况,文档指出:

...“Rhino 保证对 JavaScript 对象属性的访问是跨线程的原子访问,但不再保证脚本同时在同一范围内执行。如果两个脚本同时使用相同的范围,则脚本是负责协调对共享变量的任何访问。”

通过阅读 Rhino 文档,我得出结论,有人可以编写一个 javascript api,它也产生新的 javascript 线程,但 api 将是 rhino 特定的(例如节点只能产生一个新进程)。

我想即使对于在 javascript 中支持多线程的引擎,也应该与不考虑多线程或阻塞的脚本兼容。

关于浏览器和nodejs,我的看法是:

所有的js代码都在一个线程中执行吗? : 是的。

所有的js代码都在一个线程中执行吗? : 是的。

js代码可以导致其他线程运行吗? : 是的。

js代码可以导致其他线程运行吗? : 是的。

这些线程可以改变 js 执行上下文吗?:不能。但是它们可以(直接/间接(?))附加到事件队列中,侦听器可以从中改变执行上下文。但不要被愚弄,监听器再次在主线程上原子运行。

这些线程可以改变 js 执行上下文吗?:不能。但是它们可以(直接/间接(?))附加到事件队列中,侦听器可以从中改变执行上下文。但不要被愚弄,监听器再次在主线程上原子运行。

因此,在浏览器和 nodejs(可能还有很多其他引擎)的情况下,javascript 不是多线程的,但引擎本身是多线程的。

关于网络工作者的更新:

网络工作者的存在进一步证明了 javascript 可以是多线程的,因为有人可以在 javascript 中创建将在单独线程上运行的代码。

然而:web-workers 不会解决可以共享执行上下文的传统线程的问题。上面的规则 2 和 3 仍然适用,但这次线程代码是由用户(js 代码编写者)在 javascript 中创建的。

从效率(而不是并发)的角度来看,唯一要考虑的是生成的线程数。见下文:

About thread safety

Worker 接口产生了真正的操作系统级线程,细心的程序员可能会担心,如果您不小心,并发可能会在您的代码中产生“有趣”的效果。但是,由于 web Worker 已经仔细控制了与其他线程的通信点,因此实际上很难引起并发问题。无法访问非线程安全组件或 DOM。而且您必须通过序列化对象将特定数据传入和传出线程。所以你必须非常努力地在你的代码中引起问题。

附言

除了理论之外,请始终准备好 the accepted answer 中描述的可能的极端情况和错误


B
Bob

JavaScript/ECMAScript 被设计为存在于宿主环境中。也就是说,除非宿主环境决定解析和执行给定的脚本,并提供让 JavaScript 真正有用的环境对象(例如浏览器中的 DOM),否则 JavaScript 实际上不会做任何事情。

我认为给定的函数或脚本块将逐行执行,这对于 JavaScript 是有保证的。然而,也许一个宿主环境可以同时执行多个脚本。或者,主机环境总是可以提供一个提供多线程的对象。 setTimeoutsetInterval 是主机环境的示例,或者至少是伪示例,它们提供了一种执行某些并发(即使它不完全是并发)的方法。


k
kennebec

实际上,父窗口可以与运行自己的执行线程的子窗口或同级窗口或框架进行通信。


w
wle8300

@Bobince 提供了一个非常不透明的答案。

引用 Már Örlygsson 的回答,Javascript 始终是单线程的,因为这个简单的事实:Javascript 中的所有内容都沿单个时间线执行。

那是对单线程编程语言的严格定义。


b
bfavaretto

不。

我要反对这里的人群,但请耐心等待。单个 JS 脚本旨在有效地成为单线程,但这并不意味着它不能被不同地解释。

假设您有以下代码...

var list = [];
for (var i = 0; i < 10000; i++) {
  list[i] = i * i;
}

这是写在期望到循环结束时,列表必须有 10000 个索引平方的条目,但是 VM 可以注意到循环的每次迭代不会影响另一个,并使用两个线程重新解释。

第一个线程

for (var i = 0; i < 5000; i++) {
  list[i] = i * i;
}

第二个线程

for (var i = 5000; i < 10000; i++) {
  list[i] = i * i;
}

我在这里进行了简化,因为 JS 数组比哑内存块更复杂,但是如果这两个脚本能够以线程安全的方式向数组添加条目,那么当两者都执行完毕时,它就会有结果与单线程版本相同。

虽然我不知道有任何 VM 检测到这样的可并行化代码,但它似乎很可能在 JIT VM 的未来出现,因为它可以在某些情况下提供更快的速度。

进一步考虑这个概念,可以对代码进行注释以让 VM 知道要转换为多线程代码的内容。

// like "use strict" this enables certain features on compatible VMs.
"use parallel";

var list = [];

// This string, which has no effect on incompatible VMs, enables threading on
// this loop.
"parallel for";
for (var i = 0; i < 10000; i++) {
  list[i] = i * i;
}

由于 Web Worker 正在使用 Javascript,因此这种...更丑陋的系统不太可能出现,但我认为可以肯定地说 Javascript 传统上是单线程的。


大多数语言定义被设计为有效的单线程,并声明只要效果相同,就允许多线程。 (例如 UML)
我必须同意这个答案,因为当前的 ECMAScript 没有为并发的 ECMAScript 执行上下文提供任何规定(尽管可以说我认为 C 也可以这样说)。然后,就像这个答案一样,我认为任何具有并发线程能够修改共享状态的实现都是 ECMAScript 扩展。
F
Francisco Soto

好吧,Chrome 是多进程的,我认为每个进程都处理自己的 Javascript 代码,但就代码所知,它是“单线程”的。

Javascript 中不支持多线程,至少没有明确支持,所以它没有任何区别。


s
spyke

我已经尝试了@bobince 的示例,稍作修改:

<html>
<head>
    <title>Test</title>
</head>
<body>
    <textarea id="log" rows="20" cols="40"></textarea>
    <br />
    <button id="act">Run</button>
    <script type="text/javascript">
        let l= document.getElementById('log');
        let b = document.getElementById('act');
        let s = 0;

        b.addEventListener('click', function() {
            l.value += 'click begin\n';

            s = 10;
            let s2 = s;

            alert('alert!');

            s = s + s2;

            l.value += 'click end\n';
            l.value += `result = ${s}, should be ${s2 + s2}\n`;
            l.value += '----------\n';
        });

        window.addEventListener('resize', function() {
            if (s === 10) {
                s = 5;
            }

            l.value+= 'resize\n';
        });
    </script>
</body>
</html>

因此,当您按下 Run 时,关闭警报弹出窗口并执行“单线程”,您应该会看到如下内容:

click begin
click end
result = 20, should be 20

但是,如果您尝试在 Windows 上的 Opera 或 Firefox stable 中运行它并最小化/最大化窗口并在屏幕上弹出警报,则会出现以下情况:

click begin
resize
click end
result = 15, should be 20

我不想说这是“多线程”,但有些代码在错误的时间执行,我没想到会这样,现在我的状态已损坏。并且更好地了解这种行为。


a
aatwork

Javascript 引擎必须是单线程的,但 Javascript 运行时不需要是单线程的。

现在什么是Javascript引擎?那是执行实际 JS 代码的解释器。引擎需要一个主机。它不能自己运行。主机是 Javascript 运行时。

例如,在 Chrome 浏览器中运行的 V8 引擎是单线程的。 Chrome 浏览器是一个运行时,它有其他进程/线程来支持 V8 引擎。

您可以查看this article,其中解释得很漂亮。如果它有帮助,别忘了复出&赞成:)


J
James

尝试将两个 setTimeout 函数相互嵌套,它们将表现为多线程(即;外部计时器在执行其功能之前不会等待内部计时器完成)。


chrome 以正确的方式做到这一点,不知道@James 在哪里看到它是多线程的......:setTimeout(function(){setTimeout(function(){console.log('i herd you liek async')}, 0); alert('yo dawg!')}, 0)(为了记录,yo dawg 应该总是先出现,然后是控制台日志输出)