Parallelität und Multithreading in JavaScript: Fortgeschrittenes Niveau
\n\nJavaScript gilt traditionell als einsprachig, jedoch bietet das moderne Node.js-Ökosystem leistungsstarke Werkzeuge für die Arbeit mit Parallelität und echtem Multithreading. In diesem Artikel behandeln wir fortgeschrittene Techniken: von den Feinheiten der Event Loop bis zur Nutzung von Worker Threads, SharedArrayBuffer und Atomics. Sie lernen, Last effizient zu verteilen, Race Conditions zu vermeiden und skalierbare Anwendungen zu erstellen.
\n\n1. Event Loop, Mikrotasks und Makrotasks: Das Fundament der Parallelität
\n\nDas Verständnis der Event Loop ist der Schlüssel zur Steuerung von Parallelität ohne Blockaden. In Node.js besteht die Ereignisschleife aus mehreren Phasen: Timer, I/O-Callbacks, Idle, Poll, Check (setImmediate) und Close. Es ist wichtig, zwischen Mikrotasks (Promise, process.nextTick) und Makrotasks (setTimeout, setInterval, I/O) zu unterscheiden.
\n\nMikrotasks werden unmittelbar nach Abschluss der aktuellen Phase, vor der nächsten Makrotask, ausgeführt. Dies kann zu einer „Aushungerung“ der Event Loop führen, wenn Mikrotasks unendlich erzeugt werden.
\n\n// Beispiel: Mikrotasks blockieren Makrotasks\nsetTimeout(() => console.log('Makrotask'), 0);\n\nPromise.resolve().then(() => {\n // Unendliche Mikrotask\n process.nextTick(() => {\n console.log('Mikrotask');\n // Rekursiver Aufruf blockiert die Event Loop\n process.nextTick(() => process.nextTick(() => {}));\n });\n});\n// Ausgabe: 'Mikrotask' (unendlich), 'Makrotask' wird nie ausgeführt\n\nFür fortgeschrittene Kontrolle verwenden Sie queueMicrotask() und setImmediate(). Denken Sie daran: process.nextTick hat Priorität vor Promise-Mikrotasks.
2. Worker Threads: Echtes Multithreading in Node.js
\n\nDas Modul worker_threads ermöglicht die Ausführung von JavaScript in parallelen Threads. Jeder Worker hat eine eigene V8-Instanz und isolierten Speicher. Der Datenaustausch erfolgt über Nachrichten oder SharedArrayBuffer.
Erstellen eines einfachen Workers:
\n\n// main.js\nconst { Worker } = require('worker_threads');\n\nfunction runWorker(file, data) {\n return new Promise((resolve, reject) => {\n const worker = new Worker(file, { workerData: data });\n worker.on('message', resolve);\n worker.on('error', reject);\n worker.on('exit', (code) => {\n if (code !== 0) reject(new Error(`Worker mit Code ${code} gestoppt`));\n });\n });\n}\n\n// Ausführung\nrunWorker('./worker.js', { iterations: 1e9 })\n .then(result => console.log('Ergebnis:', result))\n .catch(err => console.error(err));\n\n// worker.js\nconst { parentPort, workerData } = require('worker_threads');\n\nfunction heavyComputation(n) {\n let sum = 0;\n for (let i = 0; i < n; i++) { sum += Math.sqrt(i); }\n return sum;\n}\n\nparentPort.postMessage(heavyComputation(workerData.iterations));\n\nWichtig: Der Worker hat keinen Zugriff auf das DOM, globale Prozessobjekte (process.env) und Module, die im Hauptthread geladen wurden. Übergeben Sie nur serialisierbare Daten.
\n\nWorker-Pool
\nEs ist aufwändig, für jede Aufgabe einen neuen Worker zu erstellen. Verwenden Sie einen Pool:
\n\nclass WorkerPool {\n constructor(file, size = 4) {\n this.workers = [];\n this.queue = [];\n for (let i = 0; i < size; i++) {\n const worker = new Worker(file);\n worker.on('message', (result) => this._handleResult(worker, result));\n worker.on('error', (err) => this._handleError(worker, err));\n this.workers.push(worker);\n }\n }\n\n runTask(data) {\n return new Promise((resolve, reject) => {\n this.queue.push({ data, resolve, reject });\n this._processQueue();\n });\n }\n\n _processQueue() {\n if (this.queue.length === 0) return;\n const worker = this.workers.find(w => !w.busy);\n if (!worker) return;\n worker.busy = true;\n const task = this.queue.shift();