线程
线程是操作系统能够进行运算调度的最小单位,首先我们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的。
单线程
单线程就是一个进程只开一个线程
Javascript 就是属于单线程,程序顺序执行(这里暂且不提JS异步),可以想象一下队列,前面一个执行完之后,后面才可以执行,当你在使用单线程语言编码时切勿有过多耗时的同步操作,否则线程会造成阻塞,导致后续响应无法处理。你如果采用 Javascript 进行编码时候,请尽可能的利用Javascript异步操作的特性。
单线程的一些说明
-
Node.js 虽然是单线程模型,但是其基于事件驱动、异步非阻塞模式,可以应用于高并发场景,避免了线程创建、线程之间上下文切换所产生的资源开销。
-
当你的项目中需要有大量计算,CPU 耗时的操作时候,要注意考虑开启多进程来完成了。
-
Node.js 开发过程中,错误会引起整个应用退出,应用的健壮性值得考验,尤其是错误的异常抛出,以及进程守护是必须要做的。
-
单线程无法利用多核CPU,但是后来Node.js 提供的API以及一些第三方工具相应都得到了解决,文章后面都会讲到。
child_process 模块与cluster 模块总结
child_process 模块实现多进程,而cluster 模块实现主从模式创建主线程,和cluster.fork方法来创建子进程
Node.js 线程
Node.js关于单线程的误区
-
const http = require('http');
-
-
const server = http.createServer();
-
server.listen(3000,()=>{
-
process.title='程序员成长指北测试进程';
-
console.log('进程id',process.pid)
-
})
仍然看本文第一段代码,创建了http服务,开启了一个进程,都说了Node.js是单线程,所以 Node 启动后线程数应该为 1,但是为什么会开启7个线程呢?难道Javascript不是单线程不知道小伙伴们有没有这个疑问?
解释一下这个原因:
Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的。
-
主线程:编译、执行代码。
-
编译/优化线程:在主线程执行的时候,可以优化代码。
-
分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据。
-
垃圾回收的几个线程。
所以大家常说的 Node 是单线程的指的是 JavaScript 的执行是单线程的(开发者编写的代码运行在单线程环境中),但 Javascript 的宿主环境,无论是 Node 还是浏览器都是多线程的因为libuv中有线程池的概念存在的,libuv会通过类似线程池的实现来模拟不同操作系统的异步调用,这对开发者来说是不可见的。
某些异步 IO 会占用额外的线程
还是上面那个例子,我们在定时器执行的同时,去读一个文件:
-
const fs = require('fs')
-
setInterval(() => {
-
console.log(new Date().getTime())
-
}, 3000)
-
-
fs.readFile('./index.html', () => {})
线程数量变成了 11 个,这是因为在 Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集计算(Zlib,Crypto)会启用 Node 的线程池,而线程池默认大小为 4,因为线程数变成了 11。我们可以手动更改线程池默认大小:
-
process.env.UV_THREADPOOL_SIZE = 64
一行代码轻松把线程变成 71。
Libuv
Libuv 是一个跨平台的异步IO库,它结合了UNIX下的libev和Windows下的IOCP的特性,最早由Node的作者开发,专门为Node提供多平台下的异步IO支持。Libuv本身是由C++语言实现的,Node中的非苏塞IO以及事件循环的底层机制都是由libuv实现的。
IOCP,说白了 IOCP 就是一个消息队列。我们设想一下,如果事先开好 N 个线程,让它们 hold 住,将所有用户的请求都投递到一个消息队列中去。让后这 N 个线程逐一从消息队列中去取出消息并加以处理。这样一来,就可以避免对没有用户请求都开新线程,不仅减少了线程的资源,也提高了线程的利用率。
libuv架构图
在Window环境下,libuv直接使用Windows的IOCP来实现异步IO。在非Windows环境下,libuv使用多线程来模拟异步IO。
注意下面我要说的话,Node的异步调用是由libuv来支持的,以上面的读取文件的例子,读文件实质的系统调用是由libuv来完成的,Node只是负责调用libuv的接口,等数据返回后再执行对应的回调方法。
Node.js 线程创建
直到 Node 10.5.0 的发布,官方才给出了一个实验性质的模块 worker_threads 给 Node 提供真正的多线程能力。
先看下简单的 demo:
-
const {
-
isMainThread,
-
parentPort,
-
workerData,
-
threadId,
-
MessageChannel,
-
MessagePort,
-
Worker
-
} = require('worker_threads');
-
-
function mainThread() {
-
for (let i = 0; i < 5; i++) {
-
const worker = new Worker(__filename, { workerData: i });
-
worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); });
-
worker.on('message', msg => {
-
console.log(`main: receive ${msg}`);
-
worker.postMessage(msg + 1);
-
});
-
}
-
}
-
-
function workerThread() {
-
console.log(`worker: workerDate ${workerData}`);
-
parentPort.on('message', msg => {
-
console.log(`worker: receive ${msg}`);
-
}),
-
parentPort.postMessage(workerData);
-
}
-
-
if (isMainThread) {
-
mainThread();
-
} else {
-
workerThread();
-
}
上述代码在主线程中开启五个子线程,并且主线程向子线程发送简单的消息。
由于 worker_thread 目前仍然处于实验阶段,所以启动时需要增加 --experimental-worker flag,运行后观察活动监视器,开启了5个子线程
worker_thread 模块
workerthread 核心代码(地址https://github.com/nodejs/node/blob/master/lib/workerthreads.js)worker_thread 模块中有 4 个对象和 2 个类,可以自己去看上面的源码。
-
isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。
-
MessagePort: 用于线程之间的通信,继承自 EventEmitter。
-
MessageChannel: 用于创建异步、双向通信的通道实例。
-
threadId: 线程 ID。
-
Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。
-
parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null
-
workerData: 用于在主进程中向子进程传递数据(data 副本)