JavaScript 是不是保证是单线程的?

Posted

技术标签:

【中文标题】JavaScript 是不是保证是单线程的?【英文标题】:Is JavaScript guaranteed to be single-threaded?JavaScript 是否保证是单线程的? 【发布时间】:2011-02-13 14:38:55 【问题描述】:

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

【问题讨论】:

在浏览器的上下文中,可能。但是有些程序允许您将 JS 视为***语言并为其他 C++ 库提供绑定。例如,flusspferd (C++ bindings for JS - AWESOME BTW) 正在用多线程 JS 做一些事情。这取决于上下文。 这是必读的:developer.mozilla.org/en/docs/Web/JavaScript/EventLoop @RickyA 你创建了一个循环引用!那篇文章链接到这个问题,该问题链接到…… 【参考方案1】:

我想说的是,规范并不妨碍某人创建一个引擎,该引擎多线程运行javascript >,需要代码执行同步以访问共享对象状态。

我认为单线程非阻塞范式是出于在 浏览器中运行 javascript 的需要,其中 ui 不应该阻塞。

Nodejs 遵循了浏览器的方法

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

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

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

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

关于 browsersnodejs 我的看法是:

    所有的js代码是在单线程中执行的吗? :是的。
    js 代码能否导致其他线程运行? :是的。
    这些线程可以改变 js 执行上下文吗?:不能。但它们可以(直接/间接(?))附加到 事件队列 从中 侦听器可以改变执行上下文。但不要上当,监听器会再次在主线程上自动运行

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


关于网络工作者的更新:

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

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

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

About thread safety:

Worker 接口产生真正的操作系统级线程,有心的程序员可能会担心,如果您不小心,并发会在您的代码中产生“有趣”的效果。

但是,由于 web Worker 已经仔细控制了与其他线程的通信点,实际上很难导致并发问题。无法访问非线程安全组件或 DOM。而且您必须通过序列化对象将特定数据传入和传出线程。所以你必须非常努力地在你的代码中引起问题。


附言

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

【讨论】:

【参考方案2】:

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

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+= 'log in\n';
    l.focus();
    l.value+= 'log out\n';
, 100);
i.focus();
<textarea id="log" rows="20" cols="40"></textarea>
<input id="inp">

在除 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 中它是(甚至更难解释)@ 987654330@.

类似地在一个元素上调用click(),它会立即在所有浏览器中调用onclick 处理程序(至少这是一致的!)。

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

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

var l= document.getElementById('log');
document.getElementById('act').onclick= function() 
    l.value+= 'alert in\n';
    alert('alert!');
    l.value+= 'alert out\n';
;
window.onresize= function() 
    l.value+= 'resize\n';
;
<textarea id="log" rows="20" cols="40"></textarea>
<button id="act">alert</button>

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

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

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

在脚本仍然线程化时可以引发您可能导致生成的事件的地方:

当模式弹出窗口(@98​​7654342@、confirmprompt)在除 Opera 之外的所有浏览器中打开时;

在支持 showModalDialog 的浏览器上;

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

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

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

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

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

【讨论】:

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

我尝试了@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>

所以,当你按下运行时,关闭警报弹出窗口并执行“单线程”,你应该会看到如下内容:

click begin
click end
result = 20, should be 20

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

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

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

【讨论】:

【参考方案4】:

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

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

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

【讨论】:

【参考方案5】:

没有。

我在这里与人群对抗,但请耐心等待。单个 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 Workers 开始使用 Javascript,这个...更丑陋的系统不太可能出现,但我认为可以肯定地说 Javascript 在传统上是单线程的。

【讨论】:

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

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

【讨论】:

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

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

【讨论】:

【参考方案8】:

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

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

【讨论】:

【参考方案9】:

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

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

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

【讨论】:

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

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

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

【讨论】:

【参考方案11】:

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

【讨论】:

【参考方案12】:

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

【讨论】:

以上是关于JavaScript 是不是保证是单线程的?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 是单线程的而且是异步的机制

Controller是单例模式的吗?如何保证线程安全?

JavaScript是单线程还是多线程(转)

NodeJS 单线程 如何保证其安全,稳定性?

JavaScript学习手册(61)

Javascript是单线程的深入分析(转)