diff --git a/src/library_pthread.js b/src/library_pthread.js index 3ed7494a49fe6..15d6a80b09eab 100644 --- a/src/library_pthread.js +++ b/src/library_pthread.js @@ -351,17 +351,17 @@ var LibraryPThread = { if (detached) { PThread.returnWorkerToPool(worker); } -#if EXIT_RUNTIME // If building with -s EXIT_RUNTIME=0, no thread will post this message, so don't even compile it in. } else if (cmd === 'exitProcess') { // A pthread has requested to exit the whole application process (runtime). - noExitRuntime = false; +#if ASSERTIONS + err("exitProcess requested by worker"); +#endif try { exit(d['returnCode']); } catch (e) { if (e instanceof ExitStatus) return; throw e; } -#endif } else if (cmd === 'cancelDone') { PThread.returnWorkerToPool(worker); } else if (cmd === 'objectTransfer') { diff --git a/src/postamble.js b/src/postamble.js index 09417c3bdec78..6a7560df2b2ee 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -377,6 +377,26 @@ function exit(status, implicit) { return; } +#if USE_PTHREADS + if (!implicit) { + if (ENVIRONMENT_IS_PTHREAD) { +#if ASSERTIONS + err('Pthread 0x' + _pthread_self().toString(16) + ' called exit(), posting exitProcess.'); +#endif + // When running in a pthread we propagate the exit back to the main thread + // where it can decide if the whole process should be shut down or not. + // The pthread may have decided not to exit its own runtime, for example + // because it runs a main loop, but that doesn't affect the main thread. + postMessage({ 'cmd': 'exitProcess', 'returnCode': status }); + throw new ExitStatus(status); + } else { +#if ASSERTIONS + err('main thead called exit: noExitRuntime=' + noExitRuntime); +#endif + } + } +#endif + if (noExitRuntime) { #if ASSERTIONS // if exit() was called, we may warn the user if the runtime isn't actually being shut down @@ -436,9 +456,19 @@ if (Module['noInitialRun']) shouldRunNow = false; #if EXIT_RUNTIME == 0 #if USE_PTHREADS -if (!ENVIRONMENT_IS_PTHREAD) // EXIT_RUNTIME=0 only applies to default behavior of the main browser thread +// EXIT_RUNTIME=0 only applies to the default behavior of the main browser +// thread. +// The default behaviour for pthreads is always to exit once they return +// from their entry point (or call pthread_exit). If we set noExitRuntime +// to true here on pthreads they would never complete and attempt to +// pthread_join to them would block forever. +// pthreads can still choose to set `noExitRuntime` explicitly, or +// call emscripten_unwind_to_js_event_loop to extend their lifetime beyond +// their main function. See comment in src/worker.js for more. +noExitRuntime = !ENVIRONMENT_IS_PTHREAD; +#else +noExitRuntime = true; #endif - noExitRuntime = true; #endif #if USE_PTHREADS diff --git a/src/worker.js b/src/worker.js index 36f0173d15157..894c6a5c5b94d 100644 --- a/src/worker.js +++ b/src/worker.js @@ -193,8 +193,12 @@ this.onmessage = function(e) { #if STACK_OVERFLOW_CHECK Module['checkStackCookie'](); #endif -#if !MINIMAL_RUNTIME // In MINIMAL_RUNTIME the noExitRuntime concept does not apply to pthreads. To exit a pthread with live runtime, use the function emscripten_unwind_to_js_event_loop() in the pthread body. - // The thread might have finished without calling pthread_exit(). If so, then perform the exit operation ourselves. +#if !MINIMAL_RUNTIME + // In MINIMAL_RUNTIME the noExitRuntime concept does not apply to + // pthreads. To exit a pthread with live runtime, use the function + // emscripten_unwind_to_js_event_loop() in the pthread body. + // The thread might have finished without calling pthread_exit(). If so, + // then perform the exit operation ourselves. // (This is a no-op if explicit pthread_exit() had been called prior.) if (!Module['getNoExitRuntime']()) #endif @@ -213,14 +217,26 @@ this.onmessage = function(e) { throw ex; } #endif -#if MINIMAL_RUNTIME // ExitStatus not present in MINIMAL_RUNTIME - Module['PThread'].threadExit(-2); - throw ex; // ExitStatus not present in MINIMAL_RUNTIME -#else - Module['PThread'].threadExit((ex instanceof Module['ExitStatus']) ? ex.status : -2); - if (!(ex instanceof Module['ExitStatus'])) throw ex; +#if !MINIMAL_RUNTIME + if (ex instanceof Module['ExitStatus']) { + if (Module['getNoExitRuntime']()) { +#if ASSERTIONS + err('Pthread 0x' + _pthread_self().toString(16) + ' called exit(), staying alive due to noExitRuntime.'); #endif + } else { +#if ASSERTIONS + err('Pthread 0x' + _pthread_self().toString(16) + ' called exit(), calling threadExit.'); +#endif + Module['PThread'].threadExit(ex.status); + } + } + else +#endif + { + Module['PThread'].threadExit(-2); + throw ex; + } #if ASSERTIONS } else { // else e == 'unwind', and we should fall through here and keep the pthread alive for asynchronous events. diff --git a/tests/core/pthread/test_pthread_exit_runtime.c b/tests/core/pthread/test_pthread_exit_runtime.c new file mode 100644 index 0000000000000..8a6459263ff88 --- /dev/null +++ b/tests/core/pthread/test_pthread_exit_runtime.c @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +pthread_t t; + +void* thread_main_exit(void* arg) { + printf("calling exit\n"); + exit(42); +} + +int main() { + printf("main\n"); + int rc = pthread_create(&t, NULL, thread_main_exit, NULL); + assert(rc == 0); + void* thread_rtn = 0; + rc = pthread_join(t, &thread_rtn); + assert(rc == 0); +#if EXIT_RUNTIME + printf("done join -- should never get here\n"); + return 1; +#else + // Since EXIT_RUNTIME is not set the exit() in the thread is not expected to + // bring down the whole process, only itself. + printf("done join -- thread exited with %ld\n", (intptr_t)thread_rtn); +#ifdef REPORT_RESULT + REPORT_RESULT(43); +#endif + return 43; +#endif +} diff --git a/tests/core/pthread/test_pthread_exit_runtime.out b/tests/core/pthread/test_pthread_exit_runtime.out new file mode 100644 index 0000000000000..52e01040d4234 --- /dev/null +++ b/tests/core/pthread/test_pthread_exit_runtime.out @@ -0,0 +1 @@ +onExit status: 42 diff --git a/tests/core/pthread/test_pthread_exit_runtime.pre.js b/tests/core/pthread/test_pthread_exit_runtime.pre.js new file mode 100644 index 0000000000000..c6ee3d253a99a --- /dev/null +++ b/tests/core/pthread/test_pthread_exit_runtime.pre.js @@ -0,0 +1,8 @@ +Module.preRun = function() { + Module['onExit'] = function(status) { + out('onExit status: ' + status); + if (typeof reportResultToServer !== 'undefined') { + reportResultToServer('onExit status: ' + status); + } + }; +} diff --git a/tests/test_browser.py b/tests/test_browser.py index e4c7f2d6eb30d..d43dbbbe7bf6c 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -3963,6 +3963,28 @@ def test_pthread_asan(self, name, args=[]): def test_pthread_asan_use_after_free(self): self.btest(path_from_root('tests', 'pthread', 'test_pthread_asan_use_after_free.cpp'), expected='1', args=['-fsanitize=address', '-s', 'INITIAL_MEMORY=256MB', '-s', 'USE_PTHREADS', '-s', 'PROXY_TO_PTHREAD', '--pre-js', path_from_root('tests', 'pthread', 'test_pthread_asan_use_after_free.js')]) + @requires_threads + def test_pthread_exit_process(self): + args = ['-s', 'USE_PTHREADS=1', + '-s', 'PROXY_TO_PTHREAD', + '-s', 'PTHREAD_POOL_SIZE=2', + '-s', 'EXIT_RUNTIME', + '-DEXIT_RUNTIME', + '-O0'] + args += ['--pre-js', path_from_root('tests', 'core', 'pthread', 'test_pthread_exit_runtime.pre.js')] + self.btest(path_from_root('tests', 'core', 'pthread', 'test_pthread_exit_runtime.c'), expected='onExit status: 42', args=args) + + @requires_threads + def test_pthread_no_exit_process(self): + # Same as above but without EXIT_RUNTIME. In this case we don't expect onExit to + # ever be called. + args = ['-s', 'USE_PTHREADS=1', + '-s', 'PROXY_TO_PTHREAD', + '-s', 'PTHREAD_POOL_SIZE=2', + '-O0'] + args += ['--pre-js', path_from_root('tests', 'core', 'pthread', 'test_pthread_exit_runtime.pre.js')] + self.btest(path_from_root('tests', 'core', 'pthread', 'test_pthread_exit_runtime.c'), expected='43', args=args) + # Tests MAIN_THREAD_EM_ASM_INT() function call signatures. def test_main_thread_em_asm_signatures(self): self.btest(path_from_root('tests', 'core', 'test_em_asm_signatures.cpp'), expected='121', args=[]) diff --git a/tests/test_core.py b/tests/test_core.py index de9c154c8bbf3..b8e189ccd2d46 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8090,6 +8090,23 @@ def test_pthread_exceptions(self): self.emcc_args += ['-fexceptions'] self.do_run_in_out_file_test('tests', 'core', 'pthread', 'exceptions.cpp') + @node_pthreads + def test_pthread_exit_process(self): + self.set_setting('PROXY_TO_PTHREAD') + self.set_setting('PTHREAD_POOL_SIZE', '2') + self.set_setting('EXIT_RUNTIME') + self.emcc_args += ['--pre-js', path_from_root('tests', 'core', 'pthread', 'test_pthread_exit_runtime.pre.js')] + self.do_run_in_out_file_test('tests', 'core', 'pthread', 'test_pthread_exit_runtime.c', assert_returncode=42) + + @node_pthreads + @disabled('https://github.com/emscripten-core/emscripten/issues/12945') + def test_pthread_no_exit_process(self): + # Same as above but without EXIT_RUNTIME + self.set_setting('PROXY_TO_PTHREAD') + self.set_setting('PTHREAD_POOL_SIZE', '2') + self.emcc_args += ['--pre-js', path_from_root('tests', 'core', 'pthread', 'test_pthread_exit_runtime.pre.js')] + self.do_run_in_out_file_test('tests', 'core', 'pthread', 'test_pthread_exit_runtime.c', assert_returncode=43) + def test_emscripten_atomics_stub(self): self.do_run_in_out_file_test('tests', 'core', 'pthread', 'emscripten_atomics.c')