线程和过程是计算机操作系统的根底概念,在程序员中属于高频词汇,那如何了解呢?Node.js 中的过程和线程又是怎么的呢?

一、过程和线程

1.1、专业性文字定义

  • 过程(Process),过程是计算机中的程序对于某数据汇合上的一次运行流动,是零碎进行资源分配和调度的根本单位,是操作系统构造的根底,过程是线程的容器。
  • 线程(Thread),线程是操作系统可能进行运算调度的最小单位,被蕴含在过程之中,是过程中的理论运作单位。

1.2、艰深了解

以上形容比拟硬,看完可能也没看懂,还不利于了解记忆。那么咱们举个简略的例子:

假如你是某个快递站点的一名小哥,起初这个站点负责的区域住户不多,收取件都是你一个人。给张三家送完件,再去李四家取件,事件得一件件做,这叫单线程,所有的工作都得按程序执行
起初这个区域住户多了,站点给这个区域调配了多个小哥,还有个小组长,你们能够为更多的住户服务了,这叫多线程,小组长是主线程,每个小哥都是一个线程
快递站点应用的小推车等工具,是站点提供的,小哥们都能够应用,并不仅供某一个人,这叫多线程资源共享。
站点小推车目前只有一个,大家都须要应用,这叫抵触。解决的办法有很多,排队期待或者等其余小哥用完后的告诉,这叫线程同步

总公司有很多站点,各个站点的经营模式简直截然不同,这叫多过程。总公司叫主过程,各个站点叫子过程
总公司和站点之间,以及各个站点相互之间,小推车都是互相独立的,不能混用,这叫过程间不共享资源。各站点间能够通过电话等形式分割,这叫管道。各站点间还有其余协同伎俩,便于实现更大的计算工作,这叫过程间同步

还能够看看阮一峰的 过程与线程的一个简略解释。

二、Node.js 中的过程和线程

Node.js 是单线程服务,事件驱动和非阻塞 I/O 模型的语言个性,使得 Node.js 高效和轻量。劣势在于免去了频繁切换线程和资源抵触;善于 I/O 密集型操作(底层模块 libuv 通过多线程调用操作系统提供的异步 I/O 能力进行多任务的执行),然而对于服务端的 Node.js,可能每秒有上百个申请须要解决,当面对 CPU 密集型申请时,因为是单线程模式,难免会造成阻塞。

2.1、Node.js 阻塞

咱们利用 Koa 简略地搭建一个 Web 服务,用斐波那契数列办法来模仿一下 Node.js 解决 CPU 密集型的计算工作:

斐波那契数列,也称黄金分割数列,这个数列从第三项开始,每一项都等于前两项只和:0、1、1、2、3、5、8、13、21、......
// app.jsconst Koa = require('koa')const router = require('koa-router')()const app = new Koa()// 用来测试是否被阻塞router.get('/test', (ctx) => {    ctx.body = {        pid: process.pid,        msg: 'Hello World'    }})router.get('/fibo', (ctx) => {    const { num = 38 } = ctx.query    const start = Date.now()    // 斐波那契数列    const fibo = (n) => {        return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1    }    fibo(num)    ctx.body = {        pid: process.pid,        duration: Date.now() - start    }})app.use(router.routes())app.listen(9000, () => {    console.log('Server is running on 9000')})

执行 node app.js 启动服务,用 Postman 发送申请,能够看到,计算 38 次消耗了 617ms,换而言之,因为执行了一个 CPU 密集型的计算工作,所以 Node.js 主线程被阻塞了六百多毫秒。如果同时解决更多的申请,或者计算工作更简单,那么在这些申请之后的所有申请都会被提早执行。

咱们再新建一个 axios.js 用来模仿发送屡次申请,此时将 app.js 中的 fibo 计算次数改为 43,用来模仿更简单的计算工作:

// axios.jsconst axios = require('axios')const start = Date.now()const fn = (url) => {    axios.get(`http://127.0.0.1:9000/${ url }`).then((res) => {        console.log(res.data, `耗时: ${ Date.now() - start }ms`)    })}fn('test')fn('fibo?num=43')fn('test')

能够看到,当申请须要执行 CPU 密集型的计算工作时,后续的申请都被阻塞期待,这类申请一多,服务根本就阻塞卡死了。对于这种有余,Node.js 始终在补救。

2.2、master-worker

master-worker 模式是一种并行模式,核心思想是:零碎有两个及以上的过程或线程协同工作时,master 负责接管和调配并整合工作,worker 负责解决工作。

2.3、多线程

线程是 CPU 调度的一个根本单位,只能同时执行一个线程的工作,同一个线程也只能被一个 CPU 调用。如果应用的是多核 CPU,那么将无奈充分利用 CPU 的性能。

多线程带给咱们灵便的编程形式,然而须要学习更多的 Api 常识,在编写更多代码的同时也存在着更多的危险,线程的切换和锁也会减少系统资源的开销。

  • worker_threads 工作线程,给 Node.js 提供了真正的多线程能力。

worker_threads 是 Node.js 提供的一种多线程 Api。对于执行 CPU 密集型的计算工作很有用,对 I/O 密集型的操作帮忙不大,因为 Node.js 内置的异步 I/O 操作比 worker_threads 更高效。worker_threads 中的 Worker,parentPort 次要用于子线程和主线程的音讯交互。

将 app.js 略微改变下,将 CPU 密集型的计算工作交给子线程计算:

// app.jsconst Koa = require('koa')const router = require('koa-router')()const { Worker } = require('worker_threads')const app = new Koa()// 用来测试是否被阻塞router.get('/test', (ctx) => {    ctx.body = {        pid: process.pid,        msg: 'Hello World'    }})router.get('/fibo', async (ctx) => {    const { num = 38 } = ctx.query    ctx.body = await asyncFibo(num)})const asyncFibo = (num) => {    return new Promise((resolve, reject) => {        // 创立 worker 线程并传递数据        const worker = new Worker('./fibo.js', { workerData: { num } })        // 主线程监听子线程发送的音讯        worker.on('message', resolve)        worker.on('error', reject)        worker.on('exit', (code) => {            if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`))        })    })}app.use(router.routes())app.listen(9000, () => {    console.log('Server is running on 9000')})

新增 fibo.js 文件,用来解决简单计算工作:

const { workerData, parentPort } = require('worker_threads')const { num } = workerDataconst start = Date.now()// 斐波那契数列const fibo = (n) => {    return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1}fibo(num)parentPort.postMessage({    pid: process.pid,    duration: Date.now() - start})

执行上文的 axios.js,此时将 app.js 中的 fibo 计算次数改为 43,用来模仿更简单的计算工作:

能够看到,将 CPU 密集型的计算工作交给子线程解决时,主线程不再被阻塞,只需期待子线程解决实现后,主线程接管子线程返回的后果即可,其余申请不再受影响。
上述代码是演示创立 worker 线程的过程和成果,理论开发中,请应用线程池来代替上述操作,因为频繁创立线程也会有资源的开销。

线程是 CPU 调度的一个根本单位,只能同时执行一个线程的工作,同一个线程也只能被一个 CPU 调用。

咱们再回味下,本大节结尾提到的线程和 CPU 的形容,此时因为是新的线程,能够在其余 CPU 外围上执行,能够更充沛的利用多核 CPU。

2.4、多过程

Node.js 为了能充分利用 CPU 的多核能力,提供了 cluster 模块,cluster 能够通过一个父过程治理多个子过程的形式来实现集群的性能。

  • child_process 子过程,衍生新的 Node.js 过程并应用建设的 IPC 通信通道调用指定的模块。
  • cluster 集群,能够创立共享服务器端口的子过程,工作过程应用 child_process 的 fork 办法衍生。

cluster 底层就是 child_process,master 过程做总控,启动 1 个 agent 过程和 n 个 worker 过程,agent 过程解决一些公共事务,比方日志等;worker 过程应用建设的 IPC(Inter-Process Communication)通信通道和 master 过程通信,和 master 过程共享服务端口。

新增 fibo-10.js,模仿发送 10 次申请:

// fibo-10.jsconst axios = require('axios')const url = `http://127.0.0.1:9000/fibo?num=38`const start = Date.now()for (let i = 0; i < 10; i++) {    axios.get(url).then((res) => {        console.log(res.data, `耗时: ${ Date.now() - start }ms`)    })}

能够看到,只应用了一个过程,10 个申请缓缓阻塞,累计耗时 15 秒:

接下来,将 app.js 略微改变下,引入 cluster 模块:

// app.jsconst cluster = require('cluster')const http = require('http')const numCPUs = require('os').cpus().length// const numCPUs = 10 // worker 过程的数量个别和 CPU 外围数雷同const Koa = require('koa')const router = require('koa-router')()const app = new Koa()// 用来测试是否被阻塞router.get('/test', (ctx) => {    ctx.body = {        pid: process.pid,        msg: 'Hello World'    }})router.get('/fibo', (ctx) => {    const { num = 38 } = ctx.query    const start = Date.now()    // 斐波那契数列    const fibo = (n) => {        return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1    }    fibo(num)    ctx.body = {        pid: process.pid,        duration: Date.now() - start    }})app.use(router.routes())if (cluster.isMaster) {    console.log(`Master ${process.pid} is running`)        // 衍生 worker 过程    for (let i = 0; i < numCPUs; i++) {        cluster.fork()    }    cluster.on('exit', (worker, code, signal) => {        console.log(`worker ${worker.process.pid} died`)    })} else {    app.listen(9000)    console.log(`Worker ${process.pid} started`)}

执行 node app.js 启动服务,能够看到,cluster 帮咱们创立了 1 个 master 过程和 4 个 worker 过程:

通过 fibo-10.js 模仿发送 10 次申请,能够看到,四个过程解决 10 个申请耗时近 9 秒:

当启动 10 个 worker 过程时,看看成果:

仅需不到 3 秒,不过过程的数量也不是有限的。在日常开发中,worker 过程的数量个别和 CPU 外围数雷同。

2.5、多过程阐明

开启多过程不全是为了解决高并发,而是为了解决 Node.js 对于多核 CPU 利用率有余的问题。
由父过程通过 fork 办法衍生进去的子过程领有和父过程一样的资源,然而各自独立,相互之间资源不共享。通常依据 CPU 外围数来设置过程数量,因为系统资源是无限的。

三、总结

1、大部分通过多线程解决 CPU 密集型计算工作的计划都能够通过多过程计划来代替;
2、Node.js 尽管异步,然而不代表不会阻塞,CPU 密集型工作最好不要在主线程解决,保障主线程的畅通;
3、不要一味的谋求高性能和高并发,达到零碎须要即可,高效、麻利才是我的项目须要的,这也是 Node.js 轻量的特点。
4、Node.js 中的过程和线程还有很多概念在文章中提到了但没开展细讲或没提到的,比方:Node.js 底层 I/O 的 libuv、IPC 通信通道、多过程如何守护、过程间资源不共享如何解决定时工作、agent 过程等;
5、以上代码可在 https://github.com/liuxy0551/node-process-thread 查看。