众所周知,JavaScript在所有现代浏览器实现中都是单线程的,但是它是在任何标准中指定的,还是仅根据传统?假定JavaScript始终是单线程的,是否完全安全?
众所周知,JavaScript在所有现代浏览器实现中都是单线程的,但是它是在任何标准中指定的,还是仅根据传统?假定JavaScript始终是单线程的,是否完全安全?
Answers:
这是个好问题。我很想说“是”。我不能
通常认为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">
在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...
事件处理程序属性,但addEventListener
and 也会发生同样的情况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
)是开放的,在所有的浏览器,但歌剧;
在showModalDialog
支持它的浏览器中;
即使您选择让脚本继续运行,“此页面上的脚本可能正忙于...”对话框也将允许触发大小调整和模糊之类的事件,即使该脚本位于脚本中间,也可以对其进行处理。忙循环,Opera除外。
对我来说,前一段时间,在带有Sun Java Plugin的IE中,调用applet上的任何方法都可以触发事件并重新输入脚本。自始至终,这始终是一个对时间敏感的错误,Sun可能已经修复了此错误(我当然希望如此)。
可能更多。自从我测试了这已经有一段时间了,浏览器也因此变得越来越复杂。
总之,大多数时候,JavaScript对大多数用户而言似乎具有严格的事件驱动的单线程执行。实际上,它没有这样的东西。尚不清楚其中有多少只是一个错误,有多少是经过深思熟虑的设计,但是如果您要编写复杂的应用程序,尤其是跨窗口/框架脚本的应用程序,那么它很有可能会咬住您-并且断断续续地,难以调试的方式。
如果情况变得最糟,则可以通过间接所有事件响应来解决并发问题。当事件进入时,将其放入队列中,然后在setInterval
函数中按顺序处理该队列。如果要编写一个打算由复杂应用程序使用的框架,那么这样做可能是一个不错的选择。postMessage
希望将来也能减轻跨文档脚本编写的痛苦。
blur
后, IE会触发。
我会说是-因为如果浏览器的javascript引擎异步运行它,则几乎所有现有(至少所有不重要的)javascript代码都会中断。
此外,HTML5已经指定了将Web多线程引入基本Javascript中的Web Workers(用于多线程javascript代码的显式,标准化API)这一事实几乎没有意义。
(其他注释者的注意:即使setTimeout/setInterval
HTTP请求重载事件(XHR)和UI事件(单击,焦点等)也提供了多线程的粗略印象-它们仍然都是沿着单个时间轴执行的-时间-因此,即使我们事先不知道它们的执行顺序,也不必担心事件处理程序,定时函数或XHR回调的执行期间外部条件会发生变化。)
我要说的是,说明书中并没有防止有人从创建发动机该运行 JavaScript的上多个线程,要求代码以访问共享对象状态执行同步。
我认为单线程非阻塞范例是出于在ui永远不会阻塞的浏览器中运行javascript的需要。
Nodejs遵循了浏览器的方法。
但是Rhino引擎支持在不同线程中运行js代码。执行不能共享上下文,但是可以共享作用域。对于这种特定情况,文档说明:
...” Rhino保证跨线程访问JavaScript对象的属性是原子的,但不再保证同时在同一作用域中执行的脚本。如果两个脚本同时使用同一作用域,则这些脚本是负责协调对共享变量的所有访问。”
通过阅读Rhino文档,我得出的结论是,有人可以编写也产生新的JavaScript线程的javascript API,但是该api是特定于Rhino的(例如,节点只能产生新的进程)。
我想即使对于支持JavaScript中多个线程的引擎,也应该与不考虑多线程或阻塞的脚本兼容。
Concearning 浏览器和的NodeJS的方式,我看到它是:
因此,在使用浏览器和Node.js(可能还有许多其他引擎)的情况下,javascript不是多线程的,而引擎本身是。
网络工作人员的存在进一步证明了javascript可以是多线程的,因为有人可以在javascript中创建将在单独的线程上运行的代码。
但是:网络工作者不会担心可以共享执行上下文的传统线程的问题。上面的规则2和3仍然适用,但是这次线程代码是由用户(js代码编写器)在javascript中创建的。
从效率(而不是并发性)的角度来看,唯一要考虑的是产生的线程数。见下文:
Worker接口产生真正的OS级线程,并且如果您不小心,那么谨慎的程序员可能会担心并发会在您的代码中引起“有趣的”结果。
但是,由于Web工作人员已经仔细控制了与其他线程的通信点,因此实际上很难引起并发问题。无法访问非线程安全组件或DOM。而且,您必须通过序列化的对象将特定数据传入和传出线程。因此,您必须非常努力地在代码中引起问题。
除了理论外,还应始终准备好接受的答案中描述的可能的极端情况和错误
没有。
我要反对这里的人群,但要忍受我。单个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是单线程的。
我尝试对@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稳定版中运行此程序,并在屏幕上弹出警告窗口的情况下最小化/最大化窗口,则将出现以下内容:
click begin
resize
click end
result = 15, should be 20
我不想说这是“多线程”,但是某些代码段在错误的时间内执行了,而我对此并不期望,现在我的状态已损坏。更好地了解这种行为。
尝试将两个setTimeout函数相互嵌套,它们将表现为多线程(即,外部计时器在执行其功能之前不会等待内部的计时器完成)。
setTimeout(function(){setTimeout(function(){console.log('i herd you liek async')}, 0); alert('yo dawg!')}, 0)
记录,哟dawg应该总是先出现,然后是控制台日志输出)