Skip to content

Commit 440dbe6

Browse files
committed
Enable pthread_create with ASYNCIFY
This means that users of ASYNCIFY/JSPI no longer need to worry about setting `PTHREAD_POOL_SIZE`. We can do the same for pthread_join in a followup I think. See: #9910
1 parent 48b0616 commit 440dbe6

8 files changed

Lines changed: 151 additions & 119 deletions

File tree

src/lib/libpthread.js

Lines changed: 82 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -270,82 +270,83 @@ var LibraryPThread = {
270270
PThread.tlsInitFunctions.forEach((f) => f());
271271
},
272272
// Loads the WebAssembly module into the given Worker.
273-
// onFinishedLoading: A callback function that will be called once all of
274-
// the workers have been initialized and are
275-
// ready to host pthreads.
276-
loadWasmModuleToWorker: (worker) => new Promise((onFinishedLoading) => {
277-
worker.onmessage = (e) => {
278-
var d = e['data'];
279-
var cmd = d.cmd;
280-
#if PTHREADS_DEBUG
281-
dbg(`main thread: received message '${cmd}' from worker. ${d}`);
282-
#endif
283-
284-
// If this message is intended to a recipient that is not the main
285-
// thread, forward it to the target thread.
286-
if (d.targetThread && d.targetThread != _pthread_self()) {
287-
var targetWorker = PThread.pthreads[d.targetThread];
288-
if (targetWorker) {
289-
targetWorker.postMessage(d, d.transferList);
290-
} else {
291-
err(`worker sent message (${cmd}) to pthread (${d.targetThread}) that no longer exists`);
273+
// @returns: A promise the resolves once the worker has loaded the wasm module
274+
// and is ready to run a pthread.
275+
loadWasmModuleToWorker: (worker) => {
276+
worker.loaded = new Promise((onFinishedLoading) => {
277+
worker.onmessage = (e) => {
278+
var d = e['data'];
279+
var cmd = d.cmd;
280+
#if PTHREADS_DEBUG
281+
dbg(`main thread: received message '${cmd}' from worker. ${d}`);
282+
#endif
283+
284+
// If this message is intended to a recipient that is not the main
285+
// thread, forward it to the target thread.
286+
if (d.targetThread && d.targetThread != _pthread_self()) {
287+
var targetWorker = PThread.pthreads[d.targetThread];
288+
if (targetWorker) {
289+
targetWorker.postMessage(d, d.transferList);
290+
} else {
291+
err(`worker sent message (${cmd}) to pthread (${d.targetThread}) that no longer exists`);
292+
}
293+
return;
292294
}
293-
return;
294-
}
295295

296-
if (d === 'setimmediate') {
297-
// Worker wants to postMessage() to itself to implement setImmediate()
298-
// emulation.
299-
worker.postMessage(d);
300-
return;
301-
}
296+
if (d === 'setimmediate') {
297+
// Worker wants to postMessage() to itself to implement setImmediate()
298+
// emulation.
299+
worker.postMessage(d);
300+
return;
301+
}
302302

303-
switch (cmd) {
304-
case {{{ CMD_CHECK_MAILBOX }}}:
305-
checkMailbox();
306-
break;
307-
case {{{ CMD_SPAWN_THREAD }}}:
308-
spawnThread(d);
309-
break;
310-
case {{{ CMD_CLEANUP_THREAD }}}:
311-
// cleanupThread needs to be run via callUserCallback since it calls
312-
// back into user code to free thread data. Without this it's possible
313-
// the unwind or ExitStatus exception could escape here.
314-
callUserCallback(() => cleanupThread(d.thread));
303+
switch (cmd) {
304+
case {{{ CMD_CHECK_MAILBOX }}}:
305+
checkMailbox();
306+
break;
307+
case {{{ CMD_SPAWN_THREAD }}}:
308+
spawnThread(d);
309+
break;
310+
case {{{ CMD_CLEANUP_THREAD }}}:
311+
// cleanupThread needs to be run via callUserCallback since it calls
312+
// back into user code to free thread data. Without this it's possible
313+
// the unwind or ExitStatus exception could escape here.
314+
callUserCallback(() => cleanupThread(d.thread));
315315
#if MAIN_MODULE
316-
case {{{ CMD_MARK_AS_FINISHED }}}:
317-
markAsFinished(d.thread);
318-
break;
316+
case {{{ CMD_MARK_AS_FINISHED }}}:
317+
markAsFinished(d.thread);
318+
break;
319319
#endif
320-
case {{{ CMD_LOADED }}}:
320+
case {{{ CMD_LOADED }}}:
321321
#if ENVIRONMENT_MAY_BE_NODE
322-
if (ENVIRONMENT_IS_NODE && !worker.strongref) {
323-
// Once worker is loaded & idle, mark it as weakly referenced,
324-
// so that mere existence of a Worker in the pool does not prevent
325-
// Node.js from exiting the app.
326-
worker.unref();
327-
}
328-
#endif
329-
onFinishedLoading(worker);
330-
break;
322+
if (ENVIRONMENT_IS_NODE && !worker.strongref) {
323+
// Once worker is loaded & idle, mark it as weakly referenced,
324+
// so that mere existence of a Worker in the pool does not prevent
325+
// Node.js from exiting the app.
326+
worker.unref();
327+
}
328+
#endif
329+
onFinishedLoading();
330+
break;
331331
#if ENVIRONMENT_MAY_BE_NODE
332-
case {{{ CMD_UNCAUGHT_EXN }}}:
333-
// Message handler for Node.js specific out-of-order behavior:
334-
// https://github.com/nodejs/node/issues/59617
335-
// A pthread sent an uncaught exception event. Re-raise it on the main thread.
336-
worker.onerror(d.error);
337-
break;
338-
#endif
339-
case {{{ CMD_CALL_HANDLER }}}:
340-
Module[d.handler](...d.args);
341-
break;
342-
default:
343-
// The received message looks like something that should be handled by this message
344-
// handler, (since there is a e.data.cmd field present), but is not one of the
345-
// recognized commands:
346-
if (cmd) err(`worker sent an unknown command ${cmd}`);
347-
}
348-
};
332+
case {{{ CMD_UNCAUGHT_EXN }}}:
333+
// Message handler for Node.js specific out-of-order behavior:
334+
// https://github.com/nodejs/node/issues/59617
335+
// A pthread sent an uncaught exception event. Re-raise it on the main thread.
336+
worker.onerror(d.error);
337+
break;
338+
#endif
339+
case {{{ CMD_CALL_HANDLER }}}:
340+
Module[d.handler](...d.args);
341+
break;
342+
default:
343+
// The received message looks like something that should be handled by this message
344+
// handler, (since there is a e.data.cmd field present), but is not one of the
345+
// recognized commands:
346+
if (cmd) err(`worker sent an unknown command ${cmd}`);
347+
}
348+
};
349+
});
349350

350351
worker.onerror = (e) => {
351352
var message = 'worker sent an error!';
@@ -442,7 +443,9 @@ var LibraryPThread = {
442443
'workerID': worker.workerID,
443444
#endif
444445
});
445-
}),
446+
447+
return worker.loaded;
448+
},
446449

447450
#if PTHREAD_POOL_SIZE
448451
async loadWasmModuleToAllWorkers() {
@@ -729,7 +732,11 @@ var LibraryPThread = {
729732
#endif
730733
// Ask the worker to start executing its pthread entry point function.
731734
worker.postMessage(msg, threadParams.transferList);
735+
#if ASYNCIFY
736+
return worker.loaded;
737+
#else
732738
return 0;
739+
#endif
733740
},
734741

735742
_emscripten_init_main_thread_js: (tb) => {
@@ -774,6 +781,11 @@ var LibraryPThread = {
774781
// allocations from __pthread_create_js we could also remove this.
775782
__pthread_create_js__noleakcheck: true,
776783
#endif
784+
// Pthread creation is async when possible. This allows us to return to the
785+
// event loop and wait for the Worker to be created.
786+
// This is needed in browsers where synchronous worker creation is still not
787+
// possible: <BUG_LINK>
788+
__pthread_create_js__async: 'auto',
777789
__pthread_create_js__deps: ['$spawnThread', '$pthreadCreateProxied',
778790
'emscripten_has_threading_support',
779791
#if OFFSCREENCANVAS_SUPPORT

src/postamble.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ var mainArgs = undefined;
7878
var ret = entryFunction(argc, {{{ to64('argv') }}});
7979
#endif // STANDALONE_WASM
8080

81-
#if ASYNCIFY == 2 && !PROXY_TO_PTHREAD
81+
#if ASYNCIFY == 2
8282
// The current spec of JSPI returns a promise only if the function suspends
8383
// and a plain value otherwise. This will likely change:
8484
// https://github.com/WebAssembly/js-promise-integration/issues/11

test/codesize/test_codesize_minimal_pthreads.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"a.out.js": 7166,
3-
"a.out.js.gz": 3553,
2+
"a.out.js": 7174,
3+
"a.out.js.gz": 3556,
44
"a.out.nodebug.wasm": 19037,
55
"a.out.nodebug.wasm.gz": 8787,
6-
"total": 26203,
7-
"total_gz": 12340,
6+
"total": 26211,
7+
"total_gz": 12343,
88
"sent": [
99
"a (memory)",
1010
"b (exit)",

test/codesize/test_codesize_minimal_pthreads_memgrowth.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"a.out.js": 7574,
2+
"a.out.js": 7582,
33
"a.out.js.gz": 3754,
44
"a.out.nodebug.wasm": 19038,
55
"a.out.nodebug.wasm.gz": 8788,
6-
"total": 26612,
6+
"total": 26620,
77
"total_gz": 12542,
88
"sent": [
99
"a (memory)",

test/decorators.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,47 @@ def metafunc(self, mode, *args, **kwargs):
603603
return metafunc
604604

605605

606+
def with_asyncify_and_jspi(func):
607+
assert callable(func)
608+
609+
@wraps(func)
610+
def metafunc(self, jspi, *args, **kwargs):
611+
if self.get_setting('WASM_ESM_INTEGRATION'):
612+
self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY')
613+
if jspi:
614+
self.set_setting('JSPI')
615+
self.require_jspi()
616+
else:
617+
self.set_setting('ASYNCIFY')
618+
return func(self, *args, **kwargs)
619+
620+
parameterize(metafunc, {'': (False,),
621+
'jspi': (True,)})
622+
return metafunc
623+
624+
625+
def also_with_asyncify_and_jspi(func):
626+
assert callable(func)
627+
628+
@wraps(func)
629+
def metafunc(self, asyncify, *args, **kwargs):
630+
if asyncify and self.get_setting('WASM_ESM_INTEGRATION'):
631+
self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY')
632+
if asyncify == 2:
633+
self.set_setting('JSPI')
634+
self.require_jspi()
635+
elif asyncify == 1:
636+
self.set_setting('ASYNCIFY')
637+
else:
638+
assert asyncify == 0
639+
return func(self, *args, **kwargs)
640+
641+
parameterize(metafunc, {'': (0,),
642+
'asyncify': (1,),
643+
'jspi': (2,)})
644+
return metafunc
645+
646+
606647
def parameterize(func, parameters):
607648
"""Add additional parameterization to a test function.
608649

test/test_browser.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
)
4949
from decorators import (
5050
also_with_asan,
51+
also_with_asyncify_and_jspi,
5152
also_with_fetch_streaming,
5253
also_with_minimal_runtime,
5354
also_with_pthreads,
@@ -66,6 +67,7 @@
6667
skip_if,
6768
skip_if_simple,
6869
with_all_sjlj,
70+
with_asyncify_and_jspi,
6971
)
7072

7173
from tools import ports, shared, utils
@@ -3666,6 +3668,10 @@ def test_pthread_pool_size_strict(self):
36663668
expected='abort:Assertion failed: thrd_create(&t4, thread_main, NULL) == thrd_success',
36673669
cflags=['-g2', '-pthread', '-sPTHREAD_POOL_SIZE=3', '-sPTHREAD_POOL_SIZE_STRICT=2'])
36683670

3671+
@with_asyncify_and_jspi
3672+
def test_pthread_asyncify(self):
3673+
self.btest_exit('pthread/test_pthread_printf.c', cflags=['-pthread'])
3674+
36693675
def test_pthread_in_pthread_pool_size_strict(self):
36703676
# Check that it fails when there's a pthread creating another pthread.
36713677
self.btest_exit('pthread/test_pthread_create_pthread.c', cflags=['-g2', '-pthread', '-sPTHREAD_POOL_SIZE=2', '-sPTHREAD_POOL_SIZE_STRICT=2'])
@@ -3681,8 +3687,11 @@ def test_pthread_atomics(self, args):
36813687
self.btest_exit('pthread/test_pthread_atomics.c', cflags=['-O3', '-pthread', '-sPTHREAD_POOL_SIZE=8', '-g1'] + args)
36823688

36833689
# Test 64-bit atomics.
3690+
@also_with_asyncify_and_jspi
36843691
def test_pthread_64bit_atomics(self):
3685-
self.btest_exit('pthread/test_pthread_64bit_atomics.c', cflags=['-O3', '-pthread', '-sPTHREAD_POOL_SIZE=8'])
3692+
if not self.get_setting('JSPI') and not self.get_setting('ASYNCIFY'):
3693+
self.set_setting('PTHREAD_POOL_SIZE', 8)
3694+
self.btest_exit('pthread/test_pthread_64bit_atomics.c', cflags=['-O3', '-pthread'])
36863695

36873696
# Test 64-bit C++11 atomics.
36883697
@also_with_pthreads

test/test_core.py

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
)
4141
from decorators import (
4242
all_engines,
43+
also_with_asyncify_and_jspi,
4344
also_with_minimal_runtime,
4445
also_with_modularize,
4546
also_with_nodefs,
@@ -76,6 +77,7 @@
7677
with_all_eh_sjlj,
7778
with_all_fs,
7879
with_all_sjlj,
80+
with_asyncify_and_jspi,
7981
with_env_modify,
8082
)
8183

@@ -255,47 +257,6 @@ def decorated(self, dylink_reversed, *args, **kwargs):
255257
only_wasm2js = skip_if('only_wasm2js', lambda t: not t.is_wasm2js())
256258

257259

258-
def with_asyncify_and_jspi(func):
259-
assert callable(func)
260-
261-
@wraps(func)
262-
def metafunc(self, jspi, *args, **kwargs):
263-
if self.get_setting('WASM_ESM_INTEGRATION'):
264-
self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY')
265-
if jspi:
266-
self.set_setting('JSPI')
267-
self.require_jspi()
268-
else:
269-
self.set_setting('ASYNCIFY')
270-
return func(self, *args, **kwargs)
271-
272-
parameterize(metafunc, {'': (False,),
273-
'jspi': (True,)})
274-
return metafunc
275-
276-
277-
def also_with_asyncify_and_jspi(func):
278-
assert callable(func)
279-
280-
@wraps(func)
281-
def metafunc(self, asyncify, *args, **kwargs):
282-
if asyncify and self.get_setting('WASM_ESM_INTEGRATION'):
283-
self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY')
284-
if asyncify == 2:
285-
self.set_setting('JSPI')
286-
self.require_jspi()
287-
elif asyncify == 1:
288-
self.set_setting('ASYNCIFY')
289-
else:
290-
assert asyncify == 0
291-
return func(self, *args, **kwargs)
292-
293-
parameterize(metafunc, {'': (0,),
294-
'asyncify': (1,),
295-
'jspi': (2,)})
296-
return metafunc
297-
298-
299260
def also_with_wasm_workers(func):
300261
assert callable(func)
301262

tools/link.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
DEFAULT_ASYNCIFY_EXPORTS = [
6565
'main',
6666
'__main_argc_argv',
67+
'_emscripten_proxy_main',
6768
]
6869

6970
VALID_ENVIRONMENTS = {'web', 'webview', 'worker', 'node', 'shell', 'worklet'}
@@ -1694,6 +1695,14 @@ def limit_incoming_module_api():
16941695
settings.REQUIRED_EXPORTS += ['setThrew']
16951696

16961697
if settings.ASYNCIFY:
1698+
# Warn against using PTHREAD_POOL_SIZE with ASYNCIFY, since there should be no need for it.
1699+
if 'PTHREAD_POOL_SIZE' in user_settings:
1700+
diagnostics.warning('emcc', 'PTHREAD_POOL_SIZE should not be needed under ASYNCIFY')
1701+
# PTHREAD_POOL_SIZE_STRICT is completely ignored since the warning/error it controls
1702+
# does not make sense with ASYNCIFY
1703+
if 'PTHREAD_POOL_SIZE_STRICT' in user_settings:
1704+
diagnostics.warning('unused-command-line-argument', 'PTHREAD_POOL_SIZE_STRICT is ignored under ASYNCIFY')
1705+
settings.PTHREAD_POOL_SIZE_STRICT = 0
16971706
if not settings.ASYNCIFY_IGNORE_INDIRECT:
16981707
# if we are not ignoring indirect calls, then we must treat invoke_* as if
16991708
# they are indirect calls, since that is what they do - we can't see their

0 commit comments

Comments
 (0)