1. Code
  2. JavaScript

Erste Schritte mit Web Workern

Scroll to top

German (Deutsch) translation by Katharina Grigorovich-Nevolina (you can also view the original English article)

Eines der vielen Designziele der JavaScript-Sprache war es, sie einfach und im weiteren Sinne einfach zu halten. Obwohl ich zugeben muss, dass es angesichts der Eigenheiten der Sprachkonstrukte alles andere als einfach ist! Mit "Single-Threaded" meinen wir jedoch, dass es in JavaScript nur einen Kontroll-Thread gibt. Ja, leider kann Ihre JavaScript-Engine jeweils nur eine Aufgabe ausführen.

Klingt das nicht zu restriktiv, um Multi-Core-Prozessoren zu verwenden, die auf Ihrer Maschine im Leerlauf liegen? HTML5 verspricht, all das zu ändern.


JavaScript's Single Threaded Model

Web Worker leben in einer eingeschränkten Welt ohne DOM-Zugriff, da DOM nicht threadsicher ist.

Eine Denkschule betrachtet die Single-Threaded-Natur von JavaScript als Vereinfachung, die andere als Einschränkung. Die letztere Gruppe hat einen sehr guten Punkt, insbesondere wenn moderne Webanwendungen JavaScript häufig verwenden, um UI-Ereignisse zu verarbeiten, serverseitige APIs abzufragen oder abzufragen, große Datenmengen zu verarbeiten und das DOM basierend auf der Antwort des Servers zu bearbeiten.

Es ist oft eine entmutigende Aufgabe, in einem einzigen Kontroll-Thread so viel zu tun, während eine reaktionsfähige Benutzeroberfläche beibehalten wird, und Entwickler müssen auf Hacks und Problemumgehungen zurückgreifen (z. B. mit setTimeout(), setInterval() oder mit XMLHttpRequest und) DOM-Ereignisse), um Parallelität zu erreichen. Es ist jedoch erwähnenswert, dass diese Techniken definitiv eine Möglichkeit bieten, asynchrone Anrufe zu tätigen, aber nicht blockieren bedeutet nicht unbedingt gleichzeitig. John Resig erklärt in seinem Blog, warum Sie nichts parallel ausführen können.

Die Einschränkungen

Wenn Sie längere Zeit mit JavaScript gearbeitet haben, ist es sehr wahrscheinlich, dass Sie auf das folgende nervige Dialogfeld gestoßen sind, in dem angegeben wird, dass die Ausführung eines Skripts zu lange dauert. Ja, fast jedes Mal, wenn Ihre Seite nicht mehr reagiert, kann der Grund auf JavaScript-Code zurückgeführt werden.

Damn this slow machine !!Damn this slow machine !!Damn this slow machine !!

Hier sind einige der Gründe, warum Ihr Browser beim Ausführen Ihres Skripts möglicherweise seine Stiefel auflegt:

  • Übermäßige DOM-Manipulation: Die DOM-Manipulation ist möglicherweise die teuerste Operation, die Sie mit JavaScript ausführen können. Infolgedessen ist Ihr Skript aufgrund vieler DOM-Manipulationsvorgänge ein guter Kandidat für das Refactoring.
  • Endlosschleifen: Es tut nie weh, Ihren Code nach komplexen verschachtelten Schleifen zu durchsuchen. Diese erledigen in der Regel viel mehr Arbeit als tatsächlich benötigt wird. Vielleicht finden Sie eine andere Lösung, die die gleiche Funktionalität bietet.
  • Beides kombinieren: Das Schlimmste, was wir tun können, ist, das DOM innerhalb einer Schleife wiederholt zu aktualisieren, wenn elegantere Lösungen wie die Verwendung eines DocumentFragments vorhanden sind.

Web Worker zur Rettung

... nicht blockieren heißt nicht unbedingt gleichzeitig ...

Dank HTML5 und Web Workers können Sie jetzt einen neuen Thread erstellen, der echte Asynchronität bietet. Der neue Worker kann im Hintergrund ausgeführt werden, während der Hauptthread UI-Ereignisse verarbeitet, selbst wenn der Worker-Thread gerade mit der Verarbeitung einer großen Datenmenge beschäftigt ist. Beispielsweise könnte ein Mitarbeiter eine große JSON-Struktur verarbeiten, um wertvolle Informationen zu extrahieren, die in der Benutzeroberfläche angezeigt werden sollen. Aber genug von meinem Geplapper; Lassen Sie uns einen Code in Aktion sehen.

Erstellen eines Worker

Normalerweise befindet sich der Code eines Web-Workers in einer separaten JavaScript-Datei. Der übergeordnete Thread erstellt einen neuen Worker, indem er den URI der Skriptdatei im Worker-Konstruktor angibt, der die JavaScript-Datei asynchron lädt und ausführt.

1
var primeWorker = new Worker('prime.js');

Starten Sie einen Worker

Um einen Worker zu starten, sendet der übergeordnete Thread eine Nachricht an den Worker wie folgt:

1
var current = $('#prime').attr('value');
2
primeWorker.postMessage(current);

Die übergeordnete Seite kann mithilfe der postMessage-API, die auch für originensübergreifende Nachrichten verwendet wird, mit Mitarbeitern kommunizieren. Neben dem Senden primitiver Datentypen an den Worker unterstützt die postMessage-API auch das Übergeben von JSON-Strukturen. Sie können jedoch keine Funktionen übergeben, da diese möglicherweise Verweise auf das zugrunde liegende DOM enthalten.

Die übergeordneten und Worker-Threads haben einen eigenen separaten Bereich. Nachrichten, die hin und her weitergeleitet werden, werden kopiert und nicht geteilt.

Hinter den Kulissen werden diese Nachrichten beim Worker serialisiert und am empfangenden Ende de-serialisiert. Aus diesem Grund wird davon abgeraten, große Datenmengen an den Mitarbeiter zu senden.

Der übergeordnete Thread kann auch einen Rückruf registrieren, um auf Nachrichten zu warten, die der Worker nach Ausführung seiner Aufgabe zurücksendet. Auf diese Weise kann der übergeordnete Thread die erforderlichen Maßnahmen ergreifen (z. B. das DOM aktualisieren), nachdem der Worker seine Rolle gespielt hat. Schauen Sie sich diesen Code an:

1
primeWorker.addEventListener('message', function(event){
2
    console.log('Receiving from Worker: '+event.data);
3
    $('#prime').html( event.data );
4
});

Das event-Objekt enthält zwei wichtige Eigenschaften:

  • target: Wird verwendet, um den Mitarbeiter zu identifizieren, der die Nachricht gesendet hat. In erster Linie nützlich in einer Umgebung mit mehreren Arbeitern.
  • data: Die vom Worker zurück an den übergeordneten Thread gesendete Nachricht.

Der Worker selbst ist in prime.js enthalten und registriert sich für das message-Ereignis, das er von seinem übergeordneten Element empfängt. Es verwendet auch dieselbe postMessage-API, um mit dem übergeordneten Thread zu kommunizieren.

1
self.addEventListener('message',  function(event){
2
    var currPrime = event.data, nextPrime;
3
    setInterval( function(){
4
5
    nextPrime = getNextPrime(currPrime);
6
    postMessage(nextPrime);	
7
    currPrime = nextPrime;
8
9
    }, 500);
10
});

Web-Mitarbeiter leben in einer eingeschränkten und thread-sicheren Umgebung.

In diesem Beispiel finden wir einfach die nächsthöhere Primzahl und senden die Ergebnisse wiederholt an den übergeordneten Thread zurück, der wiederum die Benutzeroberfläche mit dem neuen Wert aktualisiert. Im Kontext eines Arbeitnehmers beziehen sich sowohl das self als auch this auf den globalen Geltungsbereich. Der Worker kann entweder einen message-Ereignis für das Nachrichtenereignis hinzufügen oder den onmessage-Handler definieren, der auf vom übergeordneten Thread gesendete Nachrichten wartet.

Die Aufgabe, die nächste Primzahl zu finden, ist offensichtlich nicht der ideale Anwendungsfall für einen Arbeiter, sondern wurde hier gewählt, um das Konzept der Nachrichtenübermittlung zu demonstrieren. Später untersuchen wir mögliche und praktische Anwendungsfälle, bei denen die Verwendung eines Web Workers wirklich Vorteile bringt.

Workers kündigen

Die Workers sind ressourcenintensiv. Sie sind Threads auf Betriebssystemebene. Daher möchten Sie keine große Anzahl von Worker-Threads erstellen und sollten den Web-Worker beenden, nachdem er seine Arbeit abgeschlossen hat. Arbeiter können sich wie folgt kündigen:

1
self.close();

Oder ein übergeordneter Thread kann einen Worker beenden:

1
primeWorker.terminate();

Sicherheit und Einschränkungen

Innerhalb eines Arbeiterskripts haben wir keinen Zugriff auf die vielen wichtigen JavaScript-Objekte wie document, window, console, parent Element und vor allem keinen Zugriff auf das DOM. Es klingt zu restriktiv, keinen DOM-Zugriff zu haben und die Seite nicht aktualisieren zu können, aber es ist eine wichtige Entscheidung für das Sicherheitsdesign. Stellen Sie sich das Chaos vor, das es verursachen könnte, wenn mehrere Threads versuchen, dasselbe Element zu aktualisieren. Daher leben Web-Worker in einer eingeschränkten und thread-sicheren Umgebung.

Trotzdem können Sie weiterhin Worker verwenden, um Daten zu verarbeiten und das Ergebnis an den Hauptthread zurückzugeben, der dann das DOM aktualisieren kann. Obwohl ihnen der Zugriff auf einige ziemlich wichtige JavaScript-Objekte verweigert wird, dürfen Mitarbeiter einige Funktionen wie setTimeout()/clearTimeout(), setInterval()/clearInterval(), navigator usw. verwenden. Sie können auch die Objekte XMLHttpRequest und localStorage im Worker verwenden.

Einschränkungen gleichen Ursprungs

Im Kontext eines Arbeitnehmers beziehen sich sowohl das self als auch this auf den globalen Geltungsbereich.

Um mit einem Server zu kommunizieren, müssen die Mitarbeiter die Richtlinie mit demselben Ursprung befolgen. Beispielsweise kann ein auf http://www.example.com/ gehostetes Skript nicht auf ein Skript unter https://www.example.com/ zugreifen. Obwohl die Hostnamen identisch sind, heißt es in der ursprünglichen Richtlinie, dass auch das Protokoll identisch sein muss. Normalerweise ist dies kein Problem. Es ist sehr wahrscheinlich, dass Sie sowohl den Worker als auch den Client schreiben und sie aus derselben Domäne bedienen, aber es ist immer nützlich, die Einschränkung zu kennen.

Lokale Zugriffsprobleme mit Google Chrome

Google Chrome beschränkt den lokalen Zugriff auf die Worker. Daher können Sie diese Beispiele nicht in einem lokalen Setup ausführen. Wenn Sie Chrome verwenden möchten, müssen Sie diese Dateien entweder auf einem Server hosten oder das Flag --allow-file-access-from-files verwenden, wenn Sie Chrome über die Befehlszeile starten. Starten Sie Chrome unter OS X wie folgt:

1
$ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --allow-file-access-from-files

Die Verwendung dieses Flags wird jedoch in einer Produktionsumgebung nicht empfohlen. Daher ist es am besten, diese Dateien auf einem Webserver zu hosten und Ihre Web-Worker in einem unterstützten Browser zu testen.

Debuggen von Workern und Fehlerbehandlung

Wenn Sie keinen Zugriff auf console haben, ist dies nicht trivial. Dank der Chrome Developer Tools können Sie den Worker-Code jedoch wie jeden anderen JavaScript-Code debuggen.

Damn this slow machine !!Damn this slow machine !!Damn this slow machine !!

Um alle von Web-Workern ausgelösten Fehler zu behandeln, können Sie auf das error-Ereignis warten, das ein ErrorEvent-Objekt auffüllt. Sie können dieses Objekt untersuchen, um die detaillierte Fehlerursache zu ermitteln.

1
primeWorker.addEventListener('error', function(error){
2
    console.log(' Error Caused by worker: '+error.filename
3
        + ' at line number: '+error.lineno
4
        + ' Detailed Message: '+error.message);
5
});

Mehrere Worker-Threads

Obwohl es üblich ist, dass mehrere Arbeitsthreads die Arbeit untereinander aufteilen, ist Vorsicht geboten. Die offizielle Spezifikation legt fest, dass diese Arbeiter relativ schwer sind und voraussichtlich langlebige Skripte sind, die im Hintergrund ausgeführt werden. Web-Worker sind aufgrund ihrer hohen Kosten für die Startleistung und der hohen Speicherkosten pro Instanz nicht für eine große Anzahl vorgesehen.

Kurze Einführung in Shared Worker

Die Spezifikation beschreibt zwei Arten von Arbeitnehmern: engagierte und geteilte. Bisher haben wir Beispiele für engagierte Mitarbeiter gesehen. Sie sind direkt mit ihrem Erstellerskript/ihrer Erstellerseite in dem Sinne verknüpft, dass sie eine Eins-zu-Eins-Beziehung zu dem Skript/der Seite haben, mit dem sie erstellt wurden. Auf der anderen Seite können freigegebene Mitarbeiter von allen Seiten eines Ursprungs gemeinsam genutzt werden (dh alle Seiten oder Skripte auf demselben Ursprung können mit einem freigegebenen Mitarbeiter kommunizieren).

Um einen freigegebenen Worker zu erstellen, übergeben Sie einfach die URL des Skripts oder den Namen des Workers an den SharedWorker-Konstruktor.

Der Hauptunterschied bei der Verwendung von gemeinsam genutzten Mitarbeitern besteht darin, dass sie einem port zugeordnet sind, um den Überblick über das übergeordnete Skript zu behalten, das auf sie zugreift.

Das folgende Codefragment erstellt einen freigegebenen Mitarbeiter, registriert einen Rückruf zum Abhören aller vom Mitarbeiter gesendeten Nachrichten und sendet eine Nachricht an den freigegebenen Mitarbeiter:

1
var sharedWorker = new SharedWorker('findPrime.js');
2
sharedWorker.port.onmessage = function(event){
3
    ...
4
}
5
6
sharedWorker.port.postMessage('data you want to send');

Ebenso kann ein Mitarbeiter auf das connect-Ereignis warten, das empfangen wird, wenn ein neuer Client versucht, eine Verbindung zum Mitarbeiter herzustellen, und dann eine entsprechende Nachricht an ihn sendet.

1
onconnect = function(event) {
2
    // event.source contains the reference to the client's port

3
    var clientPort = event.source;
4
    // listen for any messages send my this client

5
    clientPort.onmessage = function(event) {
6
        // event.data contains the message send by client

7
        var data = event.data;
8
        ....
9
        // Post Data after processing

10
        clientPort.postMessage('processed data');
11
    }
12
};

Aufgrund ihrer gemeinsamen Natur können Sie denselben Status auf verschiedenen Registerkarten derselben Anwendung beibehalten, da beide Seiten auf unterschiedlichen Registerkarten dasselbe Skript für gemeinsam genutzte Worker verwenden, um den Status zu verwalten und zu melden. Für weitere Informationen zu geteilten Mitarbeitern empfehle ich Ihnen, die Spezifikation zu lesen.


Praktische Anwendungsfälle

Web-Worker sind aufgrund ihrer hohen Kosten für die Startleistung und der hohen Speicherkosten pro Instanz nicht für eine große Anzahl vorgesehen.

Ein reales Szenario kann sein, wenn Sie gezwungen sind, sich mit einer synchronen Drittanbieter-API zu befassen, die den Hauptthread zwingt, auf ein Ergebnis zu warten, bevor Sie mit der nächsten Anweisung fortfahren. In einem solchen Fall können Sie diese Aufgabe an einen neu erstellten Mitarbeiter delegieren, um die asynchrone Funktion zu Ihrem Vorteil zu nutzen.

Web-Worker zeichnen sich auch durch Abrufsituationen aus, in denen Sie kontinuierlich ein Ziel im Hintergrund abfragen und Nachrichten an den Haupt-Thread senden, wenn neue Daten eintreffen.

Möglicherweise müssen Sie auch eine große Datenmenge verarbeiten, die vom Server zurückgegeben wird. Traditionell wirkt sich die Verarbeitung vieler Daten negativ auf die Reaktionsfähigkeit der Anwendung aus, wodurch die Benutzererfahrung inakzeptabel wird. Eine elegantere Lösung würde die Verarbeitungsarbeit auf mehrere Mitarbeiter aufteilen, um nicht überlappende Teile der Daten zu verarbeiten.

Andere Anwendungsfälle könnten die Analyse von Video- oder Audioquellen mithilfe mehrerer Web-Worker sein, die jeweils an einem vordefinierten Teil des Problems arbeiten.


Schlussfolgerung

Stellen Sie sich die Leistung vor, die mit mehreren Threads in einer Umgebung mit nur einem Thread verbunden ist.

Wie bei vielen Dingen in der HTML5-Spezifikation wird die Web-Worker-Spezifikation weiterentwickelt. Wenn Sie Web-Worker planen, schadet es nicht, der Spezifikation einen Blick zu geben.

Die browserübergreifende Unterstützung ist für engagierte Mitarbeiter mit aktuellen Versionen von Chrome, Safari und Firefox recht gut. Selbst der IE bleibt nicht zu weit zurück, da der IE10 die Verantwortung übernimmt. Freigegebene Mitarbeiter werden jedoch nur in aktuellen Versionen von Chrome und Safari unterstützt. Überraschenderweise unterstützt die neueste Version des in Android 4.0 verfügbaren Android-Browsers keine Web-Worker, obwohl sie in Version 2.1 unterstützt wurden. Apple hat auch Web Worker-Unterstützung ab iOS 5.0 integriert.

Stellen Sie sich die Leistung vor, die mit mehreren Threads in einer Umgebung mit nur einem Thread verbunden ist. Die Möglichkeiten sind endlos!