前言:昨天遇到一个worker_threads崩溃问题,经过阅读源码和调试终于找到了具体原因。不得不说阅读源码是一种非常有效的解决问题的方法。代码示例如下。index.js:constaddon=require.resolve('./build/Release/addon.node');//这使得插件不会被卸载require(addon);const{Worker}=require('worker_threads');newWorker(`require('${addon}').start();`,{eval:true});event_loop.cc:#include"event_loop.h"voidon_close(uv_handle_t*handle){deletehandle;}voidcleanup(void*data){uv_close((uv_handle_t*)data,on_close);}voidStart(constNapi::CallbackInfo&args){Napi::Envenv=args.Env();uv_loop_t*循环;v8::Isolate*isolate=v8::Isolate::GetCurrent();napi_get_uv_event_loop(env,&loop);uv_prepare_t*prepare_handle=newuv_prepare_t;uv_prepare_init(循环,prepare_handle);uv_unref((uv_handle_t*)prepare_handle);uv_prepare_start(prepare_handle,[](uv_prepare_t*handle){});node::AddEnvironmentCleanupHook(isolate,cleanup,prepare_handle);}Napi::ObjectInitialize(Napi::Envenv,Napi::Objectexports){exports.Set(Napi::String::New(env,"start"),Napi::Function::New(env,Start));returnexports;}NODE_API_MODULE(NODE_GYP_MODULE_NAME,Initialize)一般情况下,我需要在worker_threads中使用addon,然后在子线程退出时出现segmentationfault,但是在主线程中没有问题(完整代码请参考https://github.com/theanarkh/test_worker_thread)。首先分析一下上面代码的流程。当在JS层执行start时,会在循环中插入一个task。并通过AddEnvironmentCleanupHook注册一个回调,这个回调会在线程退出的时候执行,执行完start后线程就会退出,所以此时会执行AddEnvironmentCleanupHook的回调cleanup,在cleanup中调用uv_close关闭句柄,然后线程真正退出uv_run会执行一次处理uv_close的回调,从而释放内存。当执行uv_close回调时发生崩溃时会出现此问题。通过调试,发现调用uv_close时传入的回调函数地址是A,但是最后执行的时候地址变成了B,B是非法地址,导致崩溃。出现这个问题后,我就开始调试,试图找出修改地址的地方,但没有结果。最后灵光一闪想到了动态链接库被卸载的问题,然后通过断点发现确实是这样。我们通过Node.js的源码来分析这个问题。WorkerThreadData数据(这个);{储物柜储物柜(隔离_);隔离::作用域isolate_scope(isolate_);SealHandleScopeouter_seal(isolate_);DeleteFnPtrenv_;//离开工作区域执行env_.reset();autocleanup_env=OnScopeLeave([&](){isolate_->CancelTerminateExecution();env_.reset();});//初始化子线程序{HandleScopehandle_scope(isolate_);本地<上下文>上下文;{TryCatchtry_catch(isolate_);context=NewContext(isolate_);}语境::范围context_scope(context);{env_.reset(CreateEnvironment(data.isolate_data_.get(),context,std::move(argv_),std::move(exec_argv_),static_cast(environment_flags_),thread_id_,std::move(inspector_parent_handle_)));}{Mutex::ScopedLock锁(mutex_);如果(停止_)返回;this->env_=env_.get();}{如果(LoadEnvironment(env_.get(),StartExecutionCallback{}).IsEmpty())返回;}}//进入子线程事件循环{Maybeexit_code=SpinEventLoop(env_.get());互斥体::ScopedLock锁(mutex_);如果(exit_code_==0&&exit_code.IsJust()){exit_code_=exit_code.FromJust();}}}以上是子线程执行的核心逻辑,当子线程退出时,会执行OnScopeLeave的第一个函数参数,从而执行env_.reset(),进而执行FreeEnvironmentvoidFreeEnvironment(Environment*env){Isolate*isolate=env->isolate();Isolate::DisallowJavascriptExecutionScopedisallow_js(隔离,Isolate::DisallowJavascriptExecutionScope::THROW_ON_FAILURE);{HandleScope句柄范围(隔离);//对于env->context().Context::Scopecontext_scope(env->context());SealHandleScopeseal_handle_scope(隔离);环境->设置停止(真);env->stop_sub_worker_contexts();//执行AddEnvironmentCleanupHook回调env->RunCleanup();RunAtExit(环境);}MultiIsolatePlatform*platform=env->isolate_data()->platform();如果(平台!=nullptr)平台->DrainTasks(隔离);//deleteenvobjectdeleteenv;}FreeEnvironment首先通过RunCleanup执行通过AddEnvironmentCleanupHook注册的回调,回到开头的代码是执行uv_close在循环中插入一个回调。然后FreeEnvironment删除了env对象,再看env的析构函数中的相关代码。if(!is_main_thread()){for(binding::DLib&addon:loaded_addons_){addon.Close();}}如果current是子线程,析构函数会调用addon.Close()关闭动态链接库,即Addon,当addon的引用号为0时,会被卸载。因为addon只是在子线程中使用,所以addon会被卸载。这时修改了uv_close回调函数的地址。处理完env之后,再析构WorkerThreadData,在WorkerThreadData析构函数中会再次执行uv_run,处理剩下的任务。uv_run(&loop_,UV_RUN_ONCE);所以uv_close的回调会被执行,因为此时回调函数的地址被修改为非法,导致崩溃。除了这个问题,子线程在退出前还会检查循环,如果有任务没有关闭,也会导致线程崩溃。voidCheckedUvLoopClose(uv_loop_t*loop){if(uv_loop_close(loop)==0)返回;PrintLibuvHandleInformation(循环,标准错误);fflush(标准错误);//最后,中止。CHECK(0&&"uv_loop_close()whilehavingopenhandles");}再看看uv_loop_close:intuv_loop_close(uv_loop_t*loop){QUEUE*q;uv_handle_t*h;如果(uv__has_active_reqs(loop))返回UV_EBUSY;QUEUE_FOREACH(q,&loop->handle_queue){h=QUEUE_DATA(q,uv_handle_t,handle_queue);如果(!(h->flags&UV_HANDLE_INTERNAL))返回UV_EBUSY;}uv__loop_close(loop);if(loop==default_loop_ptr)default_loop_ptr=NULL;return0;}的时间,终于成功找到了入口点的问题,通过源码深入理解了流程。源代码是学习一门技术非常重要的资料。