Skip to content

Commit 44b2c2a

Browse files
authored
Block in dlopen until all threads have loaded the module (#18376)
Fixes: #18345
1 parent ad65236 commit 44b2c2a

22 files changed

+519
-141
lines changed

emcc.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1653,7 +1653,13 @@ def setup_pthreads(target):
16531653
]
16541654

16551655
if settings.MAIN_MODULE:
1656-
settings.REQUIRED_EXPORTS += ['_emscripten_thread_sync_code', '__dl_seterr']
1656+
settings.REQUIRED_EXPORTS += [
1657+
'_emscripten_dlsync_self',
1658+
'_emscripten_dlsync_self_async',
1659+
'_emscripten_proxy_dlsync',
1660+
'_emscripten_proxy_dlsync_async',
1661+
'__dl_seterr',
1662+
]
16571663

16581664
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [
16591665
'$exitOnMainThread',

site/source/docs/compiling/Dynamic-Linking.rst

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,11 @@ the stack trace (in an unminified build); building with
206206
Limitations
207207
-----------
208208

209-
- Chromium does not support compiling >4kB WASM on the main thread, and
210-
that includes side modules; you can use ``--use-preload-plugins`` (in
211-
``emcc`` or ``file_packager.py``) to make Emscripten compile them on
212-
startup
213-
`[doc] <https://emscripten.org/docs/porting/files/packaging_files.html#preloading-files>`__
214-
`[discuss] <https://groups.google.com/forum/#!topic/emscripten-discuss/cE3hUV3fDSw>`__.
209+
- Chromium does not support compiling >4kB WASM on the main thread, and that
210+
includes side modules; you can use ``--use-preload-plugins`` (in ``emcc`` or
211+
``file_packager.py``) to make Emscripten compile them on startup
212+
`[doc] <https://emscripten.org/docs/porting/files/packaging_files.html#preloading-files>`__
213+
`[discuss] <https://groups.google.com/forum/#!topic/emscripten-discuss/cE3hUV3fDSw>`__.
215214
- ``EM_ASM`` code defined within side modules depends on ``eval`` support are
216215
is therefore incompatible with ``-sDYNAMIC_EXECUTION=0``.
217216
- ``EM_JS`` functions defined in side modules are not yet supported.
@@ -220,10 +219,26 @@ Limitations
220219
Pthreads support
221220
----------------
222221

223-
Dynamic linking + pthreads is is still experimental. While you can link with
224-
``MAIN_MODULE`` and ``-pthread`` emscripten will produce a warning by default
225-
when you do this.
226-
227-
While load-time dynamic linking should largely work and does not have any major
228-
known issues, runtime dynamic linking (with ``dlopen()``) has limited support
229-
when used with pthreads.
222+
Dynamic linking + pthreads is is still experimental. As such, linking with both
223+
``MAIN_MODULE`` and ``-pthread`` will produce a warning.
224+
225+
While load-time dynamic linking works without any complications, runtime dynamic
226+
linking via ``dlopen``/``dlsym`` can require some extra consideration. The
227+
reason for this is that keeping the indirection function pointer table in sync
228+
between threads has to be done by emscripten library code. Each time a new
229+
library is loaded or a new symbol is requested via ``dlsym``, table slots can be
230+
added and these changes need to be mirrored on every thread in the process.
231+
232+
Changes to the table are protected by a mutex, and before any thread returns
233+
from ``dlopen`` or ``dlsym`` it will wait until all other threads are sync. In
234+
order to make this synchronization as seamless as possible, we hook into the
235+
low level primitives of `emscripten_futex_wait` and `emscirpten_yield`.
236+
237+
For most use cases all this happens under hood and no special action is needed.
238+
However, there there is one class of application that currently may require
239+
modification. If your applications busy waits, or directly uses the
240+
``atomic.waitXX`` instructions (or the clang
241+
``__builtin_wasm_memory_atomic_waitXX`` builtins) you maybe need to switch it
242+
to use ``emscripten_futex_wait`` or order avoid deadlocks. If you don't use
243+
``emscripten_futex_wait`` while you block, you could potentially block other
244+
threads that are calling ``dlopen`` and/or ``dlsym``.

src/library_promise.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,25 @@ mergeInto(LibraryManager.library, {
1212
$getPromise: function(id) {
1313
return promiseMap.get(id).promise;
1414
},
15-
emscripten_promise_create__deps: ['$promiseMap'],
16-
emscripten_promise_create__sig: 'p',
17-
emscripten_promise_create: function() {
15+
16+
$makePromise__deps: ['$promiseMap'],
17+
$makePromise: function() {
1818
var promiseInfo = {};
1919
promiseInfo.promise = new Promise((resolve, reject) => {
2020
promiseInfo.reject = reject;
2121
promiseInfo.resolve = resolve;
2222
});
23-
var id = promiseMap.allocate(promiseInfo);
23+
promiseInfo.id = promiseMap.allocate(promiseInfo);
2424
#if RUNTIME_DEBUG
25-
dbg('emscripten_promise_create: ' + id);
25+
dbg('makePromise: ' + promiseInfo.id);
2626
#endif
27-
return id;
27+
return promiseInfo;
28+
},
29+
30+
emscripten_promise_create__deps: ['$makePromise'],
31+
emscripten_promise_create__sig: 'p',
32+
emscripten_promise_create: function() {
33+
return makePromise().id;
2834
},
2935

3036
emscripten_promise_destroy__deps: ['$promiseMap'],
@@ -42,7 +48,7 @@ mergeInto(LibraryManager.library, {
4248
emscripten_promise_resolve__sig: 'vpip',
4349
emscripten_promise_resolve: function(id, result, value) {
4450
#if RUNTIME_DEBUG
45-
err('emscripten_promise_resolve: ' + id);
51+
dbg('emscripten_promise_resolve: ' + id);
4652
#endif
4753
var info = promiseMap.get(id);
4854
switch (result) {

src/library_pthread.js

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ var LibraryPThread = {
3131
$PThread__deps: ['_emscripten_thread_init',
3232
'$killThread',
3333
'$cancelThread', '$cleanupThread', '$zeroMemory',
34+
#if MAIN_MODULE
35+
'$markAsFinshed',
36+
#endif
3437
'$spawnThread',
3538
'_emscripten_thread_free_data',
3639
'exit',
@@ -97,6 +100,12 @@ var LibraryPThread = {
97100
while (pthreadPoolSize--) {
98101
PThread.allocateUnusedWorker();
99102
}
103+
#endif
104+
#if MAIN_MODULE
105+
PThread.outstandingPromises = {};
106+
// Finished threads are threads that have finished running but we not yet
107+
// joined.
108+
PThread.finishedThreads = new Set();
100109
#endif
101110
},
102111

@@ -269,6 +278,10 @@ var LibraryPThread = {
269278
spawnThread(d);
270279
} else if (cmd === 'cleanupThread') {
271280
cleanupThread(d['thread']);
281+
#if MAIN_MODULE
282+
} else if (cmd === 'markAsFinshed') {
283+
markAsFinshed(d['thread']);
284+
#endif
272285
} else if (cmd === 'killThread') {
273286
killThread(d['thread']);
274287
} else if (cmd === 'cancelThread') {
@@ -540,11 +553,20 @@ var LibraryPThread = {
540553
},
541554

542555
$cleanupThread: function(pthread_ptr) {
556+
#if PTHREADS_DEBUG
557+
dbg('cleanupThread: ' + ptrToString(pthread_ptr))
558+
#endif
543559
#if ASSERTIONS
544560
assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! cleanupThread() can only ever be called from main application thread!');
545561
assert(pthread_ptr, 'Internal Error! Null pthread_ptr in cleanupThread!');
546562
#endif
547563
var worker = PThread.pthreads[pthread_ptr];
564+
#if MAIN_MODULE
565+
PThread.finishedThreads.delete(pthread_ptr);
566+
if (pthread_ptr in PThread.outstandingPromises) {
567+
PThread.outstandingPromises[pthread_ptr].resolve();
568+
}
569+
#endif
548570
assert(worker);
549571
PThread.returnWorkerToPool(worker);
550572
},
@@ -1048,7 +1070,7 @@ var LibraryPThread = {
10481070
// Before we call the thread entry point, make sure any shared libraries
10491071
// have been loaded on this there. Otherwise our table migth be not be
10501072
// in sync and might not contain the function pointer `ptr` at all.
1051-
__emscripten_thread_sync_code();
1073+
__emscripten_dlsync_self();
10521074
#endif
10531075
// pthread entry points are always of signature 'void *ThreadMain(void *arg)'
10541076
// Native codebases sometimes spawn threads with other thread entry point
@@ -1077,6 +1099,99 @@ var LibraryPThread = {
10771099
#endif
10781100
},
10791101

1102+
#if MAIN_MODULE
1103+
_emscripten_thread_exit_joinable: function(thread) {
1104+
// Called when a thread exits and is joinable. We mark these threads
1105+
// as finished, which means that are in state where are no longer actually
1106+
// runnning, but remain around waiting to be joined. In this state they
1107+
// cannot run any more proxied work.
1108+
if (!ENVIRONMENT_IS_PTHREAD) markAsFinshed(thread);
1109+
else postMessage({ 'cmd': 'markAsFinshed', 'thread': thread });
1110+
},
1111+
1112+
$markAsFinshed: function(pthread_ptr) {
1113+
#if PTHREADS_DEBUG
1114+
dbg('markAsFinshed: ' + ptrToString(pthread_ptr));
1115+
#endif
1116+
PThread.finishedThreads.add(pthread_ptr);
1117+
if (pthread_ptr in PThread.outstandingPromises) {
1118+
PThread.outstandingPromises[pthread_ptr].resolve();
1119+
}
1120+
},
1121+
1122+
// Asynchronous version dlsync_threads. Always run on the main thread.
1123+
// This work happens asynchronously. The `callback` is called once this work
1124+
// is completed, passing the ctx.
1125+
// TODO(sbc): Should we make a new form of __proxy attribute for JS library
1126+
// function that run asynchronously like but blocks the caller until they are
1127+
// done. Perhaps "sync_with_ctx"?
1128+
_emscripten_dlsync_threads_async__sig: 'vppp',
1129+
_emscripten_dlsync_threads_async__deps: ['_emscripten_proxy_dlsync_async', 'emscripten_promise_create', '$getPromise'],
1130+
_emscripten_dlsync_threads_async: function(caller, callback, ctx) {
1131+
#if PTHREADS_DEBUG
1132+
dbg("_emscripten_dlsync_threads_async caller=" + ptrToString(caller));
1133+
#endif
1134+
#if ASSERTIONS
1135+
assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! _emscripten_dlsync_threads_async() can only ever be called from main thread');
1136+
#endif
1137+
1138+
const promises = [];
1139+
assert(Object.keys(PThread.outstandingPromises).length === 0);
1140+
1141+
// This first promise resolves once the main thread has loaded all modules.
1142+
var info = makePromise();
1143+
promises.push(info.promise);
1144+
__emscripten_dlsync_self_async(info.id);
1145+
1146+
1147+
// We then create a sequence of promises, one per thread, that resolve once
1148+
// each thread has performed its sync using _emscripten_proxy_dlsync.
1149+
// Any new threads that are created after this call will automaticaly be
1150+
// in sync because we call `__emscripten_dlsync_self` in
1151+
// invokeEntryPoint before the threads entry point is called.
1152+
for (const ptr of Object.keys(PThread.pthreads)) {
1153+
const pthread_ptr = Number(ptr);
1154+
if (pthread_ptr !== caller && !PThread.finishedThreads.has(pthread_ptr)) {
1155+
info = makePromise();
1156+
__emscripten_proxy_dlsync_async(pthread_ptr, info.id);
1157+
PThread.outstandingPromises[pthread_ptr] = info;
1158+
promises.push(info.promise);
1159+
}
1160+
}
1161+
1162+
#if PTHREADS_DEBUG
1163+
dbg('_emscripten_dlsync_threads_async: waiting on ' + promises.length + ' promises');
1164+
#endif
1165+
// Once all promises are resolved then we know all threads are in sync and
1166+
// we can call the callback.
1167+
Promise.all(promises).then(() => {
1168+
PThread.outstandingPromises = {};
1169+
#if PTHREADS_DEBUG
1170+
dbg('_emscripten_dlsync_threads_async done: calling callback');
1171+
#endif
1172+
{{{ makeDynCall('vp', 'callback') }}}(ctx);
1173+
});
1174+
},
1175+
1176+
// Synchronous version dlsync_threads. This is only needed for the case then
1177+
// the main thread call dlopen and in that case we have not choice but to
1178+
// synchronously block the main thread until all other threads are in sync.
1179+
// When `dlopen` is called from a worker, the worker itself is blocked but
1180+
// the operation its waiting on (on the main thread) can be async.
1181+
_emscripten_dlsync_threads__deps: ['_emscripten_proxy_dlsync'],
1182+
_emscripten_dlsync_threads: function() {
1183+
#if ASSERTIONS
1184+
assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! _emscripten_dlsync_threads() can only ever be called from main thread');
1185+
#endif
1186+
for (const ptr of Object.keys(PThread.pthreads)) {
1187+
const pthread_ptr = Number(ptr);
1188+
if (!PThread.finishedThreads.has(pthread_ptr)) {
1189+
__emscripten_proxy_dlsync(pthread_ptr);
1190+
}
1191+
}
1192+
},
1193+
#endif // MAIN_MODULE
1194+
10801195
$executeNotifiedProxyingQueue: function(queue) {
10811196
// Set the notification state to processing.
10821197
Atomics.store(HEAP32, queue >> 2, {{{ cDefine('NOTIFICATION_RECEIVED') }}});

system/include/emscripten/threading.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,6 @@ int emscripten_pthread_attr_settransferredcanvases(pthread_attr_t *a, const char
275275
// blocking is not enabled, see ALLOW_BLOCKING_ON_MAIN_THREAD.
276276
void emscripten_check_blocking_allowed(void);
277277

278-
// Experimental API for syncing loaded code between pthreads.
279-
void _emscripten_thread_sync_code();
280-
281278
#ifdef __cplusplus
282279
}
283280
#endif

0 commit comments

Comments
 (0)