1月18日,
宣布了Node.js平台版本
11.7.0 。 在该版本的显着变化中,可以注意到来自实验模块worker_threads的结论,该结论出现在Node.js
10.5.0中 。 现在,不需要使用--experimental-worker标志。 该模块自成立以来一直保持相当稳定,因此
做出了
决定 ,反映在Node.js 11.7.0中。

该材料的作者(我们正在翻译的内容)提供了对worker_threads模块功能的讨论,特别是,他想谈谈为什么需要此模块,以及出于历史原因如何在JavaScript和Node.js中实现多线程。 在这里,我们将讨论与编写多线程JS应用程序有关的问题,解决它们的现有方法以及使用所谓的“工作线程”(有时称为“工作线程”)进行并行数据处理的未来。或只是“工人”。
单线程世界中的生活
JavaScript被认为是一种在浏览器中运行的单线程编程语言。 “单线程”表示在同一过程中(在现代浏览器中,我们正在谈论单独的浏览器选项卡),一次只能执行一组指令。
这简化了应用程序开发,简化了程序员的工作。 最初,JavaScript是仅适用于向网页添加一些交互功能的语言,例如,表单验证之类的语言。 在JS设计的任务中,没有什么需要多线程的特别复杂的事情。
Node.js的创建者
Ryan Dahl看到了这种语言限制的有趣机会。 他想实现一个基于异步I / O子系统的服务器平台。 这意味着程序员无需使用线程,这极大地简化了类似平台的开发。 在开发设计用于并行代码执行的程序时,可能会出现很难解决的问题。 例如,如果多个线程试图访问同一内存区域,则可能导致导致程序中断的所谓“进程竞争状态”。 此类错误很难重现和纠正。
Node.js平台是单线程的吗?
Node.js应用程序是单线程的吗? 是的,从某种意义上讲。 实际上,Node.js允许您并行执行某些操作,但为此,程序员无需创建线程或对其进行同步。 Node.js平台和操作系统通过自己的方式执行并行输入/输出操作,并且当需要使用我们的JavaScript代码进行数据处理时,它将在单线程模式下工作。
换句话说,除了我们的JS代码外,其他所有东西都可以并行工作。 在JavaScript代码的同步块中,命令总是一次按源代码中显示的顺序执行一次:
let flag = false function doSomething() { flag = true
这一切都很棒-如果我们所有的代码都忙于执行异步I / O。 该程序由同步代码的小块组成,这些块可以快速处理数据,例如发送到文件和流。 程序片段的代码是如此之快,以至于它不会阻止其他片段的代码的执行。 等待异步I / O结果的时间比代码执行花费的时间更多。 考虑一个小例子:
db.findOne('SELECT ... LIMIT 1', function(err, result) { if (err) return console.error(err) console.log(result) }) console.log('Running query') setTimeout(function() { console.log('Hey there') }, 1000)
对此处显示的数据库的查询可能需要大约一分钟的时间,但是启动此查询后,正在
Running query
消息将立即发送到控制台。 在这种情况下,无论执行是否完成,在请求执行一秒钟后都会显示
Hey there
消息。 我们的Node.js应用程序仅调用启动请求的函数,而不会阻止其其他代码的执行。 请求完成后,将使用回调函数将有关此情况的信息通知应用程序,然后它将收到对此请求的响应。
CPU密集型任务
如果我们需要通过JavaScript进行大量计算,该怎么办? 例如-处理存储在内存中的大量数据? 这可能导致以下事实:程序将包含同步代码的一部分,该代码的执行会花费大量时间,并阻止其他代码的执行。 想象一下,这些计算需要10秒钟。 如果我们谈论的是处理特定请求的Web服务器,这将意味着它将在至少10秒钟内无法处理其他请求。 这是一个大问题。 实际上,超过100毫秒的计算可能已经导致了此问题。
JavaScript和Node.js平台最初并不是为解决密集使用处理器资源的任务而设计的。 对于在浏览器中运行JS的情况,执行此类任务意味着用户界面上的“刹车”。 在Node.js中,这可能会限制请求平台执行新的异步I / O任务的能力,以及对与其完成相关的事件做出响应的能力。
让我们回到前面的示例。 想象一下,响应对数据库的查询,传入了数千条加密记录,这些记录必须以同步JS代码进行解密:
db.findAll('SELECT ...', function(err, results) { if (err) return console.error(err)
收到结果后,结果将在回调函数中。 此后,直到处理结束,其他JS代码都无法执行。 通常,正如已经提到的那样,由此类代码创建的系统上的负载很小,它可以快速执行分配给它的任务。 但是在这种情况下,程序会收到大量查询结果,我们仍然需要对其进行处理。 这样的事情可能要花几秒钟。 如果我们谈论的是一个可以与许多用户一起使用的服务器,这意味着他们只有在完成资源密集型操作后才能继续工作。
为什么JavaScript永远不会有线程?
鉴于以上所述,似乎要解决Node.js中的繁重计算问题,您需要添加一个新模块,该模块将允许您创建线程并对其进行管理。 没有这样的东西怎么办? 令人遗憾的是,那些使用成熟服务器平台(例如Node.js)的人没有办法很好地解决与处理大量数据相关的问题。
所有这一切都是正确的,但是如果您添加了使用JavaScript中的流的功能,这将导致这种语言的本质发生变化。 在JS中,您不能仅仅以一组新的类或函数的形式添加使用线程的功能。 为此,您需要更改语言本身。 在支持多线程的语言中,同步的概念被广泛使用。 例如,在Java中,即使
某些数字类型也不是原子
类型 。 这意味着,如果不使用同步机制来从不同的线程使用同步机制,那么所有这一切都可能导致,例如,在几个线程同时尝试更改同一变量的值之后,此类变量的几个字节将被设置为一个。流量,还有其他一些。 结果,这样的变量将包含与程序的正常操作不兼容的内容。
问题的原始解决方案:事件循环的迭代
在前一个块完成之前,Node.js不会执行事件队列中的下一个代码块。 这意味着要解决我们的问题,我们可以将其分解为由同步代码片段表示的片段,然后使用
setImmediate(callback)
形式的构造来计划这些片段的执行。 该构造函数中的
callback
函数指定的代码将在事件循环的当前迭代(刻度)任务完成后执行。 之后,使用相同的设计将下一批计算排队。 这样就不会阻塞事件的周期,同时解决了体积问题。
假设我们有一个需要处理的大型数组,而处理这样一个数组的每个元素都需要复杂的计算:
const arr = [] for (const item of arr) {
如前所述,如果我们决定在一个调用中处理整个数组,这将花费太多时间,并阻止执行另一个应用程序代码。 因此,我们将把这个大任务分成几部分,并使用
setImmediate(callback)
构造:
const crypto = require('crypto') const arr = new Array(200).fill('something') function processChunk() { if (arr.length === 0) {
现在,我们
setImmediate()
处理数组的10个元素,然后使用
setImmediate()
计划下一批计算。 这意味着如果您需要在程序中执行更多代码,则可以在处理数组片段的操作之间执行它。 为此,在示例末尾,有使用
setInterval()
代码。
如您所见,这样的代码看起来比其原始版本复杂得多。 而且该算法通常比我们的算法复杂得多,这意味着在实施该算法时,将计算分为几部分并了解其中的位置以达到正确的平衡并不容易,您需要设置
setImmediate()
计划下一个计算。 此外,现在的代码是异步的,如果我们的项目依赖于第三方库,那么我们可能无法将解决困难任务的过程分解为几个部分。
后台流程
也许上述带有
setImmediate()
方法在简单情况下可以很好地工作,但这远非理想。 此外,这里不使用线程(出于明显的原因),我们也不打算为此更改语言。 是否可以在不使用线程的情况下进行并行数据处理? 是的,这是可能的,为此,我们需要某种机制进行后台数据处理。 它是关于开始某个任务,将数据传递给该任务,以便该任务在不干扰主代码的情况下使用所需的全部内容,将所需的时间花费在所需的工作上,然后将结果返回给主要代码。 我们需要类似于以下代码片段的内容:
现实情况是,在Node.js中,您可以使用后台进程。 关键是可以使用子进程和父进程之间的消息传递机制来创建进程的分支并实现上述工作方案。 主进程可以与子进程交互,向其发送事件并从中接收事件。 此方法不使用共享内存。 进程交换的所有数据都是“克隆”的,也就是说,当一个进程对该数据实例进行更改时,这些更改对另一进程不可见。 这类似于HTTP请求-当客户端将其发送到服务器时,服务器仅接收其副本。 如果进程不使用共享内存,则意味着通过它们的同时操作,不可能创建“竞赛状态”,并且我们不需要负担处理线程的负担。 看来我们的问题已经解决了。
没错,实际上并非如此。 是的-在我们眼前,这是执行密集计算任务的解决方案之一,但这又是不完善的。 创建流程的分支是一项资源密集型操作。 完成它需要时间。 实际上,我们正在谈论从头开始创建新的虚拟机,并在增加程序消耗的内存量,这是由于进程未使用共享内存。 鉴于以上所述,可以适当地询问在完成任务之后是否有可能重新使用流程的分支。 您可以对这个问题给出肯定的答案,但是在这里您需要记住,计划将流程的分支转移到将在其中同步执行的各种资源密集型任务。 在这里可以看到两个问题:
- 尽管使用这种方法不会阻塞主进程,但后代进程仅能够顺序执行传递给它的任务。 如果我们有两个任务,其中一个要花10秒,第二个要花1秒,而我们要按此顺序完成它们,那么我们不太希望在第二个任务之前等待第一个任务完成。 由于我们正在创建流程分叉,因此我们想使用操作系统的功能来计划任务并使用处理器所有核心的计算资源。 对于那些听音乐并浏览网页的人,我们需要一种类似于在计算机上工作的东西。 为此,您可以创建两个fork进程,并在它们的帮助下组织任务的并行执行。
- 另外,如果其中一个任务导致错误结束流程,则发送给该流程的所有任务都将不被处理。
为了解决这些问题,我们需要几个派生进程,而不是一个,但是我们必须限制它们的数量,因为每个进程都占用系统资源,并且创建每个进程都需要时间。 结果,遵循支持数据库连接的系统模式,我们需要诸如现成的过程池之类的东西。 进程池管理系统在收到新任务后将使用自由进程执行它们,并且当某个进程处理了该任务后,它将能够为其分配新任务。 有一种感觉,这样的工作方案并不容易实现,实际上确实如此。 我们将使用
worker-farm软件包来实现此方案:
Worker_threads模块
那么,我们的问题解决了吗? 是的,可以说它已经解决了,但是使用这种方法,所需的内存要比我们拥有多线程解决方案时所需的内存大得多。 与进程分叉相比,线程消耗的资源少得多。 这就是为什么
worker_threads
模块出现在
worker_threads
原因
worker_threads
辅助线程在隔离的上下文中运行。 他们使用消息与主要流程交换信息。 这使我们摆脱了多线程环境所面临的“竞争条件”问题。 同时,工作流与主程序存在于同一进程中,即,与使用流程分支相比,使用这种方法所使用的内存要少得多。
此外,与工作人员一起使用,可以使用共享内存。 因此,专门为此目的,
SharedArrayBuffer
了
SharedArrayBuffer
类型的对象。 仅在程序需要对大量数据进行复杂处理的情况下,才应使用它们。 当通过消息组织工作程序与主程序之间的数据交换时,它们使您可以节省序列化和反序列化数据所需的资源。
工人工人流
如果使用版本11.7.0之前的Node.js平台,则要启用
worker_threads
模块,必须在启动
--experimental-worker
时使用
--experimental-worker
标志
--experimental-worker
此外,值得记住的是,尽管创建工作程序(例如以任何语言创建线程)都比创建进程派生所需的资源少得多,但也会给系统带来一定的负担。 也许就您而言,即使此负载也可能太大。 在这种情况下,文档建议创建一个工作池。 如果需要,当然可以创建自己的这种机制的实现,但是也许您应该在NPM注册中心中寻找合适的东西。
考虑使用工作线程的示例。 我们将有一个主文件
index.js
,其中将创建一个工作线程并将其传递一些数据进行处理。 相应的API是基于事件的,但是我将在此处使用一个Promise,该Promise会在工作者的第一条消息到达时进行解析:
如您所见,使用工作流流程机制非常简单。 即,在创建工作程序时,您需要将具有工作程序代码和数据的路径传递给工作程序设计器。 请记住,此数据是克隆的,而不是存储在共享内存中。 在启动工作人员之后,我们希望收到他的
message
,以监听
message
事件。
上面,当创建类型为
Worker
的对象时,我们向构造函数传递了带有worker代码的文件名
service.js
。 这是此文件的代码:
const { workerData, parentPort } = require('worker_threads')
工作者代码中有两件事使我们感兴趣。 首先,我们需要主应用程序传输的数据。 在我们的例子中,它们由
workerData
变量表示。 其次,我们需要一种将信息传输到主应用程序的机制。 该机制由
parentPort
对象表示,该对象具有
postMessage()
方法,通过该方法,我们可以将数据处理的结果传递给主应用程序。 就是这样。
这是一个非常简单的示例,但是使用相同的机制,您可以构建更复杂的结构。 例如,在工作流程中,如果我们的应用程序需要类似的机制,则可以向主流发送大量消息,其中包含有关数据处理状态的信息。 即使是工人,也可以分批返回数据处理结果。 例如,在工作人员忙碌的情况下(例如处理数千张图像),而您不等待所有图像都被处理而想要将处理每个图像的完成通知给主应用程序,这种情况可能会派上用场。
关于
worker_threads
模块的详细信息可以在
这里找到。
网络工作者
您可能听说过网络工作者。 它们旨在用于客户端环境,该技术已经存在了很长时间,并且
对现代浏览器
具有良好的支持 。 与Web Worker一起使用的API与Node.js模块
worker_threads
提供给我们的API不同,这都是关于他们工作环境的差异。 但是,这些技术可以解决类似的问题。 例如,可以在客户端应用程序中使用网络工作者来执行数据的加密和解密,压缩和解压缩。 在他们的帮助下,您可以处理图像,实现计算机视觉系统(例如,我们在谈论人脸识别)并解决浏览器中的其他类似问题。
总结
worker_threads
— Node.js. , , . , , , « ». , ? ,
worker_threads
, Node.js
worker-farm ,
worker_threads
, Node.js .
亲爱的读者们! Node.js-?
