Wie man CPU-gebundene Aufgaben mit Web Workern bearbeitet
JavaScript wird oft als ein einzelfädiges (single-threaded) Programmiermodell bezeichnet, da Ihr Webanwendungscode sequentiell ausgeführt wird – eine Aufgabe nach der anderen, in einem einzigen Thread. Selbst wenn Sie eine Web-App auf einem Gerät mit mehreren Kernen verwenden, nutzt JavaScript nur einen Kern. Wenn eine Aufgabe im Haupt-Thread ausgeführt wird, müssen alle nachfolgenden Aufgaben warten, bis diese abgeschlossen ist. Dauert eine Aufgabe lange, blockiert sie den Haupt-Thread und verhindert, dass die verbleibenden Aufgaben ausgeführt werden. Die meisten dieser blockierenden Aufgaben sind rechenintensive Aufgaben, auch CPU-gebundene Aufgaben genannt, wie zum Beispiel das Verarbeiten von Grafiken, mathematische Berechnungen und die Komprimierung von Videos oder Bildern.
Blockierende und nicht-blockierende Aufgaben
CPU-gebundene Aufgaben
CPU-gebundene Aufgaben beanspruchen die CPU, bis die Aufgabe abgeschlossen ist, und blockieren dabei den Haupt-Thread. Selbst wenn sie in ein Promise gewickelt werden, blockieren sie weiterhin den Haupt-Thread. Benutzer können dies bemerken, da die Benutzeroberfläche der Web-App (UI) einfrieren kann und JavaScript-abhängige Funktionen möglicherweise nicht mehr funktionieren.
I/O-gebundene Aufgaben
Zusätzlich zu CPU-gebundenen Aufgaben gibt es auch I/O-gebundene Aufgaben, die nicht blockierend sind. Diese I/O-gebundenen Aufgaben verbringen die meiste Zeit damit, Anfragen an das Betriebssystem (OS) zu senden und auf eine Antwort zu warten. Ein Beispiel ist eine Netzwerkabfrage, die die Fetch-API an einen Server sendet. Wenn Sie mit der Fetch-API eine Ressource von einem Server abrufen, übernimmt das Betriebssystem die Aufgabe, und die Fetch-API wartet auf die Antwort des Betriebssystems.
Während dieser Zeit werden die Rückruffunktionen (Callbacks) der Fetch-API in eine Warteschlange ausgelagert, wo sie auf die Antwort des Betriebssystems warten. Dadurch wird der Haupt-Thread entlastet, sodass er die nachfolgenden Aufgaben ausführen kann. Sobald die Antwort eingegangen ist, werden die Callbacks, die mit dem Fetch-API-Aufruf verknüpft sind, im Haupt-Thread ausgeführt. Die Leistung von I/O-gebundenen Aufgaben hängt davon ab, wie lange das Betriebssystem benötigt, um die Aufgabe zu beenden. Die meisten I/O-gebundenen Aufgaben, wie Fetch, implementieren Promises, die die Funktionen definieren, die ausgeführt werden sollen, wenn das Promise aufgelöst wird; also wenn das Betriebssystem die Aufgabe beendet hat und eine Antwort zurückgibt.
Lösungen zur Bearbeitung von blockierenden Aufgaben
Um das Problem blockierender Aufgaben zu lösen, haben Browser die Web Workers API eingeführt, um Multithreading-Unterstützung im Browser zu bieten. Mit Web Workern können Sie eine CPU-intensive Aufgabe an einen anderen Thread auslagern, wodurch der Haupt-Thread entlastet ist. Der Haupt-Thread führt JavaScript-Code auf einem Geräte-Kern aus, und man führt die ausgelagerte Aufgabe auf einem anderen Kern aus. Die beiden Threads können über Nachrichtenübermittlung kommunizieren und Daten austauschen.
Übersicht des Tutorials
In diesem Tutorial werden Sie:
- Eine CPU-gebundene Aufgabe erstellen, die den Haupt-Thread im Browser blockiert, und beobachten, wie sich dies auf die Web-App auswirkt.
- Versuchen, eine CPU-gebundene Aufgabe mit Promises nicht-blockierend zu machen.
- Einen Web Worker erstellen, um eine CPU-gebundene Aufgabe an einen anderen Thread auszulagern und zu verhindern, dass sie den Haupt-Thread blockiert.
Voraussetzungen
Um diesem Tutorial zu folgen, benötigen Sie:
- Ein Gerät mit zwei oder mehr Kernen und einem modernen Webbrowser.
- Eine lokale Node.js-Umgebung auf Ihrem System. Folgen Sie der entsprechenden Anleitung für Ihr Betriebssystem, um Node.js zu installieren und eine lokale Entwicklungsumgebung einzurichten.
- Grundlegende Kenntnisse über das Event-Loop, Callbacks und Promises. Sie können mehr darüber in JavaScript-Tutorials lernen.
- Grundkenntnisse in HTML, CSS und JavaScript. Sie können auf Grundlagenleitfäden für diese Sprachen zurückgreifen.
Schritt 1: Erstellen einer CPU-gebundenen Aufgabe ohne Web Worker
In diesem Schritt erstellen Sie eine Web-App, die sowohl blockierende CPU-gebundene Aufgaben als auch nicht-blockierende Aufgaben enthält. Die Anwendung wird drei Buttons haben:
- Der erste Button startet die blockierende Aufgabe, die aus einer
for
-Schleife besteht, die etwa fünf Milliarden Mal iteriert. - Der zweite Button erhöht einen auf der Webseite angezeigten Wert.
- Der dritte Button ändert die Hintergrundfarbe der Web-App.
Die Buttons zum Erhöhen des Werts und zum Ändern der Hintergrundfarbe sind nicht-blockierende Aufgaben, während die blockierende Aufgabe die Benutzeroberfläche einfrieren wird.
Einrichten des Projekts
Erstellen Sie zunächst ein Projektverzeichnis und navigieren Sie in dieses:
mkdir workers_demo cd workers_demo
Erstellen Sie anschließend die Haupt-HTML-Datei für Ihre Anwendung:
nano index.html
HTML-Struktur hinzufügen
Fügen Sie in der index.html
die folgende Struktur hinzu:
Stil hinzufügen
Erstellen und öffnen Sie die main.css
-Datei, um die Elemente zu gestalten:
nano main.css
Fügen Sie die folgenden CSS-Regeln hinzu:
body { background: #fff; font-size: 16px; } .wrapper { max-width: 600px; margin: 0 auto; } .total-count { margin-bottom: 34px; font-size: 32px; text-align: center; } .buttons { border: 1px solid green; padding: 1rem; margin-bottom: 16px; } .btn { border: 0; padding: 1rem; } .btn-blocking { background-color: #f44336; color: #fff; } #changebtn { background-color: #4caf50; color: #fff; }
JavaScript-Funktionalität hinzufügen
Erstellen und öffnen Sie die main.js
-Datei, um Interaktivität hinzuzufügen:
nano main.js
Fügen Sie den folgenden JavaScript-Code hinzu, um die Funktionalität der Buttons zu implementieren:
const blockingBtn = document.getElementById("blockbtn"); const incrementBtn = document.getElementById("incrementbtn"); const changeColorBtn = document.getElementById("changebtn"); const output = document.querySelector(".output"); const totalCountEl = document.querySelector(".total-count"); totalCountEl.textContent = 0; incrementBtn.addEventListener("click", function incrementValue() { let counter = totalCountEl.textContent; counter++; totalCountEl.textContent = counter; }); changeColorBtn.addEventListener("click", function changeBackgroundColor() { const colors = ["#009688", "#ffc107", "#dadada"]; const randomIndex = Math.floor(Math.random() * colors.length); const randomColor = colors[randomIndex]; document.body.style.background = randomColor; }); blockingBtn.addEventListener("click", function blockMainThread() { let counter = 0; for (let i = 0; i < 5_000_000_000; i++) { counter++; } output.textContent = `Ergebnis: ${counter}`; });
Testen der Anwendung
Starten Sie einen einfachen Webserver mit dem Befehl npx serve
und öffnen Sie die Datei index.html
in Ihrem Browser. Interagieren Sie mit den Buttons und beobachten Sie, wie die blockierende Aufgabe die Benutzeroberfläche einfriert, während die nicht-blockierenden Aufgaben wie erwartet funktionieren.
Schritt 2: Auslagern einer CPU-gebundenen Aufgabe mit Promises
Obwohl Promises häufig für das Management asynchroner Aufgaben in JavaScript verwendet werden, machen sie CPU-gebundene Aufgaben nicht automatisch nicht-blockierend. Diese Aufgaben nutzen weiterhin den Haupt-Thread vollständig aus, bis sie abgeschlossen sind. In diesem Schritt demonstrieren Sie diese Einschränkung, indem Sie die blockierende Aufgabe in ein Promise verpacken.
Erstellen einer Promise-basierten Funktion
Öffnen Sie die main.js
-Datei und definieren Sie eine neue Funktion, calculateCount
, die die CPU-intensive Aufgabe in ein Promise einbindet:
function calculateCount() { return new Promise((resolve, reject) => { let counter = 0; for (let i = 0; i < 5_000_000_000; i++) { counter++; } resolve(counter); }); }
Diese Funktion initialisiert ein neues Promise. Innerhalb des Promises iteriert die CPU-intensive Schleife fünf Milliarden Mal, um die Berechnung auszuführen. Sobald die Schleife abgeschlossen ist, wird das Ergebnis an die resolve
-Funktion übergeben, um das Ende der Aufgabe anzuzeigen.
Verwendung des Promises im Event Listener
Ändern Sie den Event Listener blockMainThread
, sodass er die Funktion calculateCount
mit async/await
verwendet:
blockingBtn.addEventListener("click", async function blockMainThread() { const counter = await calculateCount(); output.textContent = `Ergebnis: ${counter}`; });
Durch das Hinzufügen des async
-Schlüsselworts zur Funktion blockMainThread
wird diese asynchron. Das await
-Schlüsselwort pausiert die Ausführung, bis das Promise, das von calculateCount
zurückgegeben wird, aufgelöst ist. Sobald dies geschieht, wird das Ergebnis der Variablen counter
zugewiesen und im output
-Element angezeigt.
Testen der Anwendung
Speichern Sie Ihre Änderungen und aktualisieren Sie die Anwendung in Ihrem Browser. Klicken Sie auf den Button „Blockierende Aufgabe“ und beobachten Sie das Verhalten der Benutzeroberfläche:
- Die blockierende Aufgabe startet und die Benutzeroberfläche friert ein, bis die Aufgabe abgeschlossen ist.
- Nach Abschluss der Aufgabe wird das Ergebnis im Ausgabebereich angezeigt.
Obwohl Sie ein Promise verwendet haben, blockiert die CPU-gebundene Aufgabe weiterhin den Haupt-Thread, da die Berechnung vollständig im Haupt-Thread erfolgt. Dies zeigt, dass Promises CPU-gebundene Aufgaben nicht nicht-blockierend machen.
Fazit
Promises sind effektiv für das Management asynchroner Aufgaben, wie beispielsweise I/O-gebundene Operationen, können jedoch keine CPU-intensiven Berechnungen an einen separaten Thread auslagern. Um dieses Problem zu lösen, können Sie Web Worker verwenden, wie im nächsten Schritt beschrieben.
Schritt 3: Auslagern einer CPU-gebundenen Aufgabe mit Web Workern
Um CPU-gebundene Aufgaben zu bearbeiten, ohne den Haupt-Thread zu blockieren, können Sie Web Worker verwenden. Web Worker führen JavaScript-Code in einem separaten Thread aus, wodurch der Haupt-Thread reaktionsfähig bleibt. In diesem Schritt verschieben Sie die CPU-intensive Berechnung in einen Web Worker.
Erstellen einer Worker-Datei
Erstellen Sie eine neue Datei namens worker.js
und fügen Sie die CPU-intensive Aufgabe hinzu:
let counter = 0; for (let i = 0; i < 5_000_000_000; i++) { counter++; } postMessage(counter);
In diesem Code iteriert die for
-Schleife fünf Milliarden Mal, um die CPU-intensive Berechnung auszuführen. Das Ergebnis wird anschließend mit der Methode postMessage
an den Haupt-Thread gesendet.
Ändern des Hauptskripts
Öffnen Sie die main.js
-Datei und ändern Sie den Event Listener blockMainThread
, damit er den Web Worker verwendet:
blockingBtn.addEventListener("click", function blockMainThread() { const worker = new Worker("worker.js"); worker.onmessage = (msg) => { output.textContent = `Ergebnis: ${msg.data}`; }; });
Hier wird ein neuer Web Worker mit dem Konstruktor Worker
und dem Pfad zur Datei worker.js
erstellt. Der Worker läuft in einem separaten Thread. Der onmessage
-Event Listener hört auf Nachrichten des Workers, die mit der Methode postMessage
in worker.js
gesendet werden.
Testen der Anwendung
Speichern Sie Ihre Änderungen und aktualisieren Sie die Anwendung in Ihrem Browser. Klicken Sie auf den Button „Blockierende Aufgabe“ und beobachten Sie das Verhalten:
- Die blockierende Aufgabe wird im Web Worker ausgeführt, ohne die Benutzeroberfläche zu blockieren.
- Die Buttons „Erhöhen“ und „Hintergrund ändern“ bleiben während der Ausführung der CPU-intensiven Aufgabe vollständig reaktionsfähig.
- Nach Abschluss der Berechnung wird das Ergebnis im Ausgabebereich angezeigt.
Erklärung der Verbesserungen
Durch die Verwendung eines Web Workers wird die CPU-intensive Aufgabe in einem separaten Thread ausgeführt, wodurch der Haupt-Thread entlastet wird. Dies verbessert die Benutzererfahrung erheblich, da die Benutzeroberfläche auch während der Ausführung aufwendiger Berechnungen interaktiv bleibt.
Fazit
In diesem Schritt haben Sie erfolgreich eine CPU-intensive Aufgabe an einen Web Worker ausgelagert, um zu verhindern, dass sie den Haupt-Thread blockiert. Web Worker sind eine effektive Lösung für die Bearbeitung rechenintensiver Aufgaben in JavaScript. Für erweiterte Anwendungsfälle können Sie die Dokumentation zur Web Workers API erkunden, um mehr über Shared Workers und Service Workers zu erfahren, die zusätzliche Funktionen wie Offline-Funktionalität und Inter-Thread-Kommunikation bieten.
Fazit
In diesem Tutorial haben Sie eine Webanwendung erstellt, um zu untersuchen, wie JavaScript CPU-gebundene und I/O-gebundene Aufgaben verarbeitet. Sie haben gelernt, dass Promises zwar effektiv für die Verwaltung asynchroner Operationen wie I/O-Aufgaben sind, jedoch keine CPU-gebundenen Aufgaben nicht-blockierend machen können. Um dieses Problem zu lösen, haben Sie eine CPU-intensive Aufgabe erfolgreich an einen Web Worker ausgelagert, wodurch die Aufgabe in einem separaten Thread ausgeführt und der Haupt-Thread reaktionsfähig gehalten wurde.
Zentrale Erkenntnisse
- JavaScript’s einzelfädige Natur: JavaScript führt Code auf einem einzigen Thread aus, was bei der Bearbeitung von CPU-intensiven Aufgaben zu Blockierungen führen kann.
- Promises: Promises helfen bei der Verwaltung asynchroner Operationen, können jedoch keine rechenintensiven Aufgaben vom Haupt-Thread auslagern.
- Web Worker: Web Worker ermöglichen die Ausführung von Aufgaben in einem separaten Thread, wodurch der Haupt-Thread entlastet und die Benutzeroberfläche reaktionsfähig gehalten ist.
Nächste Schritte
Nachdem Sie gelernt haben, wie Sie Web Worker zur Bearbeitung CPU-gebundener Aufgaben einsetzen, können Sie Ihr Wissen erweitern, indem Sie weitere Funktionen und Anwendungsfälle von Web Workern erkunden:
- Shared Workers: Ermöglichen die Kommunikation zwischen mehreren Skripten, die in verschiedenen Fenstern, Tabs oder Iframes laufen.
- Service Workers: Bieten Offline-Funktionalität und ermöglichen Hintergrundsynchronisierung für Webanwendungen.
- Nachrichtenübermittlung: Lernen Sie, wie Sie komplexe Datenstrukturen zwischen Threads mithilfe des strukturierten Klonens senden.
Durch die Integration von Web Workern in Ihre Projekte können Sie rechenintensive Aufgaben effektiv bearbeiten und so eine flüssige und reaktionsschnelle Benutzererfahrung sicherstellen.