JavaScript 并发与并行的特性

JavaScript 是一门支持并行计算的编程语言。
概念解析
顺序:以数学中「加减乘除」计算来阐述,先计算乘除、再计算加减,这种逻辑是基于「时间先后」顺序的,也就称之为时序逻辑或顺序逻辑。
并行:它是非顺序逻辑下的计算模型,去除掉了时间概念,不需要考虑计算对象之间的关系,它的执行不与任何数、任何逻辑产生顺序依赖关系。
并发:在早期的计算系统是单核单进程的,一旦计算机开始打印某个东西,处于电脑前的工作人员就只能放下手上的工作,等待它,直到完成为止。所以这种集中式的环境需要通过并行来释放生产力。
清除或忽略时间维度的分布式环境,可以直接使用并行技术方案;而在集中式的环境中必须添加时间维度,才能使用并发来解决相同的问题。
并发不是并行,并发的逻辑里是存在「时间」维度的,而并行计算模型中是没有这个维度的。
在工作逻辑中:优先使用并行,然后再考虑并发。
一段代码可以运行在分布式的环境中而无视时间,那么将它抄写在纸面上并进行人工推算,与运行在一个机器环境中并没有任何计算性质的不同。前提是排除了「何时开始计算与计算多长时间」等条件依赖。
换句话说:程序可以陈述计算结果,而不依赖计算过程,那么它必然是可并行的。
并行模型
Node.js 内置 Chrome V8 引擎,完全基于 ECMAScript 规范实现,它本质上并不是「并行」的语言引擎。Node.js 在这基础上,通过宿主提供了事件机制,并且融合进了支持非阻塞 IO 的 libuv 。在应用层面的编程,就可以使用异步回调这样的并行特征。
事件回调的本质是使用「消息收发」,这也是在语言层面实现并行特性的可选模型,但 ECMAScript 并没有这样做,在 ECMAScript 2015 中提出了新的并行模型 Promise 。
Promise 在语言层面实现了并行执行的模型,它封装了一个「剥离了时间特性的数据,并代理该数据上的一切行为」,所以它的行为也是没有顺序意义的。
在传统并发思路上理解 Promise 机制时,最容易搞不清的就是「promise 什么时候执行」。
Promise 机制中并没有延时,也没有被延时的行为,更没有对「时间」这个维度的控制,因此在 JavaScript 中创建一个 promise 时,创建过程是立即完成的。使用原型方法 promise.resole、promise.reject 创建的 promise 也是立即完成的。
所有的 promise 对象都是在你需要时立即就生成的,但是 … 这些 promise 代理的那个数据还没有就绪(Ready)
而一旦就绪,promise.then(foo) 中的 foo 就被触发了。不过这仍然不是并行特性, .then 代表了对顺序逻辑的理解。
任务队列
上面我们说过,在集中式的现场不能运行并行逻辑,所以通常会使用「并发」机制来解决这一问题。而并发机制的本质就是在并行中加入了「时间」维度。
并发的过程,具体分解为三个步骤:排队、唤醒、执行。这个通常对被处理的程序称为「调度」。
JavaScript 中的代码总是从全局代码(Script)或者模块的顶层代码(Module)开始运行的。当语法分析阶段结束后,JavaScript 就可以明确地知道哪些代码是全局的、哪些代码是模块顶层的,然后会将它们作为一段代码放在一个执行上下文中,再之后,这个上下文就被放到执行引擎中去执行了。
执行引擎会从全局的或模块顶层的第一行代码执行到最后一行,这个执行序列被称为脚本任务(ScriptJob),在这个执行序列处理完之前,ScriptJob 之外的代码都是没有执行权的。
例如,代码通过 <script> 标签装载的话,这些脚本块中的全局代码作为各个独立的 ScriptJob 放在一个队列中等待引擎顺序处理。
在 ES8(2017)之后,引擎会自主启动一个称为 RunJobs 的过程,这是一个循环,每个迭代都将由引擎扫描待执行的任务队列,并取出一个等待中的任务执行它。
不同的执行逻辑将它们的任务 Job 放到队列中,引擎总是会在循环中扫描到该 Job 并执行它。
引擎会为 Promise 建立一个任务队列,这个队列与 ScriptJob 所在的队列类似。
每个任务 Job 都需要一个自己的可执行上下文,这样的执行上下文是所有的可执行结构都具备的。
可执行结构包括:
  • 脚本(Script)
  • 模块(Module)
  • 函数(Function)

既然如此,就只需要将 PromiseJob 映射为函数,那么 Job 的上下文自然也可以「作为一个函数执行结构」放到执行栈中,并且可以按照标准过程处理。

也就是说,调度一个 PromiseJob 和调度一个函数,没有区别。
执行栈
执行栈是按约定顺序处理的一种结构,结构里是多个可执行的上下文,关于执行栈的执行,有个约定的顺序原则:
当前栈顶的上下文,就是运行中的活动上下文。
即:当有一个并行行为(Promise)出现时,这个行为在概念上由于是并行的,所以不能入栈,因为栈顶是活动的上下文,入栈就会激活它的行为,因此需要通过 EnqueueJob 过程将它作为一个 PendingJob 添加到任务队列(Job Queues)中。
当栈顶为空时,引擎扫描待执行的任务队列,从上述队列中取出 PendingJob 来执行即可。
这个过程就是所谓的 RunJobs()。
Promise 的唤醒操作发生于 RunJobs 中,它会扫描 PromiseJobs 并处理那些已经就绪的 Promise。

图片授权基于 www.pixabay.com 相关协议
内容来源于《JavaScript 语言精髓与编程实战》

原创文章,作者:小道研究,如若转载,请注明出处:https://www.sudun.com/ask/34578.html

(0)
小道研究's avatar小道研究
上一篇 2024年4月15日 下午6:47
下一篇 2024年4月15日 下午6:49

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注