Pthreads-Unterstützung

Hinweis

Browser, die SharedArrayBuffer implementiert und aktiviert haben, schützen ihn hinter Cross-Origin-Opener-Policy (COOP) und Cross-Origin-Embedder-Policy (COEP) Headern. Pthreads-Code funktioniert in einer bereitgestellten Umgebung nur, wenn diese Header korrekt gesetzt sind. Für weitere Informationen klicken Sie hier

Emscripten unterstützt Multithreading mithilfe von SharedArrayBuffer in Browsern. Diese API ermöglicht das Teilen von Speicher zwischen dem Hauptthread und Web-Workern sowie atomare Operationen zur Synchronisierung, wodurch Emscripten die Unterstützung für die Pthreads (POSIX-Threads)-API implementieren kann. Diese Unterstützung gilt in Emscripten als stabil.

Kompilieren mit aktivierten Pthreads

Standardmäßig ist die Unterstützung für Pthreads nicht aktiviert. Um die Codegenerierung für Pthreads zu aktivieren, stehen die folgenden Kommandozeilen-Flags zur Verfügung:

  • Übergeben Sie das Compiler-Flag -pthread beim Kompilieren von .c/.cpp-Dateien UND beim Linken, um die endgültige .js-Ausgabedatei zu generieren.

  • Optional übergeben Sie das Linker-Flag -sPTHREAD_POOL_SIZE=<expression>, um einen vordefinierten Pool von Web-Workern anzugeben, der zur Seite preRun Zeit vor dem Aufruf der Anwendungs- main() gefüllt werden soll. Dies ist wichtig, denn wenn die Worker noch nicht existieren, müssen wir möglicherweise auf die nächste Browser-Ereignis-Iteration für bestimmte Dinge warten, siehe unten. <expression> kann ein beliebiger gültiger JavaScript-Ausdruck sein, einschließlich Ganzzahlen wie 8 für eine feste Anzahl von Threads oder, zum Beispiel, navigator.hardwareConcurrency, um so viele Threads wie CPU-Kerne zu erstellen.

Es sollten keine weiteren Änderungen erforderlich sein. Im C/C++-Code kann die Präprozessor-Prüfung #ifdef __EMSCRIPTEN_PTHREADS__ verwendet werden, um zu erkennen, ob Emscripten derzeit auf Pthreads abzielt.

Hinweis

Es ist nicht möglich, ein Binärprogramm zu erstellen, das bei Verfügbarkeit Multithreading nutzen und bei Nichtverfügbarkeit auf Single-Threading zurückfallen kann. Das Beste, was Sie tun können, sind zwei separate Builds, einer mit und einer ohne Threads, und die Auswahl zwischen ihnen zur Laufzeit.

Zusätzliche Flags

  • -sPROXY_TO_PTHREAD: In diesem Modus wird Ihre ursprüngliche main() durch eine neue ersetzt, die einen Pthread erstellt und die ursprüngliche main() darauf ausführt. Dadurch wird die main() Ihrer Anwendung außerhalb des Browser-Haupt- (UI-)Threads ausgeführt, was gut für die Reaktionsfähigkeit ist. Der Browser-Hauptthread führt immer noch Code aus, wenn Dinge an ihn proxied werden, zum Beispiel zur Handhabung von Ereignissen, Rendering usw. Der Hauptthread erledigt auch Dinge wie das Erstellen von Pthreads für Sie, sodass Sie sich synchron auf diese verlassen können.

Beachten Sie, dass Emscripten das --proxy-to-worker Linker-Flag hat, das ähnlich klingt, aber nicht verwandt ist. Dieses Flag verwendet weder Pthreads noch SharedArrayBuffer, sondern einen einfachen Web Worker, um Ihr Hauptprogramm auszuführen (und postMessage, um Nachrichten hin und her zu proxieren).

Proxying

Das Web erlaubt bestimmte Operationen nur vom Hauptbrowser-Thread aus, wie die Interaktion mit dem DOM. Infolgedessen werden verschiedene Operationen an den Hauptbrowser-Thread weitergeleitet (proxied), wenn sie in einem Hintergrund-Thread aufgerufen werden. Siehe Bug 3495 für weitere Informationen und wie Sie versuchen können, dies bis dahin zu umgehen. Um zu überprüfen, welche Operationen proxied werden, können Sie die Implementierung der Funktion in der JS-Bibliothek (src/library_*) nachschlagen und sehen, ob sie mit __proxy: 'sync' oder __proxy: 'async' kommentiert ist; beachten Sie jedoch, dass der Browser selbst bestimmte Dinge (wie einige GL-Operationen) proxied, sodass es hier keinen allgemeinen sicheren Weg gibt (abgesehen davon, nicht auf den Hauptbrowser-Thread zu blockieren).

Zusätzlich hat Emscripten derzeit ein einfaches Modell, bei dem die Datei-I/O nur im Hauptanwendungsthread stattfindet (da wir JS-Plugin-Dateisysteme unterstützen, die den Speicher nicht teilen können); dies ist eine weitere Reihe von Operationen, die proxied werden.

Proxying kann in bestimmten Fällen Probleme verursachen, siehe den Abschnitt über das Blockieren unten.

Blockieren auf dem Hauptbrowser-Thread

Beachten Sie, dass in den meisten Fällen der „Hauptbrowser-Thread“ derselbe ist wie der „Hauptanwendungs-Thread“. Der Hauptbrowser-Thread ist der Ort, an dem Webseiten JavaScript ausführen und JavaScript auf das DOM zugreifen kann (eine Seite kann auch einen Web Worker erstellen, der dann nicht mehr im Hauptthread wäre). Der Hauptanwendungs-Thread ist derjenige, auf dem Sie die Anwendung gestartet haben (durch Laden der von Emscripten ausgegebenen Haupt-JS-Datei). Wenn Sie sie im Hauptbrowser-Thread gestartet haben – indem sie eine normale HTML-Seite ist – dann sind die beiden identisch. Sie können jedoch auch eine Multithread-Anwendung in einem Worker starten; in diesem Fall ist der Hauptanwendungs-Thread dieser Worker, und es gibt keinen Zugriff auf den Hauptbrowser-Thread.

Die Web-API für Atomics erlaubt kein Blockieren im Hauptthread (insbesondere funktioniert Atomics.wait dort nicht). Solches Blockieren ist in APIs wie pthread_join und allem, was unter der Haube ein Futex-Warten verwendet, wie usleep(), emscripten_futex_wait() oder pthread_mutex_lock(), notwendig. Um sie zum Laufen zu bringen, verwenden wir ein Busy-Waiting im Hauptbrowser-Thread, was dazu führen kann, dass der Browser-Tab nicht mehr reagiert und auch Strom verschwendet wird. (Bei einem Pthread ist dies kein Problem, da er in einem Web Worker läuft, wo wir kein Busy-Waiting benötigen.)

Busy-Waiting im Hauptbrowser-Thread funktioniert im Allgemeinen trotz der gerade erwähnten Nachteile, beispielsweise beim Warten auf einen schwach beanspruchten Mutex. Allerdings sind Dinge wie pthread_join und pthread_cond_wait oft dazu gedacht, über längere Zeiträume zu blockieren, und wenn dies im Hauptbrowser-Thread geschieht und andere Threads erwarten, dass er reagiert, kann dies zu einem überraschenden Deadlock führen. Dies kann durch Proxying geschehen, siehe den vorherigen Abschnitt. Wenn der Hauptthread blockiert, während ein Worker versucht, ihn zu proxieren, kann ein Deadlock auftreten.

Fazit ist, dass es im Web schlecht ist, wenn der Hauptbrowser-Thread auf etwas anderes wartet. Daher warnt Emscripten standardmäßig, wenn pthread_join und pthread_cond_wait im Hauptbrowser-Thread stattfinden, und wirft einen Fehler, wenn ALLOW_BLOCKING_ON_MAIN_THREAD null ist (dessen Nachricht hierher verweist).

Um diese Probleme zu vermeiden, können Sie PROXY_TO_PTHREAD verwenden, das, wie bereits erwähnt, Ihre main()-Funktion auf einen Pthread verschiebt, wodurch der Hauptbrowser-Thread sich nur auf den Empfang proxierter Ereignisse konzentrieren kann. Dies wird im Allgemeinen empfohlen, kann aber einige Portierungsarbeiten erfordern, wenn die Anwendung davon ausging, dass main() im Hauptbrowser-Thread lag.

Eine weitere Möglichkeit besteht darin, blockierende Aufrufe durch nicht-blockierende zu ersetzen. Sie können beispielsweise pthread_join durch pthread_tryjoin_np ersetzen. Dies erfordert möglicherweise eine Umstrukturierung Ihrer Anwendung, um asynchrone Ereignisse zu verwenden, vielleicht durch emscripten_set_main_loop() oder ASYNCIFY.

Besondere Überlegungen

Die Emscripten-Implementierung für die Pthreads-API sollte dem POSIX-Standard genau folgen, aber es gibt einige Verhaltensunterschiede

  • Wenn pthread_create() aufgerufen wird und wir einen neuen Web Worker erstellen müssen, erfordert dies die Rückkehr zum Haupt-Ereignis-Loop. Das heißt, Sie können pthread_create nicht aufrufen und dann synchron Code weiter ausführen, der erwartet, dass der Worker mit der Ausführung beginnt – er wird erst nach Ihrer Rückkehr zum Ereignis-Loop ausgeführt. Dies ist eine Verletzung des POSIX-Verhaltens und wird gängigen Code unterbrechen, der einen Thread erstellt und ihn sofort joined oder anderweitig synchron auf einen Effekt wie einen Speicherschreibvorgang wartet. Dafür gibt es mehrere Lösungen:

    1. Kehren Sie zum Haupt-Ereignis-Loop zurück (verwenden Sie zum Beispiel emscripten_set_main_loop oder Asyncify).

    2. Verwenden Sie das Linker-Flag -sPTHREAD_POOL_SIZE=<expression>. Die Verwendung eines Pools erstellt die Web Worker, bevor main aufgerufen wird, sodass sie einfach verwendet werden können, wenn pthread_create aufgerufen wird.

    3. Verwenden Sie das Linker-Flag -sPROXY_TO_PTHREAD, wodurch main() für Sie auf einem Worker ausgeführt wird. Dabei wird pthread_create an den Hauptbrowser-Thread weitergeleitet, wo es bei Bedarf zum Haupt-Ereignis-Loop zurückkehren kann.

  • Die Emscripten-Implementierung unterstützt keine POSIX-Signale, die manchmal in Verbindung mit Pthreads verwendet werden. Dies liegt daran, dass es nicht möglich ist, Signale an Web Worker zu senden und deren Ausführung zu unterbrechen. Die einzige Ausnahme hiervon ist pthread_kill(), das wie gewohnt verwendet werden kann, um einen laufenden Thread gewaltsam zu beenden.

  • Die Emscripten-Implementierung unterstützt auch kein Multiprocessing über fork() und join().

  • Aus Gründen der Web-Sicherheit gibt es eine feste Begrenzung (standardmäßig 20) der Threads, die beim Ausführen in Firefox Nightly erzeugt werden können. #1052398. Um das Limit anzupassen, navigieren Sie zu about:config und ändern Sie den Wert der Einstellung „dom.workers.maxPerDomain“.

  • Einige der Funktionen in der Pthreads-Spezifikation werden nicht unterstützt, da die von Emscripten verwendete Upstream-Musl-Bibliothek sie nicht unterstützt, oder sie sind als optional gekennzeichnet und eine konforme Implementierung muss sie nicht unterstützen. Zu den in Emscripten nicht unterstützten Funktionen gehören die Priorisierung von Threads, und pthread_rwlock_unlock() wird nicht in der Thread-Prioritätsreihenfolge ausgeführt. Die Funktionen pthread_mutexattr_set/getprotocol(), pthread_mutexattr_set/getprioceiling() und pthread_attr_set/getscope() sind No-Ops.

  • Eine besondere Anmerkung, die beim Portieren beachtet werden sollte, ist, dass in bestehenden Codebasen die Callback-Funktionszeiger für pthread_create() und pthread_cleanup_push() manchmal das void*-Argument weglassen, was streng genommen undefiniertes Verhalten in C/C++ ist, aber in einigen x86-Aufrufkonventionen funktioniert. Dies in Emscripten führt zu einer Compiler-Warnung und kann zur Laufzeit abbrechen, wenn versucht wird, einen Funktionszeiger mit falscher Signatur aufzurufen. Daher ist es bei solchen Fehlern ratsam, die Signaturen der Thread-Callback-Funktionen zu überprüfen.

  • Beachten Sie, dass die Funktion emscripten_num_logical_cores() immer den Wert von navigator.hardwareConcurrency zurückgibt, d.h. die Anzahl der logischen Kerne auf dem System, auch wenn der Shared Memory nicht unterstützt wird. Dies bedeutet, dass emscripten_num_logical_cores() einen Wert größer als 1 zurückgeben kann, während gleichzeitig emscripten_has_threading_support() false zurückgeben kann. Der Rückgabewert von emscripten_has_threading_support() gibt an, ob der Browser Shared Memory unterstützt.

  • Pthreads + Speicherwachstum (ALLOW_MEMORY_GROWTH) ist besonders heikel, siehe Wasm design issue #1271. Dies führt derzeit dazu, dass der JS-Zugriff auf den Wasm-Speicher langsam ist – dies wird jedoch wahrscheinlich nur dann spürbar, wenn der JS große Mengen an Speicher liest und schreibt (Wasm läuft mit voller Geschwindigkeit, sodass eine Aufgabenverlagerung dies beheben kann). Dies erfordert auch, dass Ihr JS sich bewusst ist, dass die HEAP*-Ansichten aktualisiert werden müssen – JS-Code, der mit --js-library usw. eingebettet ist, wird automatisch transformiert, um die GROWABLE_HEAP_*-Hilfsfunktionen zu verwenden, wo HEAP* verwendet werden, aber externer Code, der Module.HEAP* direkt verwendet, kann Probleme mit Ansichten haben, die kleiner als der Speicher sind.

Leistung des Allokators

Der standardmäßige Systemallokator in Emscripten, dlmalloc, ist in einem Einzelthread-Programm sehr effizient, hat aber einen einzigen globalen Lock, was bedeutet, dass bei Konkurrenz um malloc ein Overhead entstehen kann. Sie können stattdessen mimalloc verwenden, indem Sie -sMALLOC=mimalloc angeben, was ein ausgefeilterer Allokator ist, der auf Multithread-Leistung abgestimmt ist. mimalloc hat separate Allokationskontexte für jeden Thread, was eine viel bessere Skalierung der Leistung unter malloc/free-Konkurrenz ermöglicht.

Beachten Sie, dass mimalloc in der Code-Größe größer ist als dlmalloc und auch zur Laufzeit mehr Speicher verbraucht (Sie müssen möglicherweise INITIAL_MEMORY auf einen höheren Wert anpassen), sodass es hier Kompromisse gibt.

Ausführen von Code und Tests

Jeder Code, der mit aktivierter Pthreads-Unterstützung kompiliert wird, funktioniert derzeit nur im Firefox Nightly-Kanal, da die SharedArrayBuffer-Spezifikation sich noch in einem experimentellen Forschungsstadium vor der Standardisierung befindet. Es gibt zwei Testsuiten, die verwendet werden können, um das Verhalten der Pthreads-API-Implementierung in Emscripten zu überprüfen:

  • Die Emscripten-Unit-Testsuite enthält mehrere Pthreads-spezifische Tests in der Suite „browser.“ Führen Sie einen der Tests mit dem Namen browser.test_pthread_* aus.

  • Eine Emscripten-spezialisierte Version der Open POSIX Test Suite ist im juj/posixtestsuite GitHub-Repository verfügbar. Diese Suite enthält etwa 300 Tests zur Pthreads-Konformität. Um diese Suite auszuführen, sollte die Einstellung dom.workers.maxPerDomain zunächst auf mindestens 50 erhöht werden.

Bitte überprüfen Sie diese zuerst im Falle von Problemen. Fehler können wie gewohnt dem Emscripten-Bugtracker gemeldet werden.