Skip to content

Commit 554b012

Browse files
committed
Implement Pthread Manager Worker for synchronous thread creation
Fixes: #18633
1 parent b44cb92 commit 554b012

6 files changed

Lines changed: 248 additions & 13 deletions

File tree

src/lib/libpthread.js

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ const CMD_CLEANUP_THREAD = 6;
4040
const CMD_MARK_AS_FINISHED = 7;
4141
const CMD_UNCAUGHT_EXN = 8;
4242
const CMD_CALL_HANDLER = 9;
43+
#if PTHREAD_MANAGER
44+
const CMD_LOAD_MANAGER = 10;
45+
const CMD_CREATE_WORKER = 11;
46+
const CMD_TERMINATE_WORKER = 12;
47+
const CMD_FORWARD_TO_WORKER = 13;
48+
const CMD_FORWARD_FROM_WORKER = 14;
49+
const CMD_ERROR_FROM_WORKER = 15;
50+
#endif
4351

4452
#if WASM_ESM_INTEGRATION
4553
const pthreadWorkerScript = TARGET_BASENAME + '.pthread.mjs';
@@ -113,13 +121,48 @@ var LibraryPThread = {
113121
// the reverse mapping, each worker has a `pthread_ptr` when its running a
114122
// pthread.
115123
pthreads: {},
124+
#if PTHREAD_MANAGER
125+
proxyWorkers: {},
126+
// A ProxyWorker acts as a main-thread representative for a Web Worker
127+
// managed by the manager worker. It implements the standard Worker
128+
// interface (postMessage, terminate, onmessage, onerror) so that the main
129+
// thread's scheduler can interact with it transparently and pool it just
130+
// like a standard worker.
131+
ProxyWorker: class {
132+
constructor(workerID) {
133+
this.workerID = workerID;
134+
PThread.proxyWorkers[workerID] = this;
135+
PThread.managerWorker.postMessage({
136+
cmd: {{{ CMD_CREATE_WORKER }}},
137+
workerID: workerID
138+
});
139+
}
140+
postMessage(msg, transfer) {
141+
PThread.managerWorker.postMessage({
142+
cmd: {{{ CMD_FORWARD_TO_WORKER }}},
143+
workerID: this.workerID,
144+
msg: msg,
145+
transferList: transfer
146+
}, transfer);
147+
}
148+
terminate() {
149+
PThread.managerWorker.postMessage({
150+
cmd: {{{ CMD_TERMINATE_WORKER }}},
151+
workerID: this.workerID
152+
});
153+
delete PThread.proxyWorkers[this.workerID];
154+
}
155+
},
156+
#endif
116157
#if MAIN_MODULE
117158
outstandingPromises: {},
118159
// Finished threads are threads that have finished running but we are not yet
119160
// joined.
120161
finishedThreads: new Set(),
121162
#endif
122-
#if ASSERTIONS
163+
#if ASSERTIONS || PTHREAD_MANAGER
164+
// Used for debugging/assertions, or functionally by PTHREAD_MANAGER to
165+
// route multiplexed messages/errors to the correct ProxyWorker instance.
123166
nextWorkerID: 1,
124167
#endif
125168
init() {
@@ -128,19 +171,46 @@ var LibraryPThread = {
128171
}
129172
},
130173
initMainThread() {
174+
#if ENVIRONMENT_MAY_BE_NODE
175+
if (ENVIRONMENT_IS_NODE) return;
176+
#endif
177+
#if PTHREAD_MANAGER
178+
#if ASSERTIONS
179+
dbg('PThread: initializing manager worker');
180+
#endif
181+
var managerReadyResolve;
182+
PThread.managerWorkerReady = new Promise((resolve) => { managerReadyResolve = resolve; });
183+
PThread.managerWorker = PThread.createRealWorker();
184+
addOnPreRun(async () => {
185+
var managerReady = PThread.initManagerWorker(PThread.managerWorker);
186+
addRunDependency('manager-worker');
187+
await managerReady;
131188
#if PTHREAD_POOL_SIZE
132-
var pthreadPoolSize = {{{ PTHREAD_POOL_SIZE }}};
133-
// Start loading up the Worker pool, if requested.
134-
while (pthreadPoolSize--) {
135-
PThread.allocateUnusedWorker();
136-
}
189+
PThread.spawnPool();
190+
#endif
191+
removeRunDependency('manager-worker');
192+
managerReadyResolve();
193+
});
194+
#endif // PTHREAD_MANAGER
195+
#if PTHREAD_POOL_SIZE
196+
#if ASSERTIONS
197+
dbg('PThread: initializing worker pool');
198+
#endif
199+
#if !PTHREAD_MANAGER
200+
PThread.spawnPool();
201+
#endif
137202
#if !MINIMAL_RUNTIME
138203
// MINIMAL_RUNTIME takes care of calling loadWasmModuleToAllWorkers
139204
// in postamble_minimal.js
140205
addOnPreRun(async () => {
141-
var pthreadPoolReady = PThread.loadWasmModuleToAllWorkers();
142206
#if !PTHREAD_POOL_DELAY_LOAD
143207
addRunDependency('loading-workers');
208+
#endif
209+
#if PTHREAD_MANAGER
210+
await PThread.managerWorkerReady;
211+
#endif
212+
var pthreadPoolReady = PThread.loadWasmModuleToAllWorkers();
213+
#if !PTHREAD_POOL_DELAY_LOAD
144214
await pthreadPoolReady;
145215
removeRunDependency('loading-workers');
146216
#endif // PTHREAD_POOL_DELAY_LOAD
@@ -265,6 +335,37 @@ var LibraryPThread = {
265335
// module loaded.
266336
PThread.tlsInitFunctions.forEach((f) => f());
267337
},
338+
#if PTHREAD_MANAGER
339+
initManagerWorker: (worker) => new Promise((onFinishedLoading) => {
340+
worker.onmessage = (e) => {
341+
var d = e.data;
342+
var cmd = d.cmd;
343+
switch (cmd) {
344+
case {{{ CMD_FORWARD_FROM_WORKER }}}:
345+
PThread.proxyWorkers[d.workerID]?.onmessage({ data: d.msg });
346+
break;
347+
case {{{ CMD_ERROR_FROM_WORKER }}}:
348+
PThread.proxyWorkers[d.workerID]?.onerror(d.error);
349+
break;
350+
case {{{ CMD_LOADED }}}:
351+
onFinishedLoading(worker);
352+
break;
353+
default:
354+
if (cmd) err(`manager worker sent an unknown command ${cmd}`);
355+
}
356+
};
357+
worker.onerror = (e) => {
358+
err(`Manager worker sent an error! ${e.filename}:${e.lineno}: ${e.message}`);
359+
throw e;
360+
};
361+
worker.postMessage({
362+
cmd: {{{ CMD_LOAD_MANAGER }}},
363+
#if ASSERTIONS
364+
workerID: worker.workerID,
365+
#endif
366+
});
367+
}),
368+
#endif
268369
// Loads the WebAssembly module into the given Worker.
269370
// onFinishedLoading: A callback function that will be called once all of
270371
// the workers have been initialized and are
@@ -442,7 +543,7 @@ var LibraryPThread = {
442543
}),
443544

444545
#if PTHREAD_POOL_SIZE
445-
async loadWasmModuleToAllWorkers() {
546+
loadWasmModuleToAllWorkers() {
446547
// Instantiation is synchronous in pthreads.
447548
if (
448549
ENVIRONMENT_IS_PTHREAD
@@ -468,7 +569,7 @@ var LibraryPThread = {
468569
#endif // PTHREAD_POOL_SIZE
469570

470571
// Creates a new web Worker and places it in the unused worker pool to wait for its use.
471-
allocateUnusedWorker() {
572+
createRealWorker() {
472573
var worker;
473574
#if EXPORT_ES6
474575
// If we're using module output, use bundler-friendly pattern.
@@ -541,12 +642,34 @@ var LibraryPThread = {
541642
#endif
542643
worker = new Worker(pthreadMainJs, {{{ pthreadWorkerOptions }}});
543644
#endif // EXPORT_ES6
544-
#if ASSERTIONS
645+
#if ASSERTIONS || PTHREAD_MANAGER
545646
worker.workerID = PThread.nextWorkerID++;
546647
#endif
648+
return worker;
649+
},
650+
651+
allocateUnusedWorker() {
652+
var worker;
653+
#if PTHREAD_MANAGER
654+
if (PThread.managerWorker) {
655+
var workerID = PThread.nextWorkerID++;
656+
worker = new PThread.ProxyWorker(workerID);
657+
} else
658+
#endif
659+
{
660+
worker = PThread.createRealWorker();
661+
}
547662
PThread.unusedWorkers.push(worker);
548663
return worker;
549664
},
665+
#if PTHREAD_POOL_SIZE
666+
spawnPool() {
667+
var pthreadPoolSize = {{{ PTHREAD_POOL_SIZE }}};
668+
while (pthreadPoolSize--) {
669+
PThread.allocateUnusedWorker();
670+
}
671+
},
672+
#endif
550673

551674
getNewWorker() {
552675
if (PThread.unusedWorkers.length == 0) {
@@ -698,6 +821,8 @@ var LibraryPThread = {
698821
assert(threadParams.pthread_ptr, 'spawnThread called with null pthread ptr');
699822
#endif
700823

824+
825+
701826
var worker = PThread.getNewWorker();
702827
if (!worker) {
703828
// No available workers in the PThread pool.

src/runtime_pthread.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,68 @@ if (ENVIRONMENT_IS_PTHREAD) {
4242
// notified about them.
4343
self.onunhandledrejection = (e) => { throw e.reason || e; };
4444

45+
#if PTHREAD_MANAGER
46+
const targetWorkers = {};
47+
48+
function handleManagerMessage(e) {
49+
var d = e.data;
50+
var cmd = d.cmd;
51+
if (cmd === {{{ CMD_CREATE_WORKER }}}) {
52+
var workerID = d.workerID;
53+
var worker = PThread.createRealWorker();
54+
worker.workerID = workerID;
55+
targetWorkers[workerID] = worker;
56+
worker.onmessage = (e) => {
57+
var msg = e.data;
58+
var transferList = msg && msg.transferList;
59+
postMessage({
60+
cmd: {{{ CMD_FORWARD_FROM_WORKER }}},
61+
workerID: workerID,
62+
msg: msg
63+
}, transferList);
64+
};
65+
worker.onerror = (e) => {
66+
postMessage({
67+
cmd: {{{ CMD_ERROR_FROM_WORKER }}},
68+
workerID: workerID,
69+
error: {
70+
message: e.message,
71+
filename: e.filename,
72+
lineno: e.lineno,
73+
colno: e.colno
74+
}
75+
});
76+
};
77+
} else if (cmd === {{{ CMD_FORWARD_TO_WORKER }}}) {
78+
var worker = targetWorkers[d.workerID];
79+
if (worker) {
80+
worker.postMessage(d.msg, d.transferList);
81+
}
82+
} else if (cmd === {{{ CMD_TERMINATE_WORKER }}}) {
83+
var worker = targetWorkers[d.workerID];
84+
if (worker) {
85+
worker.terminate();
86+
delete targetWorkers[d.workerID];
87+
}
88+
} else {
89+
#if ASSERTIONS
90+
assert(false, "unknown message in pthread manager: " + cmd);
91+
#endif
92+
}
93+
}
94+
#endif
95+
4596
{{{ asyncIf(ASYNCIFY == 2) }}}function handleMessage(e) {
4697
try {
4798
var msgData = e.data;
4899
//dbg('msgData: ' + Object.keys(msgData));
49100
var cmd = msgData.cmd;
101+
50102
if (cmd == {{{ CMD_LOAD }}}) { // Preload command that is called once per worker to parse and load the Emscripten code.
51103
#if ASSERTIONS
52104
workerID = msgData.workerID;
53105
#endif
106+
54107
#if PTHREADS_DEBUG
55108
dbg('worker: loading module')
56109
#endif
@@ -61,8 +114,12 @@ if (ENVIRONMENT_IS_PTHREAD) {
61114

62115
// And add a callback for when the runtime is initialized.
63116
startWorker = () => {
117+
#if PTHREADS_DEBUG
118+
dbg('worker: startWorker');
119+
#endif
64120
// Notify the main thread that this thread has loaded.
65121
postMessage({ cmd: {{{ CMD_LOADED }}} });
122+
66123
// Process any messages that were queued before the thread was ready.
67124
for (let msg of messageQueue) {
68125
handleMessage(msg);
@@ -127,6 +184,14 @@ if (ENVIRONMENT_IS_PTHREAD) {
127184
run();
128185
#endif
129186
#endif // MINIMAL_RUNTIME
187+
#endif
188+
#if PTHREAD_MANAGER
189+
} else if (cmd == {{{ CMD_LOAD_MANAGER }}}) {
190+
#if RUNTIME_DEBUG
191+
dbg('creating worker manager');
192+
#endif
193+
postMessage({ cmd: {{{ CMD_LOADED }}} });
194+
self.onmessage = handleManagerMessage;
130195
#endif
131196
} else if (cmd == {{{ CMD_RUN }}}) {
132197
#if ASSERTIONS
@@ -178,6 +243,7 @@ if (ENVIRONMENT_IS_PTHREAD) {
178243
if (initializedJS) {
179244
checkMailbox();
180245
}
246+
181247
} else if (cmd) {
182248
// The received message looks like something that should be handled by this message
183249
// handler, (since there is a cmd field present), but is not one of the

src/settings.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,11 @@ var WEBAUDIO_DEBUG = 0;
16171617
// repeatedly yield back to the JS event loop in order for the thread to
16181618
// actually start.
16191619
// If your application needs to be able to synchronously create new threads,
1620+
// If true, a dedicated worker is used to manage pthread lifecycles.
1621+
// This allows synchronous thread creation even when the main thread is
1622+
// blocked.
1623+
var PTHREAD_MANAGER = false;
1624+
16201625
// you can pre-create a pthread pool by specifying -sPTHREAD_POOL_SIZE=x,
16211626
// in which case the specified number of Workers will be preloaded into a pool
16221627
// before the application starts, and that many threads can then be available
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#include <pthread.h>
2+
#include <stdio.h>
3+
4+
#include <emscripten/em_js.h>
5+
6+
EM_JS(int, get_worker_id, (), {
7+
return workerID;
8+
});
9+
10+
11+
void *thread_main(void* arg) {
12+
printf("thread_main (workerID=%d)\n", get_worker_id());
13+
return NULL;
14+
}
15+
16+
int main() {
17+
printf("main (workerID=%d)\n", get_worker_id());
18+
pthread_t t;
19+
20+
pthread_create(&t, NULL, thread_main, NULL);
21+
pthread_join(t, NULL);
22+
23+
// The second pthread should run on the same worker.
24+
pthread_create(&t, NULL, thread_main, NULL);
25+
pthread_join(t, NULL);
26+
27+
printf("done\n");
28+
return 0;
29+
}

test/test_browser.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3672,13 +3672,23 @@ def test_pthread_in_pthread_pool_size_strict(self):
36723672
# Check that it fails when there's a pthread creating another pthread.
36733673
self.btest_exit('pthread/test_pthread_create_pthread.c', cflags=['-g2', '-pthread', '-sPTHREAD_POOL_SIZE=1', '-sPTHREAD_POOL_SIZE_STRICT=2', '-DSMALL_POOL'])
36743674

3675+
def test_pthread_manager(self):
3676+
self.btest_exit('pthread/test_pthread_manager.c', cflags=['-pthread', '-sPTHREAD_MANAGER'])
3677+
3678+
def test_pthread_manager_pool(self):
3679+
self.btest_exit('pthread/test_pthread_manager.c', cflags=['-pthread', '-sPTHREAD_MANAGER', '-sPTHREAD_POOL_SIZE=2'])
3680+
36753681
# Test that the emscripten_ atomics api functions work.
3682+
@parameterized({
3683+
'': (['-sPTHREAD_POOL_SIZE=8'],),
3684+
'manager': (['-sPTHREAD_MANAGER'],),
3685+
})
36763686
@parameterized({
36773687
'': ([],),
36783688
'closure': (['--closure=1'],),
36793689
})
3680-
def test_pthread_atomics(self, args):
3681-
self.btest_exit('pthread/test_pthread_atomics.c', cflags=['-O3', '-pthread', '-sPTHREAD_POOL_SIZE=8', '-g1'] + args)
3690+
def test_pthread_atomics(self, args1, args2):
3691+
self.btest_exit('pthread/test_pthread_atomics.c', cflags=['-O3', '-pthread', '-g1'] + args1 + args2)
36823692

36833693
# Test 64-bit atomics.
36843694
def test_pthread_64bit_atomics(self):

0 commit comments

Comments
 (0)