通过源码分析nodejs线程架构

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中线程的大致原理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章