JavaScript 是否保证是单线程的?

IT技术 javascript concurrency
2021-02-06 00:58:32

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

6个回答

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

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也有望在未来缓解跨文档脚本的痛苦。

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

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

此外,HTML5 已经指定了 Web Workers(一种用于多线程 JavaScript 代码的明确的标准化 API),将多线程引入基本 Javascript 几乎毫无意义。

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

我同意。如果在浏览器中将多线程添加到 Javascript,它将通过一些显式 API(例如 Web Workers),就像所有命令式语言一样。这是唯一有意义的方法。
2021-03-10 00:58:32
请注意,有一个。主 JS 线程,但有些东西在浏览器中并行运行。这不仅仅是多线程的印象。请求实际上是并行运行的。您在 JS 中定义的侦听器是一对一运行的,但请求是真正并行的。
2021-03-27 00:58:32

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

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

我要说的是,说明书中并没有防止有人从创建发动机运行JavaScript的上多个线程,要求代码以访问共享对象状态执行同步。

我认为单线程非阻塞范式源于需要在 ui 永远不会阻塞的浏览器运行 javascript

Nodejs遵循了浏览器的方法

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

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

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

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

关于浏览器nodejs,我的看法是:

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

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


关于网络工作者的更新:

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

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

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

关于线程安全

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

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


聚苯乙烯

除了理论之外,始终准备好接受的答案中描述的可能的极端情况和错误