JavaScript保证是单线程的吗?

Ego*_*hin 588 javascript concurrency

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

bob*_*nce 568

这是个好问题.我想说"是".我不能.

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();
Run Code Online (Sandbox Code Playgroud)
<textarea id="log" rows="20" cols="40"></textarea>
<input id="inp">
Run Code Online (Sandbox Code Playgroud)

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';
};
Run Code Online (Sandbox Code Playgroud)
<textarea id="log" rows="20" cols="40"></textarea>
<button id="act">alert</button>
Run Code Online (Sandbox Code Playgroud)

点击alert,你会得到一个模态对话框.在您解除对话之前不再执行脚本,是吗?不.调整主窗口的大小,您将进入alert in, resize, alert outtextarea.

您可能认为在模式对话框启动时调整窗口大小是不可能的,但不是这样:在Linux中,您可以根据需要调整窗口大小; 在Windows上它并不那么容易,但是你可以通过将屏幕分辨率从较大的屏幕分辨率更改为较小的屏幕分辨率(窗口不适合)来实现,从而使其调整大小.

您可能会认为,当用户没有与浏览器进行活动交互时,它resize可能scroll会触发(也可能更像一些),因为脚本是线程化的.对于单个窗口,您可能是对的.但是,只要你在进行跨窗口脚本编写,这一切都会进入底池.对于Safari以外的所有浏览器,当其中任何一个窗口/标签/框架忙时,它们会阻止所有窗口/标签/框架,您可以从另一个文档的代码中与文档交互,在单独的执行线程中运行并导致任何相关的事件处理程序火.

在脚本仍处于线程状态时,可以引发可以导致生成的事件的位置:

  • 当模式弹出窗口(alert,confirm,prompt)是开放的,在所有的浏览器,但歌剧;

  • showModalDialog支持它的浏览器上;

  • "此页面上的脚本可能正忙..."对话框,即使您选择让脚本继续运行,也可以调整调整大小和模糊等事件,即使脚本处于中间位置也可以处理繁忙的循环,除了Opera.

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

  • 可能更多.自从我测试了这个版本已经有一段时间了,浏览器从此开始变得复杂.

总之,大多数用户在大多数情况下都会看到JavaScript具有严格的事件驱动的单个执行线程.实际上,它没有这样的东西.目前尚不清楚这有多少只是一个错误和多少刻意的设计,但如果你正在编写复杂的应用程序,特别是跨窗口/框架脚本的应用程序,它有可能咬你吗? - ?间歇性的,难以调试的方式.

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

  • Javascript是单线程的.暂停执行alert()并不意味着事件线程停止抽取事件.只是意味着你的脚本在屏幕上显示警报时正在睡觉,但它必须保持抽取事件以便绘制屏幕.当警报响起时,事件泵正在运行,这意味着继续发送事件是完全正确的.最好的情况是,这展示了一种可以在javascript中发生的协作线程,但所有这些行为都可以通过一个函数来解释,该函数只是将一个事件附加到事件泵,以便稍后处理而不是现在处理. (102认同)
  • Chubbard是对的:JavaScript是单线程的.这不是多线程的示例,而是单个线程中的同步消息分派.是的,可以暂停堆栈并继续事件调度(例如alert()),但真正的多线程环境中出现的访问问题种类根本不可能发生; 例如,在测试和紧接着的后续赋值之间,您永远不会有变量值,因为您的线程不能被任意中断.我担心这种反应只会引起混乱. (93认同)
  • 但是,请记住合作线程仍然是单线程的.有两件事不可能同时发生,即多线程允许并注入非确定性.所描述的所有内容都是确定性的,这是对这些类型问题的一个很好的提醒.好好分析@bobince (32认同)
  • 是的,但鉴于等待用户输入的阻塞函数可能发生在任何两个语句之间,您可能存在操作系统级线程带给您的所有一致性问题.JavaScript引擎是否实际在多个OS线程中运行并不重要. (19认同)
  • @JP:我个人不想马上知道,因为这意味着我必须要小心我的代码是可重入的,调用我的模糊代码不会影响某些外部代码所依赖的状态.有太多的情况,模糊是一个意想不到的副作用,必然会抓住每一个.不幸的是,即使你*需要它,它也不可靠!在您的代码将控制权返回给浏览器后,IE会触发`blur`*. (14认同)
  • 这个答案充其量是误导性的.它描述了JavaScript**可能是异步**,这是真的.但没有WebWorkers的基本JavaScript**绝不是多线程**.请参阅下面的MárÖrlygsson的答案. (7认同)
  • 我不理解焦点/模糊行为的问题 - 对我来说似乎是完全可以预料到的.当事情集中或模糊时,我想尽快知道.但是,我明白为什么你提到它 - 它与主题相关:) (6认同)
  • @Kris:看看现代浏览器中看门狗的讨论,它确实可以随意中断你的执行线程.如果你有一个长时间运行的脚本而且运气不好,那么在测试和后续分配之间进行变量显然是可能的. (6认同)
  • 你有这个讨论的链接吗?我应该澄清一下,尽管JavaScript线程可能被中断(显然主机应用程序可能有多个线程),但问题是运行JavaScript的一个线程是否可以被另一个针对相同全局上下文运行JavaScript的线程中断.有可能通过颠覆JavaScript引擎的意图来构建一个执行此操作的应用程序,但此示例并未说明这一点. (6认同)
  • 对不起,这个广泛投票的答案是不正确的,当然不是关于现代版Chrome.所描述的"立即"事件是立即和同步调用的,而不是由事件循环调用.正如@doubleOrt所提到的,通过调用alert(),消息仅在alert()完成后显示"blur"的执行.请参阅https://jsfiddle.net/xewfpcxk/.这被认为是JavaScript的基本特征,通常称为"运行完成". (3认同)
  • 根据我的经验,在操纵DOM的过程中会出现奇怪的行为,我似乎回想起我所遇到的一个问题,我认为在一个函数中操作DOM中的大量元素之后它们会被消失/添加,当我打电话给下一个操纵功能:/ (2认同)
  • @bobince - 您的初始登录/焦点()/注销示例很有趣,但这种行为真的很奇怪吗?使用代码触发焦点,我希望它类似于login/doSomething()/ logout,并在进入注销之前完成中间位. (2认同)
  • @bobince事件循环重入为您带来与共享内存并发性截然不同的问题和解决方案.假设它们是相同的会迅速让你陷入困境.一个简单的例子是尝试使用互斥锁来制作"事件循环可重入" - 安全.如果它是一个可重入的互斥锁,它什么都不做,如果它不是可重入的,你最终就会陷入僵局.事件循环重入有很多陷阱,但它们不是一样的陷阱. (2认同)
  • 我刚刚在Windows上使用Chrome 27.0.1453.110上的警报测试了您的示例,无法重现您描述的行为; 如果我在警报处于活动状态时更改屏幕分辨率,则会在textarea中显示"警报输出调整大小".也许这些问题最近已得到解决? (2认同)
  • 好吧,好的,所以您可以说“ JavaScript是单线程的”,但是您不能说“当输入内联脚本,事件侦听器或超时时,您将完全处于控制状态,直到从块或函数的结尾返回为止“; 而且您显然甚至无法说“您将一直处于完全控制状态,直到您*调用一个泵送事件*或[现有原因]的函数”。(至少,如果关于“无响应脚本”警告的内容仍然是正确的,那不是那样。)因此也许应该将第一段重写一下,说些类似“是的,但这没有您想象的那样有用”。 (2认同)
  • _Chrome 版本 63.0.3239.132_:所描述的行为对于手动 `.focus()` 调用仍然正确,但对于 `alert()` 或弹出窗口则不正确,对于这 2 个,`登录注销模糊` 将被打印出来。 (2认同)

Már*_*son 113

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

除此之外,HTML5已经指定Web Workers(一种用于多线程javascript代码的显式,标准化API)这一事实将多线程引入基本Javascript中将是毫无意义的.

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

  • 我同意.如果在浏览器中将多线程添加到Javascript中,它将通过一些显式API(例如Web Workers),就像使用*all*命令式语言一样.这是唯一有道理的方法. (21认同)

spe*_*der 16

是的,虽然在使用任何异步API(如setInterval和xmlhttp回调)时仍然可能遇到并发编程(主要是竞争条件)的一些问题.


Che*_*hiz 10

是的,虽然Internet Explorer 9将在单独的线程上编译您的Javascript,以准备在主线程上执行.但是,对于作为程序员的人来说,这并没有改变任何东西.


Bob*_*Bob 7

JavaScript/ECMAScript旨在生活在主机环境中.也就是说,除非主机环境决定解析并执行给定的脚本,并且提供允许JavaScript实际有用的环境对象(例如浏览器中的DOM),否则JavaScript实际上不会执行任何操作.

我认为给定的函数或脚本块将逐行执行,并且保证JavaScript.但是,主机环境可能同时执行多个脚本.或者,主机环境始终可以提供提供多线程的对象.setTimeout并且setInterval是主机环境的示例,或者至少是伪示例,提供了一种进行并发的方法(即使它不是完全并发).


小智 7

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


wle*_*300 6

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

忽略了MárÖrlygsson的回答,由于以下简单事实,Javascript始终是单线程的:Javascript中的所有内容都沿着单个时间轴执行。

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


Mar*_* An 6

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

我认为单线程非阻塞范例来自于在ui永远不应该阻塞的浏览器中运行javascript的需要.

Nodejs遵循浏览器的方法.

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

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

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

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

以我看到的方式来构思浏览器和nodejs:

  • 是否所有js代码都在一个线程中执行?:是的.

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

  • 这些线程是否可以改变js执行上下文?:否.但是它们可以(直接/间接(?))附加到事件队列中.

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