Skip to content

Commit b82789d

Browse files
committed
Implement Pthread Manager Worker for synchronous thread creation
Fixes: #18633
1 parent 3eb2d79 commit b82789d

6 files changed

Lines changed: 250 additions & 14 deletions

File tree

src/lib/libpthread.js

Lines changed: 136 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
@@ -226,6 +296,7 @@ var LibraryPThread = {
226296
// Note: worker is intentionally not terminated so the pool can
227297
// dynamically grow.
228298
PThread.unusedWorkers.push(worker);
299+
229300
// Not a running Worker anymore
230301
// Detach the worker from the pthread object, and return it to the
231302
// worker pool as an unused worker.
@@ -265,6 +336,37 @@ var LibraryPThread = {
265336
// module loaded.
266337
PThread.tlsInitFunctions.forEach((f) => f());
267338
},
339+
#if PTHREAD_MANAGER
340+
initManagerWorker: (worker) => new Promise((onFinishedLoading) => {
341+
worker.onmessage = (e) => {
342+
var d = e.data;
343+
var cmd = d.cmd;
344+
switch (cmd) {
345+
case {{{ CMD_FORWARD_FROM_WORKER }}}:
346+
PThread.proxyWorkers[d.workerID]?.onmessage({ data: d.msg });
347+
break;
348+
case {{{ CMD_ERROR_FROM_WORKER }}}:
349+
PThread.proxyWorkers[d.workerID]?.onerror(d.error);
350+
break;
351+
case {{{ CMD_LOADED }}}:
352+
onFinishedLoading(worker);
353+
break;
354+
default:
355+
if (cmd) err(`manager worker sent an unknown command ${cmd}`);
356+
}
357+
};
358+
worker.onerror = (e) => {
359+
err(`Manager worker sent an error! ${e.filename}:${e.lineno}: ${e.message}`);
360+
throw e;
361+
};
362+
worker.postMessage({
363+
cmd: {{{ CMD_LOAD_MANAGER }}},
364+
#if ASSERTIONS
365+
workerID: worker.workerID,
366+
#endif
367+
});
368+
}),
369+
#endif
268370
// Loads the WebAssembly module into the given Worker.
269371
// onFinishedLoading: A callback function that will be called once all of
270372
// the workers have been initialized and are
@@ -442,7 +544,7 @@ var LibraryPThread = {
442544
}),
443545

444546
#if PTHREAD_POOL_SIZE
445-
async loadWasmModuleToAllWorkers() {
547+
loadWasmModuleToAllWorkers() {
446548
// Instantiation is synchronous in pthreads.
447549
if (
448550
ENVIRONMENT_IS_PTHREAD
@@ -468,7 +570,7 @@ var LibraryPThread = {
468570
#endif // PTHREAD_POOL_SIZE
469571

470572
// Creates a new web Worker and places it in the unused worker pool to wait for its use.
471-
allocateUnusedWorker() {
573+
createRealWorker() {
472574
var worker;
473575
#if EXPORT_ES6
474576
// If we're using module output, use bundler-friendly pattern.
@@ -541,11 +643,33 @@ var LibraryPThread = {
541643
#endif
542644
worker = new Worker(pthreadMainJs, {{{ pthreadWorkerOptions }}});
543645
#endif // EXPORT_ES6
544-
#if ASSERTIONS
646+
#if ASSERTIONS || PTHREAD_MANAGER
545647
worker.workerID = PThread.nextWorkerID++;
546648
#endif
649+
return worker;
650+
},
651+
652+
allocateUnusedWorker() {
653+
var worker;
654+
#if PTHREAD_MANAGER
655+
if (PThread.managerWorker) {
656+
var workerID = PThread.nextWorkerID++;
657+
worker = new PThread.ProxyWorker(workerID);
658+
} else
659+
#endif
660+
{
661+
worker = PThread.createRealWorker();
662+
}
547663
PThread.unusedWorkers.push(worker);
548664
},
665+
#if PTHREAD_POOL_SIZE
666+
spawnPool() {
667+
var pthreadPoolSize = {{{ PTHREAD_POOL_SIZE }}};
668+
while (pthreadPoolSize--) {
669+
PThread.allocateUnusedWorker();
670+
}
671+
},
672+
#endif
549673

550674
getNewWorker() {
551675
if (PThread.unusedWorkers.length == 0) {
@@ -697,6 +821,8 @@ var LibraryPThread = {
697821
assert(threadParams.pthread_ptr, 'spawnThread called with null pthread ptr');
698822
#endif
699823

824+
825+
700826
var worker = PThread.getNewWorker();
701827
if (!worker) {
702828
// No available workers in the PThread pool.

src/runtime_pthread.js

Lines changed: 67 additions & 1 deletion
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
@@ -190,7 +256,7 @@ if (ENVIRONMENT_IS_PTHREAD) {
190256
err(`worker: onmessage() captured an uncaught exception: ${ex}`);
191257
if (ex?.stack) err(ex.stack);
192258
#endif
193-
__emscripten_thread_crashed();
259+
if (typeof __emscripten_thread_crashed !== 'undefined') __emscripten_thread_crashed();
194260
throw ex;
195261
}
196262
};

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+
}

0 commit comments

Comments
 (0)