一文看懂 node 多线程与多进程

537 阅读5分钟

前言

最近工作中遇到一个IO阻塞的问题,发现也有其他的小伙伴遇到过,所以就想写一篇关于node多线程与多进程的文章,希望这篇文章可以让其他的遇到同样问题的同学,有一个解题思路;

什么是线程?

先看下比较官方的解释:

线程是操作系统能够进行运算调度的最小单位,线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的。

node 流程.jpg

什么是进程?

进程Process是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器。

image.png

上图是我截得Mac 活动监视器中的进程图片;
可以理解为,我们启动的每一个服务都是一个进程,上图也可以看到每一个进程下边线程并不是单一的,所以也就是说,每一个进程下边的线程是可以有多个线程存在的;

线程与进程的区别是什么?

进程和线程的区别如下:

  • 资源占用:进程拥有独立的地址空间和系统资源,而线程共享进程的地址空间和系统资源。
  • 切换开销:进程之间的切换需要保存和恢复整个进程的上下文信息,开销比较大,而线程之间的切换只需要保存和恢复线程的上下文信息,开销比较小。
  • 通信机制:进程之间需要通过进程间通信(IPC)来交换数据和信息,而线程之间可以通过共享内存和消息传递等机制来交换数据和信息。
  • 稳定性:多线程共享进程的地址空间和资源,容易出现数据竞争等问题,而多进程独立执行任务,稳定性比较高。
  • 可扩展性:多线程可以在同一个进程内创建多个线程,比较容易实现,而多进程需要在操作系统中创建多个进程,相对比较复杂。
  • 分布式:多进程的可以在不同的机器上进行并行计算,从而实现分布式计算,而多线程只能用于单机多核分布式计算;

精简概括:

属性多进程多线程比较
资源占用进程独立,内存资源占用多资源共享,内存资源占用少多线程较好
切换开销切换复杂,开销较大切换方便,开销较小多线程较好
通信机制通信困难通信简单多线程较好
稳定性进程独立,相互不影响线程资源共享,相互影响多进程较好
可扩展性比较容易实现相对比较复杂多线程较好
分布式多机多核分布式单机多核分布式多进程较好

简单的性能实验比较

单进程单线程模式计算

// process.js
function isPrime(num) {
    if (num <= 1) {
        return false;
    }

    for (let i = 2; i <= Math.sqrt(num); i++) {
        if (num % i === 0) {
            return false;
        }
    }

    return true;
}

function primeSum(start, count) {
    const arr = [];

    for (let i = start; i <= count; i++) {
        if (isPrime(i)) {
            arr.push(i);
        }
    }

    return arr;
}

console.log(primeSum(2, 10000000).length); // 664579

image.png

# 使用time node运行可以看到执行时间CPU使用率等
$ time node process.js
# 输出结果
# 2.46s    user
# 0.05s    system
# 97%      cpu
# 2.584s    total

上边是单进程单线程运行,可以看到 CPU使用:99%,总耗时:2.538s

多进程模式计算

现在把我们的代码改造一下,改成下边这样

// more_process.js
const { fork } = require('child_process');
const cpus = require('os').cpus().length;

const totalCount = 10000000;
const addUpCount = Math.ceil((totalCount - 2) / cpus);
let start = 2;
let arr = [];

for (let i = 0; i < cpus; i++) {
    const worker = fork(`${__dirname}/child_process.js`);
    const end = start + addUpCount;
    worker.send({ start, end });
    start = end;
    worker.on('message', msg => {
        arr = arr.concat(msg.arr);
        console.log('arr', arr.length);
        worker.kill();
    });
}

// children_process.js
function isPrime(num) {
    if (num <= 1) {
        return false;
    }

    for (let i = 2; i <= Math.sqrt(num); i++) {
        if (num % i === 0) {
            return false;
        }
    }

    return true;
}

function primeSum(start, count) {
    const arr = [];

    for (let i = start; i <= count; i++) {
        if (isPrime(i)) {
            arr.push(i);
        }
    }

    return arr;
}

process.on('message', ({ start, end }) => {
    console.log({ start, end });
    const arr = primeSum(start, end);
    process.send({ arr });
});

process.on('SIGHUP', () => {
    process.exit();
});

image.png

# 使用time node运行可以看到执行时间CPU使用率等
$ time node more_process.js
# 输出结果
# 3.48s    user
# 0.29s    system
# 519%     cpu
# 0.725    total

上边数据可以发现,使用多进程计算的时候,cpu使用率:597%,总耗时:0.593s 相对于单进程模式效率从2.538s提升到了0.593s,CPU使用率也从99%提升到597%

多线程模式运行

将我们的代码再次改造下

// more_worker.js
const { Worker } = require('worker_threads');
const path = require('path');

const totalCount = 10000000;
const threadCount = +process.argv[2] || 2;
console.log(`Running with ${threadCount} threads...`);

const addUpCount = Math.ceil((totalCount - 2) / threadCount);
const workers = [];
let start = 2;
let arr = [];

for (let i = 0; i < threadCount; i++) {
    const myWorker = new Worker(path.resolve(__filename, '../children_worker.js'));
    myWorker.postMessage({ start, end: addUpCount + start });
    start += addUpCount;
    workers.push(myWorker);
}

for (const _worker of workers) {
    _worker.on('error', e => {
        console.log('发生错误', e);
    });
    _worker.on('exit', () => {
        console.log('线程退出');
    });
    _worker.on('message', msg => {
        arr = arr.concat(msg.arr);
        console.log('arr', arr.length);
        _worker.unref();
    });
}
// children_worker.js
const { parentPort } = require('worker_threads');

function isPrime(num) {
    if (num <= 1) {
        return false;
    }

    for (let i = 2; i <= Math.sqrt(num); i++) {
        if (num % i === 0) {
            return false;
        }
    }

    return true;
}

function primeSum(start, count) {
    const arr = [];

    for (let i = start; i <= count; i++) {
        if (isPrime(i)) {
            arr.push(i);
        }
    }

    return arr;
}

parentPort.on('message', ({ start, end }) => {
    console.log({ start, end });
    const arr = primeSum(start, end);
    parentPort.postMessage({ arr });
});

image.png

# 使用time node运行可以看到执行时间CPU使用率等
$ time node more_worker.js 10
# 输出结果
# 3.80s    user
# 0.11s    system
# 547%     cpu
# 0.715    total

三次实验对比

> 通过上图比较可以看出,在性能实验方面,整体多线程是要优于多进程的 > 所以在开发中,如果是有大量计算的任务可以开启多个线程辅助计算

简单的内存溢出实验比较

以下实验通过node 服务进行

单进程

// single_process_service.js
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
    const { url } = ctx.request;
    if (url === '/') {
        ctx.body = { name: 'xxx', age: 14 };
    }
    // 为了能区分,使用1兆次计算
    if (url === '/compute1') {
        let obj = {};
        for (let i = 0; i < 100000; i++) {
            obj[i] = new Array(10000000);
        }
        ctx.body = { code: 200, msg: '请求成功', data: sum };
    }
    if (url === '/compute2') {
        ctx.body = { code: 200, msg: '请求成功' };
    }
});
app.listen(4000, () => {
    console.log('http://localhost:4000/start');
});
> 从上图可以看出,我们先请求compute1接口,由于内存溢出,导致整个进程挂掉,所以导致2接口也无法请求

多进程

// more_process.js
const Koa = require('koa');
const app = new Koa();
const { fork } = require('child_process');

app.use(async ctx => {
    const { url } = ctx.request;
    if (url === '/') {
        ctx.body = { name: 'xxx', age: 14 };
    }

    if (url === '/compute1') {
        const sum = await new Promise(resolve => {
            const worker = fork(`${__dirname}/process.js`, { stdio: 'inherit', detached: true });
            worker.on('message', data => {
                resolve(data);
            });
            worker.on('error', e => {
                console.log('进程发生意外错误', e);
            });
            worker.on('exit', () => {
                console.log('进程意外退出');
            });
        });
        ctx.body = { code: 200, msg: '请求成功', data: sum };
    }
    if (url === '/compute2') {
        ctx.body = { code: 200, msg: '请求成功' };
    }
});
app.listen(4003, () => {
    console.log('http://localhost:4003/ start');
});
// children_process.js
let obj = {};
for (let i = 0; i < 100000; i++) {
    obj[i] = new Array(10000000);
}
console.log('------');

服务器日志:

/compute1请求

/compute2请求

从上图可以看出,服务器显示子进程挂掉了,但是并没有影响到主进程,所以这个时候的接口服务还是可以正常运行

多线程

// worker.js
const Koa = require('koa');
const app = new Koa();

const { Worker } = require('worker_threads');
app.use(async ctx => {
    const { url } = ctx.request;
    if (url === '/') {
        ctx.body = { name: 'xxx', age: 14 };
    }

    if (url === '/compute1') {
        const sum = await new Promise(resolve => {
            const worker = new Worker(`${__dirname}/worker2.js`);
            // 接收信息
            worker.on('message', data => {
                resolve(data);
            });
            worker.on('error', e => {
                console.error('worker2 出现意外错误', e);
                resolve(1);
            });
            worker.on('exit', () => {
                console.log('worker2 意外退出');
                resolve(1);
            });
        });
        ctx.body = { code: 200, msg: '请求成功', data: sum };
    }
    if (url === '/compute2') {
        ctx.body = { code: 200, msg: '请求成功' };
    }
});
app.listen(4001, () => {
    console.log('http://localhost:4001/start');
});

服务器日志:

/compute1请求

/compute2请求

通过上图,可以看出,多线程模式下,内存溢出导致主线程直接挂了,接口1请求失败,接口2也无法进行正常请求

结论

多线程和多进程在进行内存溢出实验中,多进程更优,因为多线程是内存共享的,一荣俱荣一损俱损,但是多进程是独立运行的,即使子进程挂掉也不会影响到主进程的服务

总结

  • 单核大量计算使用多线程模式比较好,但是需要注意代码可靠性,如果内存溢出整个进程会挂掉;
  • 多进程模式在安全性、可靠性上更佳,不会因为子进程异常导致主进程挂掉;
  • 所以在使用方面还是需要开发者自行根据业务逻辑自己判断,选择使用哪种模式;

文章参考链接:# 理解Node.js中的"多线程"