Skip to content

About

Sample implementation multi thread

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Multi-Thread Demo (Node.js Worker Threads)

Eenvoudige demonstratie van parallelle verwerking met worker_threads in Node.js, inclusief synchronisatie via een main thread en bestandsgebaseerde communicatie.

Samenvatting

Het programma demonstreert hoe meerdere threads samenwerken zonder gedeeld geheugen te gebruiken. Bij uitvoering gebeurt het volgende:

  1. 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 de output/ map.
  2. Thread-0 meldt zijn resultaat terug aan de orchestrator: het pad naar het aangemaakte bestand en de random string.
  3. 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.
  4. 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.

Uitvoeren

Vereisten: Node.js installeren

Node.js is vereist om dit script te draaien. Controleer eerst of het al aanwezig is:

node --version

Zo 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

Starten

node index.js

Structuur

MultiThread/
├── index.js      # Orchestrator
├── worker.js     # Worker logica (hoofd + gewone threads)
└── output/       # Gegenereerde bestanden (aangemaakt bij uitvoeren)

Mechanisme

1. Main thread (thread-0)

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.

2. Worker threads (thread-1 t/m N)

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

3. Communicatie tussen threads

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

Hoe filepath van thread-0 bij de workers komt

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 orchestrator

Stap 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 worker

De keten is dus:
fs.writeFileSync bepaalt pad → parentPort.postMessage stuurt het → worker.on('message') resolvet de Promise → await geeft het terug.

Wat is parentPort?

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).

4. Volgorde van uitvoering

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

Voorbeeld output

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)

Diagram

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
Loading

Aanpassen

Pas THREAD_COUNT in index.js aan om meer of minder worker threads te starten.


Vergelijkbare implementaties in Java

Hetzelfde patroon (hoofd thread eerst, daarna parallel) is in Java realiseerbaar met twee gangbare frameworks: Vert.x en CompletableFuture / ExecutorService (standaard Java).

Vergelijkingstabel

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

Java Vert.x

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)

Java CompletableFuture / ExecutorService

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

About

Sample implementation multi thread

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages