Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0263dae
wip [ci skip] need link to good length docs page
kripken Oct 2, 2019
b8f47c0
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 4, 2019
9197b79
[ci skip]
kripken Oct 4, 2019
ac6d56c
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 4, 2019
37d78fb
wip [ci skip]
kripken Oct 4, 2019
68e1a88
[ci skip]
kripken Oct 4, 2019
27b72cc
test
kripken Oct 4, 2019
2e5e63e
more tests
kripken Oct 4, 2019
905e668
more
kripken Oct 4, 2019
b24df05
fix
kripken Oct 4, 2019
37f275e
wip [ci skip]
kripken Oct 4, 2019
12c1915
fix
kripken Oct 4, 2019
f254984
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 4, 2019
479058c
text
kripken Oct 4, 2019
b366dc8
fix
kripken Oct 4, 2019
5ae5bbe
fix typo
kripken Oct 5, 2019
586cc93
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 14, 2019
b21d2c7
feedback
kripken Oct 14, 2019
9d8c79b
docs
kripken Oct 14, 2019
7a0dc20
support pthread_tryjoin_np
kripken Oct 14, 2019
ec5c4ad
document pthread_tryjoin_np
kripken Oct 14, 2019
6247193
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 17, 2019
b34922c
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 18, 2019
6000e20
emscripten_block_on_main_thread => emscripten_check_blocking_allowed
kripken Oct 18, 2019
3529e3f
mention pthread_tryjoin_np in changelog [ci skip]
kripken Oct 18, 2019
9f34886
Merge branch 'incoming' into pj
kripken Oct 21, 2019
18ffe28
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 28, 2019
fa2882f
just warn for now
kripken Oct 28, 2019
3158218
flake8
kripken Oct 29, 2019
a59669f
Merge remote-tracking branch 'origin/incoming' into pj
kripken Oct 30, 2019
a0ebf2f
feedback [ci skip]
kripken Oct 30, 2019
b3bfc83
tryjoin without a pool
kripken Oct 30, 2019
345e0e7
Make sure we test failed tryjoins
kripken Oct 30, 2019
2bc7ce4
text nit [ci skip]
kripken Oct 30, 2019
24318b2
more
kripken Oct 31, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Current Trunk
- `ERROR_ON_MISSING_LIBRARIES` now also applies to internal symbols that start
with `emscripten_`. Prior to this change such missing symbols would result
in a runtime error, not they are reported at compile time.
- Pthread blocking on the main thread will now warn in the console. If
`ALLOW_BLOCKING_ON_MAIN_THREAD` is unset then the warning is an error.
- Add `pthread_tryjoin_np`, which is a POSIX API similar to `pthread_join`
but without blocking.

v1.39.1: 10/30/2019
-------------------
Expand Down
2 changes: 1 addition & 1 deletion site/source/docs/porting/asyncify.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.. Asyncify:
.. _Asyncify:

========================
Asyncify
Expand Down
74 changes: 70 additions & 4 deletions site/source/docs/porting/pthreads.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,83 @@ but is unrelated. That flag does not use pthreads or SharedArrayBuffer, and
instead uses a plain Web Worker to run your main program (and postMessage to
proxy messages back and forth).

Proxying
========

The Web allows certain operations to only happen from the main browser thread,
like interacting with the DOM. As a result, various operations are proxied to
the main browser thread if they are called on a background thread. See
`bug 3495 <https://github.com/emscripten-core/emscripten/issues/3495>`_ for
more information and how to try to work around this until then. 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'``; however, note that the browser
itself proxies certain things (like some GL operations), so there is no
general way to be safe here (aside from not blocking on the main browser
thread).

In addition, Emscripten currently has a simple model of file I/O only happening
on the main application thread (as we support JS plugin filesystems, which
cannot share memory); this is another set of operations that are proxied.

Proxying can cause problems in certain cases, see the section on blocking below.

Blocking on the main browser thread
===================================

Note that in most cases the "main browser thread" is the same as the "main
application thread". The main browser thread is where web pages start to run
JavaScript, and where JavaScript can access the DOM (a page can also create a Web
Worker, which would no longer be on the main thread). The main application
thread is the one on which you started up the application (by loading the main
JS file emitted by Emscripten). If you started it on the main browser thread -
by it being a normal HTML page - then the two are identical. However, you can
also start a multithreaded application in a worker; in that case the main
application thread is that worker, and there is no access to the main browser
thread.

The Web API for atomics does not allow blocking on the main thread
(specifically, ``Atomics.wait`` doesn't work there). Such blocking is
necessary in APIs like ``pthread_join`` and anything that uses a futex wait
under the hood, like ``usleep()``, ``emscripten_futex_wait()``, or
``pthread_mutex_lock()``. To make them work, we use a busy-wait on the main
browser thread, which can make the browser tab unresponsive, and also wastes
power. (On a pthread, this isn't a problem as it runs in a Web Worker, where
we don't need to busy-wait.)

Busy-waiting on the main browser thread in general will work despite the
downsides just mentioned, for things like waiting on a lightly-contended mutex.
However, things like ``pthread_join`` and ``pthread_cond_wait``
are often intended to block for long periods of time, and if that
happens on the main browser thread, and while other threads expect it to
Comment thread
kripken marked this conversation as resolved.
respond, it can cause a surprising deadlock. That can happen because of
proxying, see the previous section. If the main thread blocks while a worker
attempts to proxy to it, a deadlock can occur.

The bottom line is that on the Web it is bad for the main browser thread to
wait on anything else. Therefore by default Emscripten warns if
``pthread_join`` and ``pthread_cond_wait`` happen on the main browser thread,
and will throw an error if ``ALLOW_BLOCKING_ON_MAIN_THREAD`` is zero
(whose message will point to here).

To avoid these problems, you can use ``PROXY_TO_PTHREAD``, which as
mentioned earlier moves your ``main()`` function to a pthread, which leaves
the main browser thread to focus only on receiving proxied events. This is
recommended in general, but may take some porting work, if the application
assumed ``main()`` was on the main browser thread.

Another option is to replace blocking calls with nonblocking ones. For example
you can replace ``pthread_join`` with ``pthread_tryjoin_np``. This may require
your application to be refactored to use asynchronous events, perhaps through
:c:func:`emscripten_set_main_loop` or :ref:`Asyncify`.

Special considerations
======================

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

- 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.

- 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'``.)

- 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.

- 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.

- The Emscripten implementation does also not support multiprocessing via ``fork()`` and ``join()``.
Expand Down
32 changes: 30 additions & 2 deletions src/library_pthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -802,8 +802,18 @@ var LibraryPThread = {
if (canceled == 2) throw 'Canceled!';
},

{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join__deps: ['_cleanup_thread', '_pthread_testcancel_js', 'emscripten_main_thread_process_queued_calls', 'emscripten_futex_wait'],
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join: function(thread, status) {
emscripten_check_blocking_allowed: function() {
#if ASSERTIONS
assert(ENVIRONMENT_IS_WEB);
warnOnce('Blocking on the main thread is very dangerous, see https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread');
#endif
#if !ALLOW_BLOCKING_ON_MAIN_THREAD
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');
#endif
},

_emscripten_do_pthread_join__deps: ['_cleanup_thread', '_pthread_testcancel_js', 'emscripten_main_thread_process_queued_calls', 'emscripten_futex_wait', 'emscripten_check_blocking_allowed'],
_emscripten_do_pthread_join: function(thread, status, block) {
if (!thread) {
err('pthread_join attempted on a null thread pointer!');
return ERRNO_CODES.ESRCH;
Expand All @@ -827,6 +837,11 @@ var LibraryPThread = {
err('Attempted to join thread ' + thread + ', which was already detached!');
return ERRNO_CODES.EINVAL; // The thread is already detached, can no longer join it!
}

if (block && ENVIRONMENT_IS_WEB) {
_emscripten_check_blocking_allowed();
}

for (;;) {
var threadStatus = Atomics.load(HEAPU32, (thread + {{{ C_STRUCTS.pthread.threadStatus }}} ) >> 2);
if (threadStatus == 1) { // Exited?
Expand All @@ -838,6 +853,9 @@ var LibraryPThread = {
else postMessage({ 'cmd': 'cleanupThread', 'thread': thread });
return 0;
}
if (!block) {
return ERRNO_CODES.EBUSY;
}
// TODO HACK! Replace the _js variant with just _pthread_testcancel:
//_pthread_testcancel();
__pthread_testcancel_js();
Expand All @@ -848,6 +866,16 @@ var LibraryPThread = {
}
},

{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join__deps: ['_emscripten_do_pthread_join'],
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_join: function(thread, status) {
return __emscripten_do_pthread_join(thread, status, true);
},

pthread_tryjoin_np__deps: ['_emscripten_do_pthread_join'],
pthread_tryjoin_np: function(thread, status) {
return __emscripten_do_pthread_join(thread, status, false);
},

pthread_kill__deps: ['_kill_thread'],
pthread_kill: function(thread, signal) {
if (signal < 0 || signal >= 65/*_NSIG*/) return ERRNO_CODES.EINVAL;
Expand Down
8 changes: 8 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,14 @@ var PTHREAD_HINT_NUM_CORES = 4;
// True when building with --threadprofiler
var PTHREADS_PROFILING = 0;

// It is dangerous to call pthread_join or pthread_cond_wait
// on the main thread, as doing so can cause deadlocks on the Web (and also
// it works using a busy-wait which is expensive). See
// https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread
// This may become set to 0 by default in the future; for now, this just
// warns in the console.
var ALLOW_BLOCKING_ON_MAIN_THREAD = 1;

// If true, add in debug traces for diagnosing pthreads related issues.
var PTHREADS_DEBUG = 0;

Expand Down
4 changes: 4 additions & 0 deletions system/include/emscripten/threading.h
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ struct thread_profiler_block
char name[32];
};

// Called when blocking on the main thread. This will error if main thread
// blocking is not enabled, see ALLOW_BLOCKING_ON_MAIN_THREAD.
void emscripten_check_blocking_allowed(void);

#ifdef __cplusplus
}
#endif
6 changes: 6 additions & 0 deletions system/lib/libc/musl/src/thread/pthread_cond_timedwait.c
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ int __pthread_cond_timedwait(pthread_cond_t *restrict c, pthread_mutex_t *restri
int e, seq, clock = c->_c_clock, cs, shared=0, oldstate, tmp;
volatile int *fut;

#ifdef __EMSCRIPTEN__
if (pthread_self() == emscripten_main_browser_thread_id()) {
emscripten_check_blocking_allowed();
}
#endif

if ((m->_m_type&15) && (m->_m_lock&INT_MAX) != __pthread_self()->tid)
return EPERM;

Expand Down
87 changes: 87 additions & 0 deletions tests/pthread/main_thread_join.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2019 The Emscripten Authors. All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License. Both these licenses can be
// found in the LICENSE file.

#include <assert.h>
#include <emscripten.h>
#include <pthread.h>
#include <stdio.h>

#include <atomic>

pthread_t thread;

std::atomic<int> tries;

static const int EXPECTED_TRIES = 7;

void loop() {
void* retval;
printf("try...\n");
if (pthread_tryjoin_np(thread, &retval) == 0) {
Comment thread
kripken marked this conversation as resolved.
emscripten_cancel_main_loop();
assert(tries.load() == EXPECTED_TRIES);
#ifdef REPORT_RESULT
REPORT_RESULT(2);
#endif
}
tries++;
}

void *ThreadMain(void *arg) {
#ifdef TRY_JOIN
// Delay to force the main thread to try and fail a few times before
// succeeding.
while (tries.load() < EXPECTED_TRIES) {}
#endif
pthread_exit((void*)0);
}

pthread_t CreateThread() {
pthread_t ret;
int rc = pthread_create(&ret, NULL, ThreadMain, (void*)0);
assert(rc == 0);
return ret;
}

int main() {
if (!emscripten_has_threading_support()) {
#ifdef REPORT_RESULT
REPORT_RESULT(0);
#endif
printf("Skipped: Threading is not supported.\n");
return 0;
}

int x = EM_ASM_INT({
onerror = function(e) {
var message = e.toString();
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;
if (success && !Module.reported) {
Module.reported = true;
console.log("reporting success");
// manually REPORT_RESULT; we shouldn't call back into native code at this point
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8888/report_result?0");
xhr.send();
}
};
return 0;
});

thread = CreateThread();
#ifdef TRY_JOIN
emscripten_set_main_loop(loop, 0, 0);
#else
int status;
// This should fail on the main thread.
puts("trying to block...");
pthread_join(thread, (void**)&status);
puts("blocked ok.");
#ifdef REPORT_RESULT
REPORT_RESULT(1);
#endif
#endif // TRY_JOIN
}

47 changes: 47 additions & 0 deletions tests/pthread/main_thread_wait.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2019 The Emscripten Authors. All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License. Both these licenses can be
// found in the LICENSE file.

#include <assert.h>
#include <emscripten.h>
#include <pthread.h>
#include <stdio.h>

int main() {
if (!emscripten_has_threading_support())
{
#ifdef REPORT_RESULT
REPORT_RESULT(0);
#endif
printf("Skipped: Threading is not supported.\n");
return 0;
}

int x = EM_ASM_INT({
onerror = function(e) {
var message = e.toString();
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;
if (success && !Module.reported) {
Module.reported = true;
console.log("reporting success");
// manually REPORT_RESULT; we shouldn't call back into native code at this point
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8888/report_result?0");
xhr.send();
}
};
return 0;
});

// This should fail on the main thread.
puts("trying to block...");
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_wait(&cv, &lock);
puts("blocked ok.");
#ifdef REPORT_RESULT
REPORT_RESULT(1);
#endif
}

18 changes: 18 additions & 0 deletions tests/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3620,6 +3620,24 @@ def test_pthread_64bit_cxx11_atomics(self):
for pthreads in [[], ['-s', 'USE_PTHREADS=1']]:
self.btest(path_from_root('tests', 'pthread', 'test_pthread_64bit_cxx11_atomics.cpp'), expected='0', args=opt + pthreads + ['-std=c++11'])

@parameterized({
'join': ('join',),
'wait': ('wait',),
})
@requires_threads
def test_pthread_main_thread_blocking(self, name):
print('Test that we error if not ALLOW_BLOCKING_ON_MAIN_THREAD')
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'])
if name == 'join':
print('Test that by default we just warn about blocking on the main thread.')
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'])
print('Test that tryjoin is fine, even if not ALLOW_BLOCKING_ON_MAIN_THREAD')
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'])
print('Test that tryjoin is fine, even if not ALLOW_BLOCKING_ON_MAIN_THREAD, and even without a pool')
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'])
print('Test that everything works ok when we are on a pthread.')
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'])

# Test the old GCC atomic __sync_fetch_and_op builtin operations.
@requires_threads
def test_pthread_gcc_atomic_fetch_and_op(self):
Expand Down