Eenvoudige demonstratie van parallelle verwerking met worker_threads in Node.js,
inclusief synchronisatie via een main thread en bestandsgebaseerde communicatie.
Het programma demonstreert hoe meerdere threads samenwerken zonder gedeeld geheugen te gebruiken. Bij uitvoering gebeurt het volgende:
- Thread-0 start als eerste en genereert een willekeurige string (via
crypto.randomBytes). Deze string schrijft hij weg naar een tekstbestand (thread-0_<timestamp>.txt) in deoutput/map. - Thread-0 meldt zijn resultaat terug aan de orchestrator: het pad naar het aangemaakte bestand en de random string.
- De overige threads (1 t/m N) starten parallel, elk met het pad naar het bestand van thread-0.
Zij lezen de random string uit dat bestand en schrijven vervolgens hun eigen bestand (
thread-N_<timestamp>.txt) met diezelfde string erin. - Alle bestanden bevatten dezelfde random string, maar zijn door verschillende threads aangemaakt. Zo is zichtbaar dat de waarde consistent wordt doorgegeven via het bestandssysteem.
Het kernpunt van de demo: threads communiceren niet via gedeeld geheugen, maar via bestanden en berichten (postMessage). De random string die thread-0 genereert, wordt door alle andere threads hergebruikt — zonder dat ze die zelf berekenen.
Node.js is vereist om dit script te draaien. Controleer eerst of het al aanwezig is:
node --versionZo niet, installeer het via nodejs.org (kies de LTS-versie) of via een package manager:
| Platform | Methode | Commando / Link |
|---|---|---|
| Windows | winget | winget install OpenJS.NodeJS.LTS |
| Windows | Chocolatey | choco install nodejs-lts |
| Windows | Handmatig | nodejs.org/en/download → Windows Installer (.msi) |
| macOS | Homebrew | brew install node |
| macOS | Handmatig | nodejs.org/en/download → macOS Installer (.pkg) |
| Ubuntu/Debian | apt | sudo apt install nodejs npm |
| Linux (algemeen) | Handmatig | nodejs.org/en/download → Linux Binaries |
node index.jsMultiThread/
├── index.js # Orchestrator
├── worker.js # Worker logica (hoofd + gewone threads)
└── output/ # Gegenereerde bestanden (aangemaakt bij uitvoeren)
De orchestrator start eerst thread-0 en wacht synchroon tot die klaar is:
orchestrate()
└── await startWorker(0) ← blokkeert totdat thread-0 klaar is
└── worker.js (thread 0)
├── genereert random string via crypto.randomBytes
├── schrijft thread-0_<timestamp>.txt
└── parentPort.postMessage({ filepath, randomString })
parentPort.postMessage() stuurt het bestandspad en de random string terug naar
de orchestrator, die deze ontvangt via de message-event handler.
Zodra thread-0 klaar is, worden alle worker threads parallel gestart via
Promise.allSettled(). Elk krijgt het pad naar het hoofd bestand mee via workerData:
orchestrate()
└── Promise.allSettled([
startWorker(1, mainFilePath), ─┐
startWorker(2, mainFilePath), ├─ parallel
startWorker(3, mainFilePath), │
... ─┘
])
└── worker.js (thread N)
├── leest thread-0_<timestamp>.txt
├── extraheert random string via regex
└── schrijft thread-N_<timestamp>.txt
De communicatie verloopt niet via gedeeld geheugen maar via twee kanalen:
| Channel | Richting | Wat |
|---|---|---|
workerData |
orchestrator → worker | thread-nummer, output-map, pad hoofd bestand |
parentPort.postMessage |
worker → orchestrator | bestandspad, random string (alleen thread-0) |
| File system | thread-0 → thread-1..N | random string via thread-0_*.txt |
Stap 1 — worker.js (thread-0) bepaalt het pad en stuurt het terug:
// worker.js
const filepath = path.join(outputDir, filename); // volledig pad van het nieuwe bestand
fs.writeFileSync(filepath, content, 'utf8');
parentPort.postMessage({ filepath, randomString }); // ← stuurt pad terug naar orchestratorStap 2 — index.js ontvangt het via de message event en resolvet de Promise:
// index.js — binnen startWorker()
worker.on('message', (data) => resolve({ threadNumber, ...data }));
// ↑
// spread van { filepath, randomString }
// result: { threadNumber: 0, filepath: '...', randomString: '...' }Stap 3 — await geeft het object terug, zodat filepath beschikbaar is:
// index.js — in orchestrate()
const main = await startWorker(0);
// main = { threadNumber: 0, filepath: 'C:\\...\\thread-0_....txt', randomString: '8f3e...' }
startWorker(i + 1, main.filepath); // ← filepath doorgeven aan elke workerDe keten is dus:
fs.writeFileSync bepaalt pad → parentPort.postMessage stuurt het → worker.on('message') resolvet de Promise → await geeft het terug.
parentPort is het ingebouwde communicatiekanaal van een worker terug naar de orchestrator.
Elke Worker die in index.js aangemaakt wordt, krijgt automatisch een gekoppeld poortpaar:
index.js (orchestrator) worker.js (thread)
┌──────────────────────────┐ ┌──────────────────────────┐
│ │ │ │
│ worker.on('message', │◄────────│ parentPort │
│ handler) │ │ .postMessage(data) │
│ │ │ │
└──────────────────────────┘ └──────────────────────────┘
| Kant | Rol | |
|---|---|---|
parentPort |
worker.js | zender — stuurt data naar de orchestrator |
worker.on('message', ...) |
index.js | ontvanger — reageert op binnenkomende data |
Zonder parentPort heeft een worker geen terugweg. Dan is workerData de enige communicatie,
maar die gaat slechts één richting op (orchestrator → worker).
t=0 [orchestrator] start thread-0
t=1 [thread-0] schrijft thread-0_timestamp.txt ← random string hierin
t=2 [orchestrator] ontvangt filepath van thread-0
t=3 [thread-1..5] starten parallel
t=4 [thread-1..5] lezen elk thread-0_timestamp.txt
t=5 [thread-1..5] schrijven eigen bestand met dezelfde random string
thread-0_...txt (hoofd bestand):
Thread nummer : 0 (hoofd thread)
Aangemaakt op : 2026-05-24T14:47:42.602Z
Random string : 8f3e833ed52b570b2a4de335d7bf9eeb
thread-3_...txt (worker bestand):
Thread nummer : 3
Aangemaakt op : 2026-05-24T14:47:42.724Z
Random string : 8f3e833ed52b570b2a4de335d7bf9eeb (gelezen uit thread-0)
sequenceDiagram
participant O as Orchestrator<br/>(index.js)
participant T0 as Thread-0<br/>(main)
participant FS as File system
participant T1 as Thread-1
participant T2 as Thread-2
participant TN as Thread-N
O->>T0: startWorker(0)
activate T0
T0->>T0: crypto.randomBytes() → randomString
T0->>FS: schrijf thread-0_timestamp.txt
T0-->>O: postMessage({ filepath, randomString })
deactivate T0
note over O: await voltooid — thread-0 klaar
par parallel
O->>T1: startWorker(1, mainFilePath)
and
O->>T2: startWorker(2, mainFilePath)
and
O->>TN: startWorker(N, mainFilePath)
end
activate T1
activate T2
activate TN
T1->>FS: lees thread-0_timestamp.txt
T2->>FS: lees thread-0_timestamp.txt
TN->>FS: lees thread-0_timestamp.txt
T1->>FS: schrijf thread-1_timestamp.txt
T2->>FS: schrijf thread-2_timestamp.txt
TN->>FS: schrijf thread-N_timestamp.txt
T1-->>O: postMessage({ filepath })
T2-->>O: postMessage({ filepath })
TN-->>O: postMessage({ filepath })
deactivate T1
deactivate T2
deactivate TN
note over O: Promise.allSettled() voltooid
Pas THREAD_COUNT in index.js aan om meer of minder worker threads te starten.
Hetzelfde patroon (hoofd thread eerst, daarna parallel) is in Java realiseerbaar met twee gangbare frameworks: Vert.x en CompletableFuture / ExecutorService (standaard Java).
| Concept | Node.js (worker_threads) |
Java Vert.x | Java CompletableFuture |
|---|---|---|---|
| Thread starten | new Worker(file, { workerData }) |
vertx.deployVerticle(worker) |
CompletableFuture.supplyAsync(task, pool) |
| Data meegeven | workerData object |
DeploymentOptions.setConfig() |
lambda closure / constructor argument |
| Terugsturen naar orchestrator | parentPort.postMessage() |
EventBus.send() |
return waarde van supplyAsync |
| Wachten op main thread | await startWorker(0) |
handler callback in deployVerticle |
.get() of .join() op de Future |
| Parallel starten | Promise.allSettled([...]) |
meerdere deployVerticle aanroepen |
CompletableFuture.allOf(...) |
| Communication model | message passing (ports) | event bus (publish/subscribe) | gedeeld geheugen / Future return |
Vert.x werkt met Verticles (vergelijkbaar met workers) die communiceren via een interne Event Bus. De orchestrator deploys eerst de hoofd-Verticle en wacht op een reply via de event bus voordat de rest gestart wordt.
// Orchestrator.java
public class Orchestrator extends AbstractVerticle {
private static final int THREAD_COUNT = 5;
@Override
public void start(Promise<Void> startPromise) {
DeploymentOptions workerOpts = new DeploymentOptions().setWorker(true);
// Stap 1: deploy hoofd-Verticle (thread-0) en wacht op reply
vertx.deployVerticle(new WorkerVerticle(0, null), workerOpts, res -> {
if (res.failed()) { startPromise.fail(res.cause()); return; }
// Ontvang filepath van thread-0 via event bus
vertx.eventBus().<String>request("thread.0.done", "", reply -> {
String mainFilePath = reply.result().body();
// Stap 2: start overige Verticles parallel
List<Future<String>> futures = new ArrayList<>();
for (int i = 1; i <= THREAD_COUNT; i++) {
int threadNum = i;
Promise<String> p = Promise.promise();
futures.add(p.future());
vertx.deployVerticle(new WorkerVerticle(threadNum, mainFilePath), workerOpts);
}
CompositeFuture.all(futures).onComplete(all -> startPromise.complete());
});
});
}
}
// WorkerVerticle.java
public class WorkerVerticle extends AbstractVerticle {
private final int threadNumber;
private final String mainFilePath;
public WorkerVerticle(int threadNumber, String mainFilePath) {
this.threadNumber = threadNumber;
this.mainFilePath = mainFilePath;
}
@Override
public void start() throws Exception {
String outputDir = "output";
String timestamp = Instant.now().toString().replace(":", "-").replace(".", "-");
String filename = "thread-" + threadNumber + "_" + timestamp + ".txt";
Path filepath = Paths.get(outputDir, filename);
if (threadNumber == 0) {
// Hoofd thread: genereer random string
String randomString = generateRandomHex(16);
String content = "Thread nummer : 0 (hoofd thread)\n"
+ "Aangemaakt op : " + Instant.now() + "\n"
+ "Random string : " + randomString;
Files.writeString(filepath, content);
// Stuur filepath terug via event bus (equivalent van parentPort.postMessage)
vertx.eventBus().send("thread.0.done", filepath.toString());
} else {
// Worker thread: lees random string uit hoofd bestand
String mainContent = Files.readString(Path.of(mainFilePath));
String randomString = extractRandomString(mainContent);
String content = "Thread nummer : " + threadNumber + "\n"
+ "Aangemaakt op : " + Instant.now() + "\n"
+ "Random string : " + randomString + " (gelezen uit thread-0)";
Files.writeString(filepath, content);
}
}
}Overeenkomsten met Node.js:
| Node.js | Vert.x equivalent |
|---|---|
new Worker(file, { workerData }) |
vertx.deployVerticle(new WorkerVerticle(...)) |
parentPort.postMessage({ filepath }) |
vertx.eventBus().send("thread.0.done", filepath) |
worker.on('message', handler) |
vertx.eventBus().request("thread.0.done", "", reply -> ...) |
Promise.allSettled([...]) |
CompositeFuture.all(futures) |
De standaard Java aanpak zonder extra frameworks. CompletableFuture.supplyAsync()
voert taken uit op een thread pool; de return waarde vervangt parentPort.postMessage.
// Orchestrator.java
public class Orchestrator {
private static final int THREAD_COUNT = 5;
private static final ExecutorService pool = Executors.newFixedThreadPool(THREAD_COUNT + 1);
public static void main(String[] args) throws Exception {
Files.createDirectories(Path.of("output"));
// Stap 1: start hoofd thread (thread-0) en wacht synchroon op resultaat
// .get() blokkeert — equivalent van 'await startWorker(0)'
WorkerResult main = CompletableFuture
.supplyAsync(() -> runWorker(0, null), pool)
.get(); // ← wacht tot thread-0 klaar is
System.out.println("Hoofd thread klaar. Random string: " + main.randomString());
// Stap 2: start overige threads parallel met mainFilePath
List<CompletableFuture<WorkerResult>> futures = IntStream
.rangeClosed(1, THREAD_COUNT)
.mapToObj(i -> CompletableFuture.supplyAsync(() -> runWorker(i, main.filepath()), pool))
.toList();
// Wacht tot alle workers klaar zijn — equivalent van Promise.allSettled()
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
futures.forEach(f -> {
try { System.out.println("Thread " + f.get().threadNumber() + ": geslaagd"); }
catch (Exception e) { System.err.println("Fout: " + e.getMessage()); }
});
pool.shutdown();
}
private static WorkerResult runWorker(int threadNumber, String mainFilePath) {
try {
String timestamp = Instant.now().toString().replace(":", "-").replace(".", "-");
String filename = "thread-" + threadNumber + "_" + timestamp + ".txt";
Path filepath = Path.of("output", filename);
String randomString;
if (threadNumber == 0) {
// Hoofd thread: genereer random string
randomString = HexFormat.of().formatHex(
SecureRandom.getInstanceStrong().generateSeed(16));
String content = "Thread nummer : 0 (hoofd thread)\n"
+ "Aangemaakt op : " + Instant.now() + "\n"
+ "Random string : " + randomString;
Files.writeString(filepath, content);
} else {
// Worker thread: lees random string uit hoofd bestand
String mainContent = Files.readString(Path.of(mainFilePath));
randomString = mainContent.lines()
.filter(l -> l.startsWith("Random string"))
.map(l -> l.split(": ")[1].trim())
.findFirst().orElse("onbekend");
String content = "Thread nummer : " + threadNumber + "\n"
+ "Aangemaakt op : " + Instant.now() + "\n"
+ "Random string : " + randomString + " (gelezen uit thread-0)";
Files.writeString(filepath, content);
}
return new WorkerResult(threadNumber, filepath.toString(), randomString);
} catch (Exception e) { throw new RuntimeException(e); }
}
// Return-type van elke worker — vervangt parentPort.postMessage
record WorkerResult(int threadNumber, String filepath, String randomString) {}
}Overeenkomsten met Node.js:
| Node.js | Java CompletableFuture equivalent |
|---|---|
new Worker(file, { workerData }) |
CompletableFuture.supplyAsync(() -> runWorker(i, path), pool) |
parentPort.postMessage({ filepath }) |
return new WorkerResult(...) (return waarde van de lambda) |
await startWorker(0) |
.get() op de CompletableFuture van thread-0 |
Promise.allSettled([...]) |
CompletableFuture.allOf(...).join() |
workerData |
lambda closure of constructor argument |