Skip to content

Commit 3dc4011

Browse files
authored
Warn on blocking on the main thread (#9579)
Add a new ALLOW_BLOCKING_ON_MAIN_THREAD option, set to 1 by default. If unset, it will throw when blocking on the main thread. If 1 as in the default case, we warn in the console about possible issues and link to the docs about that. This ignores things like mutexes which are usually brief, and checks for pthread_join or pthread_cond_wait operations, which tend to be longer and run the risk of Web-specific proxying-related deadlocks. Add pthread_tryjoin_np which does a non-blocking join, which always works on the main thread. Improve the docs in this area.
1 parent a05b7e6 commit 3dc4011

File tree

10 files changed

+275
-7
lines changed

10 files changed

+275
-7
lines changed

ChangeLog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Current Trunk
2020
- `ERROR_ON_MISSING_LIBRARIES` now also applies to internal symbols that start
2121
with `emscripten_`. Prior to this change such missing symbols would result
2222
in a runtime error, not they are reported at compile time.
23+
- Pthread blocking on the main thread will now warn in the console. If
24+
`ALLOW_BLOCKING_ON_MAIN_THREAD` is unset then the warning is an error.
25+
- Add `pthread_tryjoin_np`, which is a POSIX API similar to `pthread_join`
26+
but without blocking.
2327

2428
v1.39.1: 10/30/2019
2529
-------------------

site/source/docs/porting/asyncify.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. Asyncify:
1+
.. _Asyncify:
22

33
========================
44
Asyncify

site/source/docs/porting/pthreads.rst

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,83 @@ but is unrelated. That flag does not use pthreads or SharedArrayBuffer, and
4343
instead uses a plain Web Worker to run your main program (and postMessage to
4444
proxy messages back and forth).
4545

46+
Proxying
47+
========
48+
49+
The Web allows certain operations to only happen from the main browser thread,
50+
like interacting with the DOM. As a result, various operations are proxied to
51+
the main browser thread if they are called on a background thread. See
52+
`bug 3495 <https://github.com/emscripten-core/emscripten/issues/3495>`_ for
53+
more information and how to try to work around this until then. To check which
54+
operations are proxied, you can look for the function's implementation in
55+
the JS library (``src/library_*``) and see if it is annotated with
56+
``__proxy: 'sync'`` or ``__proxy: 'async'``; however, note that the browser
57+
itself proxies certain things (like some GL operations), so there is no
58+
general way to be safe here (aside from not blocking on the main browser
59+
thread).
60+
61+
In addition, Emscripten currently has a simple model of file I/O only happening
62+
on the main application thread (as we support JS plugin filesystems, which
63+
cannot share memory); this is another set of operations that are proxied.
64+
65+
Proxying can cause problems in certain cases, see the section on blocking below.
66+
67+
Blocking on the main browser thread
68+
===================================
69+
70+
Note that in most cases the "main browser thread" is the same as the "main
71+
application thread". The main browser thread is where web pages start to run
72+
JavaScript, and where JavaScript can access the DOM (a page can also create a Web
73+
Worker, which would no longer be on the main thread). The main application
74+
thread is the one on which you started up the application (by loading the main
75+
JS file emitted by Emscripten). If you started it on the main browser thread -
76+
by it being a normal HTML page - then the two are identical. However, you can
77+
also start a multithreaded application in a worker; in that case the main
78+
application thread is that worker, and there is no access to the main browser
79+
thread.
80+
81+
The Web API for atomics does not allow blocking on the main thread
82+
(specifically, ``Atomics.wait`` doesn't work there). Such blocking is
83+
necessary in APIs like ``pthread_join`` and anything that uses a futex wait
84+
under the hood, like ``usleep()``, ``emscripten_futex_wait()``, or
85+
``pthread_mutex_lock()``. To make them work, we use a busy-wait on the main
86+
browser thread, which can make the browser tab unresponsive, and also wastes
87+
power. (On a pthread, this isn't a problem as it runs in a Web Worker, where
88+
we don't need to busy-wait.)
89+
90+
Busy-waiting on the main browser thread in general will work despite the
91+
downsides just mentioned, for things like waiting on a lightly-contended mutex.
92+
However, things like ``pthread_join`` and ``pthread_cond_wait``
93+
are often intended to block for long periods of time, and if that
94+
happens on the main browser thread, and while other threads expect it to
95+
respond, it can cause a surprising deadlock. That can happen because of
96+
proxying, see the previous section. If the main thread blocks while a worker
97+
attempts to proxy to it, a deadlock can occur.
98+
99+
The bottom line is that on the Web it is bad for the main browser thread to
100+
wait on anything else. Therefore by default Emscripten warns if
101+
``pthread_join`` and ``pthread_cond_wait`` happen on the main browser thread,
102+
and will throw an error if ``ALLOW_BLOCKING_ON_MAIN_THREAD`` is zero
103+
(whose message will point to here).
104+
105+
To avoid these problems, you can use ``PROXY_TO_PTHREAD``, which as
106+
mentioned earlier moves your ``main()`` function to a pthread, which leaves
107+
the main browser thread to focus only on receiving proxied events. This is
108+
recommended in general, but may take some porting work, if the application
109+
assumed ``main()`` was on the main browser thread.
110+
111+
Another option is to replace blocking calls with nonblocking ones. For example
112+
you can replace ``pthread_join`` with ``pthread_tryjoin_np``. This may require
113+
your application to be refactored to use asynchronous events, perhaps through
114+
:c:func:`emscripten_set_main_loop` or :ref:`Asyncify`.
115+
46116
Special considerations
47117
======================
48118

49119
The Emscripten implementation for the pthreads API should follow the POSIX standard closely, but some behavioral differences do exist:
50120

51121
- When the linker flag ``-s PTHREAD_POOL_SIZE=<integer>`` is not specified and ``pthread_create()`` is called, the new thread will not start until control is yielded back to the browser's main event loop, because the web worker cannot be created while JS or wasm code is running. This is a violation of POSIX behavior and will break common code which creates a thread and immediately joins it or otherwise synchronously waits to observe an effect such as a memory write. Using a pool creates the web workers before main is called, allowing thread creation to be synchronous.
52122

53-
- Browser DOM access is only possible on the main browser thread, and therefore things that may access the DOM, like filesystem operations (``fopen()``, etc.) or changing the HTML page's title, etc., are proxied over to the main browser thread. This proxying can generate a deadlock in a special situation that native code running pthreads does not have. See `bug 3495 <https://github.com/emscripten-core/emscripten/issues/3495>`_ for more information and how to work around this until this proxying is no longer needed in Emscripten. (To check which operations are proxied, you can look for the function's implementation in the JS library (``src/library_*``) and see if it is annotated with ``__proxy: 'sync'`` or ``__proxy: 'async'``.)
54-
55-
- When doing a futex wait, e.g. ``usleep()``, ``emscripten_futex_wait()``, or ``pthread_mutex_lock()``, we use ``Atomics.wait`` on workers, which the browser should do pretty efficiently. But that is not available on the main thread, and so we busy-wait there. Busy-waiting is not recommended because it freezes the tab, and also wastes power.
56-
57123
- The Emscripten implementation does not support `POSIX signals <http://man7.org/linux/man-pages/man7/signal.7.html>`_, which are sometimes used in conjunction with pthreads. This is because it is not possible to send signals to web workers and pre-empt their execution. The only exception to this is pthread_kill() which can be used as normal to forcibly terminate a running thread.
58124

59125
- The Emscripten implementation does also not support multiprocessing via ``fork()`` and ``join()``.

src/library_pthread.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -802,8 +802,18 @@ var LibraryPThread = {
802802
if (canceled == 2) throw 'Canceled!';
803803
},
804804

805-
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join__deps: ['_cleanup_thread', '_pthread_testcancel_js', 'emscripten_main_thread_process_queued_calls', 'emscripten_futex_wait'],
806-
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join: function(thread, status) {
805+
emscripten_check_blocking_allowed: function() {
806+
#if ASSERTIONS
807+
assert(ENVIRONMENT_IS_WEB);
808+
warnOnce('Blocking on the main thread is very dangerous, see https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread');
809+
#endif
810+
#if !ALLOW_BLOCKING_ON_MAIN_THREAD
811+
abort('Blocking on the main thread is not allowed by default. See https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread');
812+
#endif
813+
},
814+
815+
_emscripten_do_pthread_join__deps: ['_cleanup_thread', '_pthread_testcancel_js', 'emscripten_main_thread_process_queued_calls', 'emscripten_futex_wait', 'emscripten_check_blocking_allowed'],
816+
_emscripten_do_pthread_join: function(thread, status, block) {
807817
if (!thread) {
808818
err('pthread_join attempted on a null thread pointer!');
809819
return ERRNO_CODES.ESRCH;
@@ -827,6 +837,11 @@ var LibraryPThread = {
827837
err('Attempted to join thread ' + thread + ', which was already detached!');
828838
return ERRNO_CODES.EINVAL; // The thread is already detached, can no longer join it!
829839
}
840+
841+
if (block && ENVIRONMENT_IS_WEB) {
842+
_emscripten_check_blocking_allowed();
843+
}
844+
830845
for (;;) {
831846
var threadStatus = Atomics.load(HEAPU32, (thread + {{{ C_STRUCTS.pthread.threadStatus }}} ) >> 2);
832847
if (threadStatus == 1) { // Exited?
@@ -838,6 +853,9 @@ var LibraryPThread = {
838853
else postMessage({ 'cmd': 'cleanupThread', 'thread': thread });
839854
return 0;
840855
}
856+
if (!block) {
857+
return ERRNO_CODES.EBUSY;
858+
}
841859
// TODO HACK! Replace the _js variant with just _pthread_testcancel:
842860
//_pthread_testcancel();
843861
__pthread_testcancel_js();
@@ -848,6 +866,16 @@ var LibraryPThread = {
848866
}
849867
},
850868

869+
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join__deps: ['_emscripten_do_pthread_join'],
870+
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join: function(thread, status) {
871+
return __emscripten_do_pthread_join(thread, status, true);
872+
},
873+
874+
pthread_tryjoin_np__deps: ['_emscripten_do_pthread_join'],
875+
pthread_tryjoin_np: function(thread, status) {
876+
return __emscripten_do_pthread_join(thread, status, false);
877+
},
878+
851879
pthread_kill__deps: ['_kill_thread'],
852880
pthread_kill: function(thread, signal) {
853881
if (signal < 0 || signal >= 65/*_NSIG*/) return ERRNO_CODES.EINVAL;

src/settings.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,14 @@ var PTHREAD_HINT_NUM_CORES = 4;
13251325
// True when building with --threadprofiler
13261326
var PTHREADS_PROFILING = 0;
13271327

1328+
// It is dangerous to call pthread_join or pthread_cond_wait
1329+
// on the main thread, as doing so can cause deadlocks on the Web (and also
1330+
// it works using a busy-wait which is expensive). See
1331+
// https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread
1332+
// This may become set to 0 by default in the future; for now, this just
1333+
// warns in the console.
1334+
var ALLOW_BLOCKING_ON_MAIN_THREAD = 1;
1335+
13281336
// If true, add in debug traces for diagnosing pthreads related issues.
13291337
var PTHREADS_DEBUG = 0;
13301338

system/include/emscripten/threading.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ struct thread_profiler_block
372372
char name[32];
373373
};
374374

375+
// Called when blocking on the main thread. This will error if main thread
376+
// blocking is not enabled, see ALLOW_BLOCKING_ON_MAIN_THREAD.
377+
void emscripten_check_blocking_allowed(void);
378+
375379
#ifdef __cplusplus
376380
}
377381
#endif

system/lib/libc/musl/src/thread/pthread_cond_timedwait.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ int __pthread_cond_timedwait(pthread_cond_t *restrict c, pthread_mutex_t *restri
8080
int e, seq, clock = c->_c_clock, cs, shared=0, oldstate, tmp;
8181
volatile int *fut;
8282

83+
#ifdef __EMSCRIPTEN__
84+
if (pthread_self() == emscripten_main_browser_thread_id()) {
85+
emscripten_check_blocking_allowed();
86+
}
87+
#endif
88+
8389
if ((m->_m_type&15) && (m->_m_lock&INT_MAX) != __pthread_self()->tid)
8490
return EPERM;
8591

tests/pthread/main_thread_join.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2019 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#include <assert.h>
7+
#include <emscripten.h>
8+
#include <pthread.h>
9+
#include <stdio.h>
10+
11+
#include <atomic>
12+
13+
pthread_t thread;
14+
15+
std::atomic<int> tries;
16+
17+
static const int EXPECTED_TRIES = 7;
18+
19+
void loop() {
20+
void* retval;
21+
printf("try...\n");
22+
if (pthread_tryjoin_np(thread, &retval) == 0) {
23+
emscripten_cancel_main_loop();
24+
assert(tries.load() == EXPECTED_TRIES);
25+
#ifdef REPORT_RESULT
26+
REPORT_RESULT(2);
27+
#endif
28+
}
29+
tries++;
30+
}
31+
32+
void *ThreadMain(void *arg) {
33+
#ifdef TRY_JOIN
34+
// Delay to force the main thread to try and fail a few times before
35+
// succeeding.
36+
while (tries.load() < EXPECTED_TRIES) {}
37+
#endif
38+
pthread_exit((void*)0);
39+
}
40+
41+
pthread_t CreateThread() {
42+
pthread_t ret;
43+
int rc = pthread_create(&ret, NULL, ThreadMain, (void*)0);
44+
assert(rc == 0);
45+
return ret;
46+
}
47+
48+
int main() {
49+
if (!emscripten_has_threading_support()) {
50+
#ifdef REPORT_RESULT
51+
REPORT_RESULT(0);
52+
#endif
53+
printf("Skipped: Threading is not supported.\n");
54+
return 0;
55+
}
56+
57+
int x = EM_ASM_INT({
58+
onerror = function(e) {
59+
var message = e.toString();
60+
var success = message.indexOf("Blocking on the main thread is not allowed by default. See https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread") >= 0;
61+
if (success && !Module.reported) {
62+
Module.reported = true;
63+
console.log("reporting success");
64+
// manually REPORT_RESULT; we shouldn't call back into native code at this point
65+
var xhr = new XMLHttpRequest();
66+
xhr.open("GET", "http://localhost:8888/report_result?0");
67+
xhr.send();
68+
}
69+
};
70+
return 0;
71+
});
72+
73+
thread = CreateThread();
74+
#ifdef TRY_JOIN
75+
emscripten_set_main_loop(loop, 0, 0);
76+
#else
77+
int status;
78+
// This should fail on the main thread.
79+
puts("trying to block...");
80+
pthread_join(thread, (void**)&status);
81+
puts("blocked ok.");
82+
#ifdef REPORT_RESULT
83+
REPORT_RESULT(1);
84+
#endif
85+
#endif // TRY_JOIN
86+
}
87+

tests/pthread/main_thread_wait.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2019 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#include <assert.h>
7+
#include <emscripten.h>
8+
#include <pthread.h>
9+
#include <stdio.h>
10+
11+
int main() {
12+
if (!emscripten_has_threading_support())
13+
{
14+
#ifdef REPORT_RESULT
15+
REPORT_RESULT(0);
16+
#endif
17+
printf("Skipped: Threading is not supported.\n");
18+
return 0;
19+
}
20+
21+
int x = EM_ASM_INT({
22+
onerror = function(e) {
23+
var message = e.toString();
24+
var success = message.indexOf("Blocking on the main thread is not allowed by default. See https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread") >= 0;
25+
if (success && !Module.reported) {
26+
Module.reported = true;
27+
console.log("reporting success");
28+
// manually REPORT_RESULT; we shouldn't call back into native code at this point
29+
var xhr = new XMLHttpRequest();
30+
xhr.open("GET", "http://localhost:8888/report_result?0");
31+
xhr.send();
32+
}
33+
};
34+
return 0;
35+
});
36+
37+
// This should fail on the main thread.
38+
puts("trying to block...");
39+
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
40+
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
41+
pthread_cond_wait(&cv, &lock);
42+
puts("blocked ok.");
43+
#ifdef REPORT_RESULT
44+
REPORT_RESULT(1);
45+
#endif
46+
}
47+

tests/test_browser.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3620,6 +3620,24 @@ def test_pthread_64bit_cxx11_atomics(self):
36203620
for pthreads in [[], ['-s', 'USE_PTHREADS=1']]:
36213621
self.btest(path_from_root('tests', 'pthread', 'test_pthread_64bit_cxx11_atomics.cpp'), expected='0', args=opt + pthreads + ['-std=c++11'])
36223622

3623+
@parameterized({
3624+
'join': ('join',),
3625+
'wait': ('wait',),
3626+
})
3627+
@requires_threads
3628+
def test_pthread_main_thread_blocking(self, name):
3629+
print('Test that we error if not ALLOW_BLOCKING_ON_MAIN_THREAD')
3630+
self.btest(path_from_root('tests', 'pthread', 'main_thread_%s.cpp' % name), expected='0', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PTHREAD_POOL_SIZE=1', '-s', 'ALLOW_BLOCKING_ON_MAIN_THREAD=0'])
3631+
if name == 'join':
3632+
print('Test that by default we just warn about blocking on the main thread.')
3633+
self.btest(path_from_root('tests', 'pthread', 'main_thread_%s.cpp' % name), expected='1', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PTHREAD_POOL_SIZE=1'])
3634+
print('Test that tryjoin is fine, even if not ALLOW_BLOCKING_ON_MAIN_THREAD')
3635+
self.btest(path_from_root('tests', 'pthread', 'main_thread_join.cpp'), expected='2', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PTHREAD_POOL_SIZE=1', '-g', '-DTRY_JOIN', '-s', 'ALLOW_BLOCKING_ON_MAIN_THREAD=0'])
3636+
print('Test that tryjoin is fine, even if not ALLOW_BLOCKING_ON_MAIN_THREAD, and even without a pool')
3637+
self.btest(path_from_root('tests', 'pthread', 'main_thread_join.cpp'), expected='2', args=['-O3', '-s', 'USE_PTHREADS=1', '-g', '-DTRY_JOIN', '-s', 'ALLOW_BLOCKING_ON_MAIN_THREAD=0'])
3638+
print('Test that everything works ok when we are on a pthread.')
3639+
self.btest(path_from_root('tests', 'pthread', 'main_thread_%s.cpp' % name), expected='1', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PTHREAD_POOL_SIZE=1', '-s', 'PROXY_TO_PTHREAD', '-s', 'ALLOW_BLOCKING_ON_MAIN_THREAD=0'])
3640+
36233641
# Test the old GCC atomic __sync_fetch_and_op builtin operations.
36243642
@requires_threads
36253643
def test_pthread_gcc_atomic_fetch_and_op(self):

0 commit comments

Comments
 (0)