JavaScript是否保证是单线程的?

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

137041 次浏览

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

我的答案是肯定的——因为如果浏览器的javascript引擎异步运行,几乎所有现有的(至少所有非平凡的)javascript代码都会崩溃。

此外,HTML5已经指定了Web Workers(一个用于多线程javascript代码的显式标准化API)将多线程引入基本javascript是毫无意义的。

尽管setTimeout/setInterval, http请求onload事件(XHR)和UI事件(点击,聚焦等)提供了多线程的粗略印象-它们仍然都沿着单一的时间轴执行-一次一个-所以即使我们预先不知道它们的执行顺序,也没有必要担心在事件处理程序,定时函数或XHR回调的执行过程中外部条件的变化。

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

Javascript中不支持多线程,至少不是显式的,所以没有什么区别。

是的,尽管Internet Explorer 9会在一个单独的线程上编译你的Javascript,为在主线程上执行做准备。但是,对于作为程序员的您来说,这并没有任何改变。

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

我认为给定的函数或脚本块将逐行执行,这是JavaScript的保证。然而,宿主环境可能同时执行多个脚本。或者,宿主环境可以始终提供一个提供多线程的对象。setTimeoutsetInterval是一个例子,或者至少是伪例子,一个主机环境提供了一种实现并发的方法(即使它不是真正的并发)。

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

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

(*:忽略了浏览器是否真的使用一个操作系统线程实现他们的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">

结果在log in, blur, log out除IE。这些事件并不仅仅因为您直接调用了focus()而触发,它们也可能因为您调用了alert(),或者打开了一个弹出窗口,或者其他任何移动焦点的东西而发生。

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

类似地,在提供它的元素上调用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以外的所有浏览器,当其中一个浏览器忙时,它会阻塞所有窗口/选项卡/框架,您可以从另一个文档的代码中与一个文档交互,在单独的执行线程中运行,并导致任何相关的事件处理程序被触发。

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

  • 当模式弹出(alertconfirmprompt)是打开的,在所有浏览器,但Opera;

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

  • “本页上的某个脚本可能正在忙…”对话框,即使你选择让脚本继续运行,也允许像调整大小和模糊这样的事件触发并在脚本处于繁忙循环中间时进行处理,除非在Opera中。

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

  • 可能更多。我测试这个已经有一段时间了,浏览器也变得越来越复杂。

总之,对于大多数用户来说,JavaScript在大多数情况下都具有严格的事件驱动的单线程执行。在现实中,它没有这样的东西。目前还不清楚其中有多少是简单的错误,有多少是故意设计的,但如果您正在编写复杂的应用程序,特别是跨窗口/框架脚本的应用程序,那么它很可能会以间歇性的、难以调试的方式咬你一口。

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

实际上,父窗口可以与拥有自己执行线程的子窗口或兄弟窗口或框架通信。

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

不。

我要跟大家唱反调,但请大家耐心听我说。单个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传统上是单线程的。

@Bobince提供了一个非常模糊的答案。

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

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

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

但如果你尝试在Opera或Firefox稳定的Windows上运行这个,并最小化/最大化屏幕上弹出的警告窗口,那么会有这样的东西:

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

我不想说,这是“多线程”,但一些代码在错误的时间执行,我没有预料到这一点,现在我有一个损坏的状态。

我会说,规范不能阻止某人从创建引擎运行 javascript上的多个线程,要求代码执行同步访问共享对象状态。

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

Nodejs紧随浏览器的方法

犀牛引擎然而,支持在不同线程中运行js代码。执行不能共享上下文,但可以共享作用域。 对于这个特定的情况,文档声明:

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

通过阅读Rhino文档,我得出结论,对于某些人来说,编写一个javascript api也可以生成新的javascript线程,但api将是特定于Rhino的(例如,节点只能生成一个新进程)。

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

我认为浏览器nodejs是这样的:

    1. 所有js代码在单线程中执行吗?: # EYZ2
    1. js代码会导致要运行的其他线程吗?: # EYZ2
    1. 这些线程改变js执行上下文?但是它们可以(直接/间接地(?))附加到事件队列 监听器可以改变执行上下文。但是不要被愚弄,侦听器再次运行原子地在主线程上

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


关于网络工作者的更新:

网络工作者的存在进一步证明了javascript可以是多线程的,从某种意义上说,有人可以用javascript创建代码,这些代码将在单独的线程上运行。

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

效率(和没有并发)的角度来看,唯一与考虑相关的是衍生线程的数量。见下文:

# EYZ0:

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

然而,由于网络工作者小心翼翼地将控制通信点与其他线程一起使用,所以它实际上非常很难引起并发性问题。不能访问非线程安全组件或DOM。并且您必须通过序列化对象将特定数据传入和传出线程。所以你必须非常努力地在你的代码中制造问题。


注:

除了理论,要随时准备好可能出现的极端情况和公认的答案中描述的bug

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

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

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

你可以查看这篇文章,那里有漂亮的解释。如果有帮助,别忘了回复&upvote:)