03月29, 2018

nodejs 多线程编程

nodejs child_process 模块多线程编程

通常情况下,node是采用了一个单线程在执行,但是单线程在抛出异常时就会down掉,这显然不符合后端程序的要求,加之现代电脑基本是多核心处理器,为了更好的利用多核处理器,提高程序性能node也提供了进程child_process模块。在这之前我们来看看node中进程对象包含那些信息。

process 对象

process的属性

var process = require("process");

console.log(`启动参数:${process.argv}`)
console.log(`node安装路径:${process.execPath}`)
console.log(`node版本:${process.version}`)
console.log(`运行平台:${process.platform}`)
console.log(`进程pid:${process.pid}`)
console.log(`窗口标题:${process.title}`)
console.log(`处理器架构:${process.arch}`)
console.log(`环境变量:${JSON.stringify(process.env)}`)
console.log(`V8配置:${JSON.stringify(process.config)}`)

let memoryUsage = process.memoryUsage();
console.log(`node程序消耗内存:${(memoryUsage.rss)/1024/1024} MB`)
console.log(`V8 分配内存:${memoryUsage.heapTotal/1024/1024} MB`)
console.log(`V8 消耗内存:${memoryUsage.heapUsed/1024/1024} MB`)

process的事件

process 对象是 EventEmitter 的实例。

beforeExit 事件

当 Node.js 的事件循环数组已经为空,并且没有额外的工作被添加进来,事件 'beforeExit' 会被触发。而显示手动调用用process.exit()或者程序抛出异常事,不会触发该事件。

process.on('beforeExit',function (code) {
    console.log(`程序正常结束1 code: ${code}`)

});

process.on('beforeExit',function (code) {
    console.log(`程序正常结束2 code: ${code}`)
    process.exit();
});

然后在该方法里头我们应该手动的调用退出方法退出,不然就会一直循环执行,所有监听该事件的方法。个人觉得是一个非常奇怪的设计,按理说根据其名字的意思这种事件的监听方法应该只能被调用一次才对,但是作者显然是为了能多个监听才设计成需要手动退出的方法。也许有其他用意吧!

disconnect 事件

如果 Node.js 进程是由 IPC 通道的方式创建的,当 IPC 通道关闭时,会触发'disconnect'事件。当子线程调用disconnect()方法时,线程与父线程链接断开时,触发此事件。这个事件应该是属于child_process里头的。

exit 事件

进程退出之前会触发exit。实际测试时,调用process.abort()不会触发该事件。

  • 程序正常退出会触发
  • 调用process.exit()会触发。
  • 抛出异常也会触发

code 在exit查看详情

process.on('exit',function (code) {
    console.log(`程序正常结束 code: ${code}`)

});

process.exit();//触发
throw new Error();//触发

process.abort();//不触发

'exit' 事件监听器的回调函数,只允许包含同步操作。异步操作不会执行。

process.on('exit', (code) => {
  setTimeout(() => {
    console.log('该函数不会被执行');
  }, 0);
});

message 事件

这个事件是子线程调用send()方法事触发的事件,也是属于child_process里头的。

uncaughtException 事件

如果 Javascript 未捕获的异常,沿着代码调用路径反向传递回事件循环,会触发 'uncaughtException' 事件。 Node.js 默认情况下会将这些异常堆栈打印到 stderr 然后进程退出。 为 'uncaughtException' 事件增加监听器会覆盖上述默认行为。作为一个web服务器一次请求出错就down掉是不行的,我们可以利用这个事件,覆盖默认方法,只打印,不结束程序。

const http = require("http");

let app = http.createServer();

app.on('request',function (req, res) {
    res.end("hello world !");
    throw new Error();
});

process.on('uncaughtException',function (err) {
    console.trace(err);
})

let port = 3000;
app.listen(port,function () {
    let memoryUsage = process.memoryUsage();
    console.log(`node程序消耗内存:${(memoryUsage.rss)/1024/1024} MB`)
    console.log(`V8 分配内存:${memoryUsage.heapTotal/1024/1024} MB`)
    console.log(`V8 消耗内存:${memoryUsage.heapUsed/1024/1024} MB`)
});
console.log(`start with ${port}`);
console.log(`pid ${process.pid}`)

rejectionHandled 和 unhandledRejection 事件

官方原话:

如果有 Promise 被 rejected,并且此 Promise在 Nodje.js 事件循环的下次轮询及之后期间,被绑定了一个错误处理器(例如使用 promise.catch()),会触发 'rejectionHandled' 事件。>

简单的来说就是一个promise对象调用reject,并且抛了异常,而异常的拦截方式是在未来发生的,此时uncaughtException事件就不会触发,所以增加触发一个rejectionHandled来处理该Promise对象和一个unhandledRejection事件来处理这个错误。

'use strict';

// 内置P romise
var p = (new Promise(function(resolve, reject){
    reject(new Error('Error from promise by reject'));
    // 或者通过 throw 的方式抛出,效果相同
    // throw new Error('Error from promise by throw');

}));

// 或者在 then 通过 throw 抛出错误,也有同样效果
/**
 var p = (new Promise(function(resolve){
    resolve('Data');
}))
 .then(function(res){
    console.info('Receive: ', res);
    throw new Error('Error from promise by throw');
});
 */

process.on('uncaughtException', function(e){
    console.log('uncaughtException:', e.message);
});

/*
 这里被注释时程序会报错
process.on('unhandledRejection', (reason, p) => {
    console.log('unhandledRejection:', reason.message);
});
*/

process.on('rejectionHandled', (p) => {
    console.log('rejectionHandled:', p.toString());
});

setTimeout(function(){
    p.catch(function(e){
        console.error('p.catch', e.message);
    });
}, 1e3);

warning 事件

任何时候Node.js发出进程告警,都会触发'warning'事件。 用于日志输出吧。可已手动调用process.emitWarning()触发警告

process.on('warning',function (warning) {
    console.log(`程序警告 code: ${warning}`)

});

process.emitWarning("触发警告");

方法

process.abort()

process.abort()方法会使Node.js进程立即结束,并生成一个core文件。这个方法应该是操作虚拟机层面直接强制结束。不会触发exitbeforeExit

process.chdir(directory)

process.chdir()方法变更Node.js进程的当前工作目录,如果变更目录失败会抛出异常(例如,如果指定的目录不存在)。

console.log(process.cwd())
process.chdir('../')
console.log(process.cwd())

process.cwd()

process cwd() 方法返回 Node.js 进程当前工作的目录。

console.log('当前目录的路径: ${process.cwd()}');

process.disconnect()

这个方法会触发disconnect事件,是属于child_process里头的。

process.emitWarning(warning[, options])(8.0.0)

这个方法可用于发出定制的或应用特定的进程警告,可以使用'warning'事件监听。

process.exit(code)

退出进程,触发'exit'事件。 code:

  • 1 未捕获异常 - 有一个未被捕获的异常, 并且没被一个 domain 或 an 'uncaughtException' 事件处理器处理。
  • 2 - 未被使用 (Bash为防内部滥用而保留)
  • 3 内部JavaScript 分析错误 - Node.js的内部的JavaScript源代码 在引导进程中导致了一个语法分析错误。 这是非常少见的, 一般只会在开发Node.js本身的时候出现。
  • 4 内部JavaScript执行失败 - 引导进程执行Node.js的内部的JavaScript源代码时,返回函数值失败。 这是非常少见的, 一般只会在开发Node.js本身的时候出现。
  • 5 致命错误 - 在V8中有一个致命的错误. 比较典型的是以FATALERROR为前缀从stderr打印出来的消息。
  • 6 非函数的内部异常处理 - 发生了一个内部异常,但是内部异常处理函数 被设置成了一个非函数,或者不能被调用。
  • 7 内部异常处理运行时失败 - 有一个不能被捕获的异常。 在试图处理这个异常时,处理函数本身抛出了一个错误。 这是可能发生的, 比如, 如果一个 'uncaughtException' 或者 domain.on('error') 处理函数抛出了一个错误。
  • 8 - 未被使用. 在之前版本的Node.js, 退出码8有时候表示一个未被捕获的异常。
  • 9 - 不可用参数也许是某个未知选项没有确定,或者没给必需要的选项填值。
  • 10 内部JavaScript运行时失败 - 调用引导函数时, 引导进程执行Node.js的内部的JavaScript源代码抛出错误。 这是非常少见的, 一般只会在开发Node.js本身的时候出现。
  • 12 不可用的调试参数 - --inspect 和/或 --inspect-brk 选项已设置,但选择的端口号无效或不可用。
  • 128 退出信号 - 如果Node.js的接收信号致命诸如 SIGKILL 或 SIGHUP,那么它的退出代码将是 128 加上信号的码值。 这是POSIX的标准做法,因为退出码被定义为7位整数,并且信号退出设置高位,然后包含信号码值。

process.hrtime([time])

  • time 上一次调用process.hrtime()的结果

process.hrtime()方法返回当前时间以[seconds, nanoseconds] tuple Array表示的高精度解析值

process.kill(pid[, signal])

实测官方例子报错,百度查了一下说是windows不可用!呵呵�!

process.on('SIGHUP', () => {
    console.log('Got SIGHUP signal.');
});

setTimeout(() => {
    console.log('Exiting.');
    process.exit(0);
}, 100);

process.kill(process.pid, 'SIGHUP');

subprocess.send(message[, callback])

这个方法会触发message事件,也放到child_process里头再说。

nextTick (重要函数还需要补充)

process.nextTick()方法将 callback 添加到"next tick 队列"。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

// nextTick 回调函数在下句代码执行完后执行
function foo() {
    console.log('foo')
}
process.nextTick(foo)
function bar() {
console.log('bar')
console.log('bar1')

/*
    bar
    bar1
    foo
*/

child_process

child_process提供了三种创建子进程的方式

  • exec: 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式返回。
  • spawn: 使用指定的命令行参数创建新进程。
  • fork: 是 spawn()的特殊形式,用于在子进程中运行的模块,如 fork('./son.js') 相当于 spawn('node', ['./son.js']) 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。
// master.js
const childProcess = require('child_process');

function exec(i) {
    let workerProcess = childProcess.exec('node support.js ' + i, function (err, stdout, stderr) {
        if (err) {
            console.trace(err)
        }
        console.log(`stdout: ${stdout}`);
        console.log(`stderr: ${stderr}`);
    });

    workerProcess.on('exit', function (code) {
        console.log(`子进程已经退出: ${code}`)
    })
};

function spawn(i) {
    let workerProcess = childProcess.spawn('node',['support.js',i]);

    workerProcess.stdout.on('data',function (data) {
        console.log(`stdout:${data}`)
    });

    workerProcess.stderr.on('data',function (data) {
        console.log(`stderr:${data}`)
    });

    workerProcess.on('close',function (code) {
        console.log(`子进程已经退出: ${code}`)
    })

}

function fork(i) {
    let workerProcess = childProcess.fork('support.js',[i]);
    workerProcess.on('close',function (code) {
        console.log(`子进程已经退出: ${code}`)
    });

    workerProcess.on('disconnect',function (code) {
        console.log(`管道关闭`)
    })
}

for (let i = 0; i < 3; i++) {
    exec(i);
    // spawn(i);
    // fork(i);
}
// woker.js
console.log(`进程 ${process.argv[2]} 执行`)

send()方法 和 'message'事件

通过fork方法创建的子线程回和父线程建立一个通道,父子线程可以通过这个通道来进行数据通讯。

//master.js
const childProcess = require('child_process');

let worker = childProcess.fork('worker.js',['worker']);
worker.on('close',function (code) {
    console.log(`${new Date().getTime()} 子进程已经退出: ${code}`)
});

worker.on('message', function(m){
    console.log(`${new Date().getTime()} message from worker: ${JSON.stringify(m)}`);
});

worker.send({from: 'master'},function () {
    console.log(`${new Date().getTime()} master send 的 callback`)
});

console.log(`${new Date().getTime()} master中worker pid ${worker.pid}`)

console.log(`${new Date().getTime()} master pid ${process.pid}`)
//worker.js
console.log(`${new Date().getTime()} 进程 ${process.argv[2]} 执行`)
process.on('message', function(m){
    console.log(`${new Date().getTime()} message from master: + ${JSON.stringify(m)} `);
    process.exit();
});

process.send({from: 'worker'},function () {
    console.log(`${new Date().getTime()} worker send 的 callback`)
});
console.log(`${new Date().getTime()} worker pid ${process.pid}` )

在master线程中,调用worker.send()方法,worker会把消息带到自己的线程中,这里调用方式感觉非常操蛋,就像父亲要跟儿子打电话,然后把儿子叫过来,让儿子拿父亲的手机给儿子的手机打电话。感觉要是使用process.send(worker,msg)的方式更好理解一些。然后我给每一行打印加上了时间戳,打印的结果非常郁闷,嗯嗯,这很nodejs,我也懒得去细细验证分析了,最终目的达到了就行了。 打印结果:

1522309161138 master中worker pid 22656
1522309161140 master pid 1520
1522309161140 master send 的 callback
1522309161393 进程 worker 执行
1522309161397 message from worker: {"from":"worker"}
1522309161397 worker pid 22656
1522309161398 worker send 的 callback
1522309161402 message from master: + {"from":"master"} 
1522309161417 子进程已经退出: 0

disconnect()方法 和 disconnect事件

在调用disconnect()时会断开连接,此时会触发disconnect事件。

//master.js
const childProcess = require('child_process');

let worker = childProcess.fork('worker-disconnect.js',['worker']);
worker.on('close',function (code) {
    console.log(`${new Date().getTime()} 子进程已经退出: ${code}`)
});

worker.on('disconnect', function(m){
    console.log(`${new Date().getTime()} disconnect with worker`);
});


console.log(`${new Date().getTime()} master中worker pid ${worker.pid}`)
console.log(`${new Date().getTime()} master pid ${process.pid}`)

//断开连接 和在worker中断开作用差不多
// worker.disconnect();
//worker.js
console.log(`${new Date().getTime()} 进程 ${process.argv[2]} 执行`)
process.on('disconnect', function(m){
    console.log(`${new Date().getTime()} disconnect with master`);
    process.exit();
});


console.log(`${new Date().getTime()} worker pid ${process.pid}` )
//断开连接
process.disconnect()
1522639674721 master中worker pid 5116
1522639674724 master pid 8824
1522639674998 进程 worker 执行
1522639675000 worker pid 5116
1522639675002 disconnect with worker
1522639675002 disconnect with master
1522639675021 子进程已经退出: 0

参考:

https://zhuanlan.zhihu.com/p/30047612

本文链接:https://www.qiangshuidiyu.xin/post/process .html

-- EOF --

Comments