众所周知,在所有现代浏览器实现中,JavaScript 都是单线程的,但这是在任何标准中指定的还是仅由传统指定的?假设 JavaScript 总是单线程的是否完全安全?
JavaScript 是否保证是单线程的?
这是个好问题。我很想说“是”。我不能。
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
,除了在它所在的 Operalog in, blur, log out, change
和 IE 所在的位置(甚至更不明确)log in, change, log out, blur
。
类似地,调用click()
提供它的元素会onclick
立即在所有浏览器中调用处理程序(至少这是一致的!)。
(我在这里使用直接on...
事件处理程序属性,但同样发生在addEventListener
和 上attachEvent
。)
尽管您没有采取任何措施来激发它,但还有很多情况会在您的代码被线程化时触发事件。一个例子:
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 之外的所有浏览器,当它们中的任何一个忙时,它会阻止所有窗口/选项卡/框架,您可以从另一个文档的代码与文档交互,在单独的执行线程中运行并导致任何相关的事件处理程序火。
在脚本仍然线程化时可以引发您可以导致生成的事件的地方:
当模态弹出窗口 (
alert
,confirm
,prompt
) 打开时,在除 Opera 之外的所有浏览器中;在
showModalDialog
支持它的浏览器上;“此页面上的脚本可能正忙...”对话框,即使您选择让脚本继续运行,也允许触发和处理诸如调整大小和模糊之类的事件,即使脚本处于中间忙循环,除了在 Opera 中。
不久前对我来说,在带有 Sun Java 插件的 IE 中,调用小程序上的任何方法都可能允许触发事件并重新输入脚本。这一直是一个对时间敏感的错误,而且 Sun 可能已经修复了它(我当然希望如此)。
可能更多。自从我测试这个已经有一段时间了,浏览器已经变得复杂了。
总之,在大多数情况下,对于大多数用户来说,JavaScript 似乎具有严格的事件驱动单线程执行。实际上,它没有这样的东西。目前尚不清楚这其中有多少只是一个错误,有多少是有意设计的,但是如果您正在编写复杂的应用程序,尤其是跨窗口/框架脚本的应用程序,那么它很有可能会咬到您——而且在间歇性的情况下,难以调试的方式。
如果最坏的情况发生,您可以通过间接所有事件响应来解决并发问题。当一个事件进来时,把它放到一个队列中,然后在一个setInterval
函数中按顺序处理这个队列。如果您正在编写一个打算供复杂应用程序使用的框架,那么这样做可能是一个不错的举措。postMessage
也有望在未来缓解跨文档脚本的痛苦。
我会说是的 - 因为如果浏览器的 javascript 引擎异步运行它,几乎所有现有的(至少所有非平凡的)javascript 代码都会中断。
此外,HTML5 已经指定了 Web Workers(一种用于多线程 JavaScript 代码的明确的标准化 API),将多线程引入基本 Javascript 几乎毫无意义。
(其他评论者请注意:尽管setTimeout/setInterval
HTTP 请求加载事件 (XHR) 和 UI 事件(单击、焦点等)提供了多线程的粗略印象 - 它们仍然沿单个时间线执行 - 一个在一段时间 - 因此,即使我们事先不知道它们的执行顺序,也无需担心在执行事件处理程序、定时函数或 XHR 回调期间外部条件发生变化。)
是的,尽管在使用任何异步 API(例如 setInterval 和 xmlhttp 回调)时,您仍然会遇到并发编程的一些问题(主要是竞争条件)。
是的,尽管 Internet Explorer 9 会在单独的线程上编译您的 Javascript,以准备在主线程上执行。但是,作为程序员,这不会改变您的任何内容。
我要说的是,说明书中并没有防止有人从创建发动机该运行JavaScript的上多个线程,要求代码以访问共享对象状态执行同步。
我认为单线程非阻塞范式源于需要在 ui 永远不会阻塞的浏览器中运行 javascript 。
Nodejs遵循了浏览器的方法。
然而,Rhino引擎支持在不同线程中运行 js 代码。执行不能共享上下文,但它们可以共享范围。对于这种特定情况,文档说明:
...“Rhino 保证对 JavaScript 对象的属性的访问是跨线程原子的,但不会对同时在同一范围内执行的脚本做出更多保证。如果两个脚本同时使用相同的范围,则脚本是负责协调对共享变量的任何访问。”
通过阅读 Rhino 文档,我得出结论,有人可以编写一个 javascript api,它也可以生成新的 javascript 线程,但 api 将是 Rhino 特定的(例如,节点只能生成一个新进程)。
我想即使对于在 javascript 中支持多线程的引擎,也应该与不考虑多线程或阻塞的脚本兼容。
关于浏览器和nodejs,我的看法是:
-
- 难道所有的JS代码在执行单线程?:是的。
-
- 灿JS代码造成其他线程运行?:是的。
-
- 这些线程可以改变 js 执行上下文吗?:不可以。但是它们可以(直接/间接(?))附加到事件队列中, 侦听器可以从中改变执行上下文。但是不要被愚弄,侦听器再次在主线程上原子地运行。
因此,在浏览器和 nodejs(可能还有很多其他引擎)的情况下,javascript 不是多线程的,但引擎本身是.
关于网络工作者的更新:
网络工作者的存在进一步证明了 javascript 可以是多线程的,从某种意义上说,有人可以在 javascript 中创建将在单独线程上运行的代码。
然而:web-workers 不会解决可以共享执行上下文的传统线程的问题。上面的规则 2 和 3 仍然适用,但这次线程代码是由用户(js 代码编写者)在 javascript 中创建的。
从效率(而不是并发)的角度来看,唯一需要考虑的是衍生线程的数量。见下文:
Worker 接口产生了真正的操作系统级线程,细心的程序员可能会担心,如果您不小心,并发性可能会在您的代码中产生“有趣”的效果。
但是,由于 web Worker 对与其他线程的通信点进行了精心控制,因此实际上很难导致并发问题。无法访问非线程安全组件或 DOM。并且您必须通过序列化对象将特定数据传入和传出线程。所以你必须非常努力地在你的代码中引起问题。
聚苯乙烯
除了理论之外,始终准备好接受的答案中描述的可能的极端情况和错误