Die Wasm Workers API ermöglicht C/C++-Code die Nutzung von Web Workern und Shared WebAssembly.Memory (SharedArrayBuffer), um multithreaded Programme über eine direkte webähnliche Programmier-API zu erstellen.
#include <emscripten/wasm_worker.h>
#include <stdio.h>
void run_in_worker()
{
printf("Hello from Wasm Worker!\n");
}
int main()
{
emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(/*stackSize: */1024);
emscripten_wasm_worker_post_function_v(worker, run_in_worker);
}
Der Code wird durch Übergabe des Emscripten-Flags -sWASM_WORKERS sowohl beim Kompilieren als auch beim Linken erstellt. Der Beispielcode erstellt einen neuen Worker auf dem Hauptbrowser-Thread, der dasselbe WebAssembly.Module und WebAssembly.Memory-Objekt gemeinsam nutzt. Anschließend wird eine postMessage() an den Worker übergeben, um ihn aufzufordern, die Funktion run_in_worker() auszuführen, um einen String auszugeben.
Um die Speicherplatzierung beim Erstellen eines Workers explizit zu steuern, verwenden Sie die Funktion emscripten_create_wasm_worker(). Diese Funktion benötigt einen Speicherbereich, der groß genug sein muss, um sowohl den Stack als auch die TLS-Daten für den Worker aufzunehmen. Sie können __builtin_wasm_tls_size() verwenden, um zur Laufzeit herauszufinden, wie viel Platz für die TLS-Daten des Programms benötigt wird.
In WebAssembly-Programmen kann das Memory-Objekt, das den Anwendungszustand enthält, über mehrere Worker hinweg geteilt werden. Dies ermöglicht einen direkten, hochperformanten (und, wenn nicht explizit darauf geachtet wird, race-anfälligen!) Zugriff, um Datenzustände synchron zwischen mehreren Workern zu teilen (Shared State Multithreading).
POSIX Threads (Pthreads) API, und
Wasm Workers API.
Die Pthreads-API hat eine lange Geschichte in der nativen C-Programmierung und dem POSIX-Standard, während die Wasm Workers API nur für den Emscripten-Compiler einzigartig ist.
Diese beiden APIs bieten weitgehend denselben Funktionsumfang, weisen jedoch wichtige Unterschiede auf, die in dieser Dokumentation erläutert werden sollen, um bei der Entscheidung zu helfen, welche API man verwenden sollte.
Die Zielgruppen und Anwendungsfälle dieser beiden Multithreading-APIs unterscheiden sich geringfügig.
Der Fokus der Pthreads API liegt auf Portabilität und plattformübergreifender Kompatibilität. Diese API wird am besten in Szenarien eingesetzt, in denen Portabilität am wichtigsten ist, z.B. wenn eine Codebasis auf mehrere Plattformen cross-kompiliert wird, wie beim Erstellen sowohl einer nativen Linux x664-Executable als auch einer Emscripten WebAssembly-basierten Website.
Die Pthreads API in Emscripten versucht, die Kompatibilität und die Funktionen, die die nativen Pthreads-Plattformen bereits bieten, sorgfältig zu emulieren. Dies hilft bei der Portierung großer C/C++-Codebasen auf WebAssembly.
Die Wasm Workers API hingegen versucht, eine "direkte Abbildung" auf die Web-Multithreading-Primitive, wie sie im Web existieren, bereitzustellen und damit fertig zu sein. Wenn eine Anwendung nur für WebAssembly entwickelt wird und Portabilität keine Rolle spielt, kann die Verwendung von Wasm Workern große Vorteile in Form von einfacherer kompilierter Ausgabe, geringerer Komplexität, kleinerer Code-Größe und möglicherweise besserer Leistung bieten.
Dieser Vorteil ist jedoch möglicherweise kein offensichtlicher Gewinn. Die Pthreads-API wurde entwickelt, um aus der synchronen C/C++-Sprache nützlich zu sein, während Web Workers entwickelt wurden, um aus asynchronem JavaScript nützlich zu sein. WebAssembly C/C++-Programme können sich irgendwo dazwischen befinden.
Pthreads und Wasm Workers teilen mehrere Gemeinsamkeiten
Beide können die emscripten_atomic_* Atomics API verwenden,
Beide können die GCC __sync_* Atomics API verwenden,
Beide können die C11- und C++11-Atomics-APIs verwenden,
Beide Thread-Typen haben einen lokalen Stack.
Beide Arten von Threads verfügen über Thread-Local Storage (TLS)-Unterstützung über die Schlüsselwörter
thread_local(C++11),_Thread_local(C11) und__thread(GNU11).Beide Thread-Typen unterstützen TLS über explizit verknüpfte Wasm-Globale (siehe
test/wasm_worker/wasm_worker_tls_wasm_assembly.c/.Sfür Beispielcode)Beide Arten von Threads haben das Konzept einer Thread-ID (
pthread_self()für pthreads,emscripten_wasm_worker_self_id()für Wasm Workers)Beide Thread-Typen unterstützen ein ereignisbasiertes und ein Endlos-Schleifen-Programmiermodell.
Beide können
EM_ASMundEM_JSAPI verwenden, um JS-Code auf dem aufrufenden Thread auszuführen.Beide können JS-Bibliotheksfunktionen aufrufen (verknüpft mit der Direktive
--js-library), um JS-Code auf dem aufrufenden Thread auszuführen.Weder Pthreads noch Wasm Workers können in Verbindung mit dem Linker-Flag
-sSINGLE_FILEverwendet werden.
Die Unterschiede sind jedoch bemerkenswerter.
Nur Pthreads können die Funktionen MAIN_THREAD_EM_ASM*() und MAIN_THREAD_ASYNC_EM_ASM() sowie die Proxy-Direktive foo__proxy: 'sync'/'async' in JS-Bibliotheken verwenden.
Wasm Worker hingegen bieten keine integrierte Proxy-Funktion für JS-Funktionen. Das Proxying einer JS-Funktion mit Wasm Workern kann durch explizites Übergeben der Adresse dieser Funktion an die emscripten_wasm_worker_post_function_* API erfolgen.
Wenn Sie von innerhalb eines Workers synchron auf das Ende der gesendeten Funktion warten müssen, verwenden Sie eine der emscripten_wasm_worker_*() Thread-Synchronisierungsfunktionen, um den aufrufenden Thread schlafen zu legen, bis der Empfänger den Vorgang beendet hat.
Beachten Sie, dass Wasm Worker nicht können
Auf Kosten der Leistung und der Code-Größe implementieren Pthreads das Konzept von POSIX-Abbruchpunkten (pthread_cancel(), pthread_testcancel()).
Wasm Worker sind leichter und leistungsfähiger, indem sie dieses Konzept nicht aktivieren.
Das Erstellen neuer Worker kann langsam sein. Das Spawnen eines Workers in JavaScript ist eine asynchrone Operation. Um einen synchronen Pthread-Start (für Anwendungen, die ihn benötigen) zu unterstützen und die Startleistung von Threads zu verbessern, werden Pthreads in einem zwischengespeicherten, von der Emscripten-Laufzeit verwalteten Worker-Pool gehostet.
Wasm Workers verzichten auf dieses Konzept, und infolgedessen starten Wasm Workers immer asynchron. Wenn Sie erkennen müssen, wann ein Wasm Worker gestartet wurde, senden Sie manuell ein Ping-Pong-Funktions- und Antwortpaar zwischen dem Worker und seinem Ersteller. Wenn Sie schnell neue Threads starten müssen, sollten Sie einen Pool von Wasm Workern selbst verwalten.
Im Web, wenn ein Worker einen eigenen Kind-Worker erzeugt, wird eine verschachtelte Worker-Hierarchie erstellt, auf die der Haupt-Thread nicht direkt zugreifen kann. Um Portabilitätsprobleme, die aus dieser Art von Topologie entstehen, zu umgehen, flachen Pthreads die Worker-Erstellungskette unter der Haube ab, so dass nur der Hauptbrowser-Thread Threads erzeugt.
Wasm Worker implementieren diese Art von Topologie-Abflachung nicht, und das Erstellen eines Wasm Workers in einem Wasm Worker erzeugt eine verschachtelte Worker-Hierarchie. Wenn Sie Wasm Worker innerhalb eines Wasm Workers erstellen müssen, überlegen Sie, welche Art von Hierarchie Sie wünschen, und flachen Sie die Hierarchie bei Bedarf manuell ab, indem Sie die Worker-Erstellung selbst an den Haupt-Thread übergeben.
Beachten Sie, dass die Unterstützung für verschachtelte Worker je nach Browser variiert. Stand 02/2022 werden verschachtelte Worker in Safari nicht unterstützt. Siehe hier für ein Polyfill.
Die in emscripten/wasm_worker.h angebotenen Multithreading-Synchronisationsprimitive (emscripten_lock_*, emscripten_semaphore_*, emscripten_condvar_*) können, falls gewünscht, frei von Pthreads aus aufgerufen werden, aber Wasm Worker können keine der Synchronisationsfunktionen in der Pthread-API (pthread_mutex_*, pthread_cond_, pthread_rwlock_*, etc.) verwenden, da ihnen die benötigte Pthread-Laufzeit fehlt.
Das Start-/Ausführungsmodell von Pthreads besteht darin, eine bestimmte Thread-Einstiegspunktfunktion auszuführen. Wenn diese Funktion beendet wird, beendet sich der Pthread (standardmäßig) ebenfalls, und der Worker, der diesen Pthread hostet, kehrt zum Worker-Pool zurück, um auf die Erstellung eines weiteren Threads auf ihm zu warten.
Wasm Worker implementieren stattdessen das direkte webähnliche Modell, bei dem ein neu erstellter Worker in seiner Ereignisschleife untätig sitzt und auf Funktionen wartet, die ihm zugewiesen werden. Wenn diese Funktionen beendet sind, kehrt der Worker zu seiner Ereignisschleife zurück und wartet darauf, weitere Funktionen (oder Webereignisse im Worker-Scope) zur Ausführung zu erhalten. Ein Wasm Worker wird nur mit einem Aufruf von emscripten_terminate_wasm_worker(worker_id) oder emscripten_terminate_all_wasm_workers() beendet.
Pthreads ermöglichen die Registrierung von Thread-Exit-Handlern über pthread_atexit, die beim Beenden des Threads aufgerufen werden. Wasm Workers kennen dieses Konzept nicht.
Um eine flexible synchrone Ausführung von Code auf anderen Threads zu ermöglichen und Support-APIs beispielsweise für MEMFS-Dateisystem- und Offscreen-Framebuffer-Funktionen (WebGL, emuliert von einem Worker) zu implementieren, verfügen der Hauptbrowser-Thread und jeder Pthread über eine systemgestützte „Proxy-Nachrichtenwarteschlange“, um Nachrichten zu empfangen.
Dies ermöglicht es dem Benutzercode, API-Funktionen wie emscripten_sync_run_in_main_runtime_thread(), emscripten_async_run_in_main_runtime_thread(), emscripten_dispatch_to_thread() usw. aus emscripten/threading.h aufzurufen, um proxied Aufrufe durchzuführen.
Wasm Worker bieten diese Funktionalität nicht. Bei Bedarf sollte eine solche Nachrichtenübermittlung vom Benutzer manuell über reguläre multithreaded, synchronisierte Programmiertechniken (Mutexes, Futexes, Semaphore usw.) implementiert werden.
Ein weiteres die Portabilität unterstützendes Emulationsmerkmal, das Pthreads bietet, ist, dass die von emscripten_get_now() zurückgegebenen Zeitwerte über alle Threads hinweg auf eine gemeinsame Zeitbasis synchronisiert werden.
Wasm Worker verzichten auf dieses Konzept, und es wird empfohlen, die Funktion emscripten_performance_now() für hochperformante Zeitmessungen in einem Wasm Worker zu verwenden und zu vermeiden, die resultierenden Werte über Worker hinweg zu vergleichen oder sie manuell zu synchronisieren.
Die in emscripten/html5.h bereitgestellte Multithread-Eingabe-API funktioniert nur mit der Pthread-API. Beim Aufruf einer der Funktionen emscripten_set_*_callback_on_thread() kann der Ziel-Pthread als Empfänger der empfangenen Ereignisse ausgewählt werden.
Mit Wasm Workern sollte, falls gewünscht, das „Backproxying“ von Ereignissen vom Hauptbrowser-Thread zu einem Wasm Worker manuell implementiert werden, z.B. durch Verwendung der emscripten_wasm_worker_post_function_*() API-Familie.
Beachten Sie jedoch, dass das Rück-Proxying von Eingabeereignissen den Nachteil hat, dass es sicherheitssensible Operationen wie Vollbildanfragen, Zeigerfixierung und Wiederaufnahme der Audiowiedergabe verhindert, da die Verarbeitung des Eingabeereignisses vom Ereignis-Callback-Kontext, der die ursprüngliche Operation ausführt, getrennt ist.
Die Mutex-Implementierung von pthread_mutex_* hat einige verschiedene Erstellungsoptionen, eine davon ist ein „rekursiver“ Mutex.
Das durch die emscripten_lock_* API implementierte Lock ist nicht rekursiv (und bietet keine Option).
Pthreads bietet auch einen Programmiererschutz gegen einen Programmierfehler, bei dem ein Thread ein von einem anderen Thread besetztes Lock nicht freigibt. Die emscripten_lock_* API verfolgt den Lock-Besitz nicht.
Pthreads haben eine feste Abhängigkeit von dynamischer Speicherallokation und rufen malloc und free auf, um Thread-spezifische Daten, Stacks und TLS-Slots zuzuweisen.
Mit Ausnahme der Hilfsfunktion emscripten_malloc_wasm_worker() sind Wasm Worker nicht auf einen dynamischen Speicherallokator angewiesen. Speicherbedarfe werden vom Aufrufer zum Zeitpunkt der Worker-Erstellung gedeckt und können bei Bedarf statisch platziert werden.
Der Overhead der Disk-Größe von Pthreads liegt in der Größenordnung von einigen hundert KB. Die Wasm Workers Laufzeit hingegen ist für winzige Bereitstellungen optimiert, nur wenige hundert Bytes auf der Festplatte.
Um die verschiedenen APIs, die zwischen Pthreads und Wasm Workern verfügbar sind, besser zu verstehen, konsultieren Sie die folgende Tabelle.
| Funktion | Pthreads | Wasm Workers |
| Thread-Beendigung | Thread-Aufrufepthread_exit(status)oder Haupt-Thread-Aufrufe pthread_kill(code) |
Worker kann sich nicht selbst beenden, Eltern-Thread beendet durch Aufruf vonemscripten_terminate_wasm_worker(worker) |
| Thread-Stack | In pthread_attr_t Struktur angeben. | Thread-Stack-Bereich explizit verwalten mitemscripten_create_wasm_worker_*_tls()Funktionen, oder Stack+TLS-Bereich automatisch zuweisen mit emscripten_malloc_wasm_worker()API. |
| Thread Local Storage (TLS) | Transparent unterstützt. | Unterstützt entweder explizit mitemscripten_create_wasm_worker_*_tls()Funktionen, oder automatisch über emscripten_malloc_wasm_worker()API. |
| Thread-ID | Das Erstellen eines Pthread erhält dessen ID. Aufrufenpthread_self()um die ID des aufrufenden Threads zu erhalten. |
Das Erstellen eines Workers erhält dessen ID. Aufrufenemscripten_wasm_worker_self_id()ID des aufrufenden Threads abrufen. |
| Hochauflösender Timer | ``emscripten_get_now()`` | ``emscripten_performance_now()`` |
| Synchrone Blockierung im Haupt-Thread | Synchronisationsprimitive fallen intern auf Busy-Spin-Schleifen zurück. | Explizite Spin- vs. Sleep-Synchronisationsprimitive. |
| Futex-API | emscripten_futex_wait emscripten_futex_wakein emscripten/threading.h |
emscripten_atomic_wait_u32 emscripten_atomic_wait_u64 emscripten_atomic_notifyin emscripten/atomic.h |
| Asynchroner Futex-Wartevorgang | N/A | emscripten_atomic_wait_async() emscripten_*_async_acquire()Dies sind jedoch eine schwierige Fußfalle, lesen Sie WebAssembly/threads Issue #176 |
| C/C++ Funktions-Proxying | emscripten/threading.h API zum Proxying von Funktionsaufrufen an andere Threads. | Verwenden Sie die emscripten_wasm_worker_post_function_*() API, um Funktionen an andere Threads zu senden. Diese Nachrichten folgen der Semantik einer Ereigniswarteschlange anstatt der Semantik einer Proxy-Warteschlange. |
| Build-Flags | Kompilieren und Linken mit -pthread | Kompilieren und Linken mit -sWASM_WORKERS |
| Präprozessor-Direktiven | __EMSCRIPTEN_SHARED_MEMORY__=1 und __EMSCRIPTEN_PTHREADS__=1 sind aktiv | __EMSCRIPTEN_SHARED_MEMORY__=1 und __EMSCRIPTEN_WASM_WORKERS__=1 sind aktiv |
| JS-Bibliotheksdirektiven | USE_PTHREADS und SHARED_MEMORY sind aktiv | USE_PTHREADS, SHARED_MEMORY und WASM_WORKER sind aktiv |
| Atomics API | Unterstützt, verwenden Sie eine der __atomic_* API, __sync_* API oder C++11 std::atomic API. | |
| Nicht-rekursiver Mutex | pthread_mutex_* |
emscripten_lock_* |
| Rekursiver Mutex | pthread_mutex_* |
N/A |
| Semaphore | N/A | emscripten_semaphore_* |
| Bedingungsvariablen | pthread_cond_* |
emscripten_condvar_* |
| Lese-Schreib-Sperren | pthread_rwlock_* |
N/A |
| Spinlocks | pthread_spin_* |
emscripten_lock_busyspin* |
| WebGL Offscreen Framebuffer | Supported with -sOFFSCREEN_FRAMEBUFFER |
Not supported. |
Beim Instanziieren eines Wasm Workers muss ein Speicherarray für den LLVM-Datenstack des erstellten Workers erstellt werden. Dieser Datenstack wird im Allgemeinen nur aus lokalen Variablen bestehen, die von LLVM in den Speicher "ausgelagert" wurden, z.B. um große Arrays, Strukturen oder andere Variablen zu enthalten, auf die über eine Speicheradresse verwiesen wird. Dieser Stack wird keine Kontrollflussinformationen enthalten.
Da WebAssembly keinen virtuellen Speicher unterstützt, kann die Größe des LLVM-Datenstacks, der sowohl für Wasm Worker als auch für den Hauptthread definiert ist, zur Laufzeit nicht erweitert werden. Wenn also der Worker (oder der Hauptthread) keinen Stack-Speicher mehr hat, ist das Programmverhalten undefiniert. Verwenden Sie das Emscripten-Linker-Flag -sSTACK_OVERFLOW_CHECK=2, um zur Laufzeit Stack-Überlaufprüfungen in den Programmcode einzufügen, um diese Situationen während der Entwicklung zu erkennen.
Beachten Sie, dass, um die Notwendigkeit zweier separater Zuweisungen zu vermeiden, der TLS-Speicher für den Wasm Worker am unteren Ende (niedrige Speicheradresse) des Wasm Worker-Stack-Speicherplatzes liegt.
Emscripten stellt eine zweite Worker-API als Teil des emscripten.h-Headers bereit. Diese Worker-API ist älter als das Aufkommen von SharedArrayBuffer und unterscheidet sich deutlich von der Wasm Workers API; die Benennung dieser beiden APIs ist lediglich aus historischen Gründen ähnlich.
Beide APIs ermöglichen es, Web Worker vom Hauptthread aus zu spawnen, obwohl die Semantik unterschiedlich ist.
Mit der Worker-API kann der Benutzer einen Web Worker von einer benutzerdefinierten URL aus starten. Diese URL kann auf eine völlig separate JS-Datei zeigen, die nicht mit Emscripten kompiliert wurde, um Worker von beliebigen URLs zu laden. Mit Wasm Workern wird keine benutzerdefinierte URL angegeben: Wasm Worker starten immer einen Web Worker, der im selben WebAssembly+JavaScript-Kontext wie das Hauptprogramm rechnet.
Die Worker-API ist nicht mit SharedArrayBuffer integriert, daher ist die Interaktion mit dem geladenen Worker immer asynchron. Wasm Worker hingegen basieren auf SharedArrayBuffer, und jeder Wasm Worker teilt und rechnet im selben WebAssembly-Speicheradressraum des Hauptthreads.
Sowohl die Worker API als auch die Wasm Workers API bieten dem Benutzer die Möglichkeit, Funktionsaufrufe über postMessage() an den Worker zu senden. In der Worker API ist das Senden von Nachrichten darauf beschränkt, vom Hauptthread zum Worker zu stammen/initiieren (unter Verwendung der API emscripten_call_worker() und emscripten_worker_respond() in <emscripten.h>). Mit Wasm Workern kann man jedoch auch Funktionsaufrufe an den übergeordneten (besitzenden) Thread senden.
Beim Senden von Funktionsaufrufen mit der Emscripten Worker API ist es erforderlich, dass die Ziel-Worker-URL auf ein mit Emscripten kompiliertes Programm zeigt (damit sie die Module-Struktur besitzt, um Funktionsnamen zu lokalisieren). Nur Funktionen, die in das Module-Objekt exportiert wurden, sind aufrufbar. Mit Wasm Workern kann jede C/C++-Funktion gesendet werden und muss nicht exportiert werden.
Sie einen Worker einfach aus einer JS-Datei starten möchten, die nicht mit Emscripten erstellt wurde
Sie als Worker ein einzelnes separates kompiliertes Programm starten möchten, das sich vom Haupt-Thread-Programm unterscheidet, und die Haupt-Thread- und Worker-Programme keinen gemeinsamen Code teilen
Sie die Verwendung von SharedArrayBuffer oder die Einrichtung von COOP+COEP-Headern nicht benötigen
Sie nur asynchron mit dem Worker über postMessage()-Funktionsaufrufe kommunizieren müssen
Sie einen oder mehrere neue Threads erstellen möchten, die synchron im selben Wasm Module-Kontext rechnen
Sie mehrere Worker aus derselben Codebasis spawnen und Speicher sparen möchten, indem Sie das WebAssembly-Modul (Objektcode) und den Speicher (Adressraum) über die Worker hinweg teilen
Sie die Kommunikation zwischen Threads synchron mittels atomarer Primitive und Sperren koordinieren möchten
Ihr Webserver mit den benötigten COOP+COEP-Headern konfiguriert wurde, um SharedArrayBuffer-Funktionen auf der Website zu ermöglichen
Die folgenden Build-Optionen werden derzeit mit Wasm Workern nicht unterstützt
-sSINGLE_FILE
Dynamische Verknüpfung (-sLINKABLE, -sMAIN_MODULE, -sSIDE_MODULE)
-sPROXY_TO_WORKER
-sPROXY_TO_PTHREAD
Siehe das Verzeichnis test/wasm_workers/ für Codebeispiele zur Funktionalität der verschiedenen Wasm Workers API.