From 155931519054a247a752f0fc85f6e81062322c3c Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Thu, 12 Mar 2026 02:09:14 +0000 Subject: [PATCH 1/2] Fix Node.js exit for library builds with pthreads When building without a main function (a library build), and using pthreads to do background work, the main Node.js thread has no event loop work to keep Node.js alive. The workers are also unreferenced when they start executing, so Node.js will exit prematurely before the background thread can finish its work. To prevent this, change the worker reference logic so that we only `unref()` workers upon execution if `HAS_MAIN` is enabled. For library builds, we keep them referenced (`worker.ref()`) to keep the process alive while they are active. We also ensure idle workers returned to the pool are `unref()`'d across all Node builds (not just with `PROXY_TO_PTHREAD`) so that an idle pool doesn't leak references preventing exit. Fixes #23092 --- src/lib/libpthread.js | 8 +++--- test/core/pthread/test_pthread_exit_library.c | 26 +++++++++++++++++++ .../pthread/test_pthread_exit_library.out | 3 +++ .../pthread/test_pthread_exit_library.pre.js | 7 +++++ test/test_core.py | 7 +++++ 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 test/core/pthread/test_pthread_exit_library.c create mode 100644 test/core/pthread/test_pthread_exit_library.out create mode 100644 test/core/pthread/test_pthread_exit_library.pre.js diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 0ad90f9cd68dc..fda84d4ef6b45 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -208,11 +208,11 @@ var LibraryPThread = { // worker pool as an unused worker. worker.pthread_ptr = 0; -#if ENVIRONMENT_MAY_BE_NODE && PROXY_TO_PTHREAD +#if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { - // Once the proxied main thread has finished, mark it as weakly + // Once the worker is returned to the pool, mark it as weakly // referenced so that its existence does not prevent Node.js from - // exiting. This has no effect if the worker is already weakly + // exiting. This has no effect if the worker is already weakly // referenced. worker.unref(); } @@ -687,7 +687,7 @@ var LibraryPThread = { msg.moduleCanvasId = threadParams.moduleCanvasId; msg.offscreenCanvases = threadParams.offscreenCanvases; #endif -#if ENVIRONMENT_MAY_BE_NODE +#if ENVIRONMENT_MAY_BE_NODE && HAS_MAIN if (ENVIRONMENT_IS_NODE) { // Mark worker as weakly referenced once we start executing a pthread, // so that its existence does not prevent Node.js from exiting. This diff --git a/test/core/pthread/test_pthread_exit_library.c b/test/core/pthread/test_pthread_exit_library.c new file mode 100644 index 0000000000000..73f7f3b454401 --- /dev/null +++ b/test/core/pthread/test_pthread_exit_library.c @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include +#include + +void* worker(void* arg) { + printf("worker starting\n"); + fflush(stdout); + emscripten_thread_sleep(100); + + // proxy back to the main thread + MAIN_THREAD_ASYNC_EM_ASM({ + resolve(); + }); + return NULL; +} + +EMSCRIPTEN_KEEPALIVE +void create_thread_async() { + pthread_t thread; + int rc = pthread_create(&thread, NULL, worker, NULL); + assert(rc == 0); + pthread_detach(thread); +} diff --git a/test/core/pthread/test_pthread_exit_library.out b/test/core/pthread/test_pthread_exit_library.out new file mode 100644 index 0000000000000..74b439c12c36e --- /dev/null +++ b/test/core/pthread/test_pthread_exit_library.out @@ -0,0 +1,3 @@ +initialized +worker starting +exiting diff --git a/test/core/pthread/test_pthread_exit_library.pre.js b/test/core/pthread/test_pthread_exit_library.pre.js new file mode 100644 index 0000000000000..ab185ae8750b7 --- /dev/null +++ b/test/core/pthread/test_pthread_exit_library.pre.js @@ -0,0 +1,7 @@ +let { promise, resolve } = Promise.withResolvers(); +promise.then(() => console.log('exiting')); + +Module['onRuntimeInitialized'] = function() { + console.log('initialized'); + _create_thread_async(); +}; diff --git a/test/test_core.py b/test/test_core.py index b0a64a4940c7c..2aad268fbea65 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9164,6 +9164,13 @@ def test_pthread_keepalive(self): def test_pthread_weak_ref(self): self.do_core_test('pthread/test_pthread_weak_ref.c') + @no_asan('asan exits the runtime') + @requires_pthreads + def test_pthread_exit_library(self): + # Test that Node.js doesn't exit while there are still pthreads running when there is no main function. + self.cflags += ['--pre-js', test_file('core/pthread/test_pthread_exit_library.pre.js')] + self.do_core_test('pthread/test_pthread_exit_library.c') + @requires_pthreads def test_pthread_exit_main(self): self.do_core_test('pthread/test_pthread_exit_main.c') From 7e1a2d926cfeac746a3f119f233204228d2f478e Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Fri, 13 Mar 2026 22:18:51 +0000 Subject: [PATCH 2/2] only unref when needed --- src/lib/libpthread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index fda84d4ef6b45..c60fed2271f20 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -208,7 +208,7 @@ var LibraryPThread = { // worker pool as an unused worker. worker.pthread_ptr = 0; -#if ENVIRONMENT_MAY_BE_NODE +#if ENVIRONMENT_MAY_BE_NODE && (PROXY_TO_PTHREAD || !HAS_MAIN) if (ENVIRONMENT_IS_NODE) { // Once the worker is returned to the pool, mark it as weakly // referenced so that its existence does not prevent Node.js from