通過源碼分析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中線程的大致原理。

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