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