nodejs支持了进程之后,又支持了线程。类似浏览器端的web worker。因为nodejs是单线程的,但是底层又实现了一个线程池,接着实现了进程,又实现了线程。一下变得混乱起来,我们要了解这些功能的实现原理,才能更好地使用他。上篇大致分析了进程的原理,这一篇来讲一下线程的原理。只有了解线程的实现,才能知道什么时候应该用线程,为什么可以用线程。
线程的实现也非常复杂。虽然底层只是对线程库的封装,但是把它和nodejs原本的架构结合起来似乎就变得麻烦起来。下面开始分析创建线程的过程。分析线程实现之前,我们先看一下线程通信的实现,因为线程实现中会用到。通俗来说,他的实现类似一个管道。
1 Message代表一个消息。
2 MessagePortData是对Message操作的一个封装和对消息的承载。
3 MessagePort是代表通信的端点。
1 MessageChannel类似socket通信,他包括两个端点。定义一个MessageChannel相当于建立一个tcp连接,他首先申请两个端点(MessagePort),然后把他们关联起来。
分析完线程通信的实现,我们开始分析线程的实现。nodejs中node_worker.cc实现了线程模块的功能。我们看一下这个模块的定义。
NODE_MODULE_CONTEXT_AWARE_INTERNAL(worker, node::worker::InitWorker)
这意味着我们在js里执行下面的代码时
const exportSomething = internalBinding('worker');
拿到的对象是由node::worker::InitWorker导出的结果。所以我们看看他导出了什么(只列出核心代码)。
void InitWorker(...) {
Environment* env = Environment::GetCurrent(context);
{
// 定义一个函数模板,模板函数是Worker::New
Local<FunctionTemplate> w = env->NewFunctionTemplate(Worker::New);
// 设置一些原型方法
env->SetProtoMethod(w, "startThread", Worker::StartThread);
env->SetProtoMethod(w, "stopThread", Worker::StopThread);
// 导出这个函数
Local<String> workerString =
FIXED_ONE_BYTE_STRING(env->isolate(), "Worker");
w->SetClassName(workerString);
target->Set(env->context(),
workerString,
w->GetFunction(env->context()).ToLocalChecked()).Check();
}
// 导出额外的一些变量
env->SetMethod(target, "getEnvMessagePort", GetEnvMessagePort);
target
->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "isMainThread"),
Boolean::New(env->isolate(), env->is_main_thread()))
.Check();
}
翻译成js大概是
function New() {}
New.prototype = {
startThread,StartThread,
StopThread: StopThread,
...
}
module.exports = {
Worker: New,
getEnvMessagePort: GetEnvMessagePort,
isMainThread: true | false
}
了解了c++导出的变量,我们看看js层的封装。
class Worker extends EventEmitter {
constructor(filename, options = {}) {
super();
this[kHandle] = new Worker(url,...));
this[kPort] = this[kHandle].messagePort;
const { port1, port2 } = new MessageChannel();
this[kPublicPort] = port1;
this[kPort].postMessage({
type: messageTypes.LOAD_SCRIPT,
filename,
workerData: options.workerData,
publicPort: port2,
}, [port2]);
this[kHandle].startThread();
}
}
下面我们逐步分析上面的代码。
1 this[kHandle] = new Worker(url,…));
根据上面的分析我们知道Worker函数对应的是c++层的New函数。所以我们看看New函数做了什么 。
void Worker::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
// 忽略一系列参数处理
Worker* worker = new Worker(env,
args.This(),
url,
per_isolate_opts,
std::move(exec_argv_out),
env_vars);
}
是对Worker类的封装。
Worker::Worker(...) {
// 申请一个端点
parent_port_ = MessagePort::New(env, env->context());
// 申请一个MessagePortData类对象
child_port_data_ = std::make_unique<MessagePortData>(nullptr);
// 使得两个MessagePortDatat互相关联
MessagePort::Entangle(parent_port_, child_port_data_.get());
// 设置messagePort属性为parent_port_ 的值
object()->Set(env->context(),
env->message_port_string(),
parent_port_->object()).Check();
}
所以new Worker就是定义了一个对象,并初始化了三个属性。
2 this[kPort] = this[kHandle].messagePort;
我们看到new Worker的时候定义了messagePort这个属性。他对应c++的parent_port_ 属性。是初始化时,父线程和子线程通信的一端。另一端在子线程中维护。
3 const { port1, port2 } = new MessageChannel();
申请两个可以互相通信的端点。用户后面的线程间通信。
4 保存主线程端的端点,后续用于通信。并且给子线程发送一些信息。告诉子线程通信的端口(子线程端的)和执行的js文件名。
this[kPublicPort] = port1;
this[kPort].postMessage({
type: messageTypes.LOAD_SCRIPT,
filename,
workerData: options.workerData,
publicPort: port2,
}, [port2]);
在通信中,至少要存在两个端,假设x和y。那么x.postMessage(…),就是给y发信息。但是根据图二,我们发现只有一个MessagePort。所以上面代码中的postMessage只是把消息缓存到消息队列里(MessagePortData中)。
5 this[kHandle].startThread();
开始启动线程。根据c++层导出的变量我们知道startThread对应函数是StartThread。
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
Worker* w;
uv_thread_create_ex(&w->tid_, &thread_options, [](void* arg) {
Worker* w = static_cast<Worker*>(arg);
w->Run();
}, static_cast<void*>(w)), 0
}
创建一个线程,然后在里面执行Run函数。Run函数非常复杂,下面列出几个步骤。
1 创建一个通信端点。和图二的完成关联。这样子线程就可以处理刚才缓存的消息了。
2 处理刚才缓存的消息。把parentPort设置成父线程传过来的端点。子线程就可以和父线程通信了。
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.once('message', (message) => {
...
});
worker.postMessage('Hello, world!');
} else {
parentPort.once('message', (message) => {
parentPort.postMessage(message);
});
}
3 执行主线程的js文件(即new Worker时传进来的参数,有很多种形式)。
4 开启新的事件循环,即子线程和主线程是分开的两个事件循环。
以上就是nodejs中线程的大致原理。