前言
最近工作中遇到一个IO阻塞的问题,发现也有其他的小伙伴遇到过,所以就想写一篇关于node多线程与多进程的文章,希望这篇文章可以让其他的遇到同样问题的同学,有一个解题思路;
什么是线程?
先看下比较官方的解释:
线程是操作系统能够进行运算调度的最小单位,线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的。
什么是进程?
进程Process是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器。
上图是我截得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
# 使用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();
});
# 使用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 });
});
# 使用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');
});
多进程
// 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中的"多线程"