Skip to main content

Overview

The JoinManager class handles thread dependencies, allowing threads to wait for other threads to complete execution. It supports both single-thread joining (join) and multi-thread waiting (awaitAll), tracking which threads have finished and managing queues of waiting threads.

Constructor

JoinManager
class
Creates a new join manager with no finished threads or waiters.
const joinManager = new JoinManager();
Initial State:
  • finished: new Set()
  • joinWaiters: new Map()
  • awaitAllWaiters: []

Properties

finished
Set<string>
Set of thread names that have completed execution (called END).
joinWaiters
Map<string, Array<Thread>>
Maps target thread name to list of threads waiting for that specific thread to finish.
awaitAllWaiters
Array<object>
List of entries for threads waiting on multiple targets. Each entry has:
  • thread: The waiting thread
  • targets: Array of thread names it’s waiting for

Methods

join(thread, targetName)

Wait for a specific thread to complete.
thread
Thread
required
The thread that wants to wait.
targetName
string
required
Name of the thread to wait for.
return
boolean
  • true: Target has already finished, thread can proceed
  • false: Target still running, thread must wait
Behavior:
  1. Target finished: If target is in finished set, immediately returns true
  2. Must wait: Adds thread to the target’s waiting queue and returns false
Source: joinManager.js:13
// Example: Thread waiting for another to finish
const canProceed = joinManager.join(thread1, 'WorkerThread');

if (canProceed) {
  console.log('WorkerThread already finished');
} else {
  console.log('Waiting for WorkerThread to finish');
  thread1.state = 'blocked';
}

awaitAll(thread, targets)

Wait for multiple threads to complete.
thread
Thread
required
The thread that wants to wait.
targets
Array<string>
required
Array of thread names to wait for.
return
boolean
  • true: All targets have finished, thread can proceed
  • false: At least one target still running, thread must wait
Behavior:
  1. Sanitizes targets: Removes duplicates and null/undefined values
  2. All finished: If all targets are in finished set, returns true
  3. Must wait: Registers thread in awaitAllWaiters with its target list
Source: joinManager.js:23
// Example: Waiting for multiple threads
const allDone = joinManager.awaitAll(mainThread, ['Worker1', 'Worker2', 'Worker3']);

if (allDone) {
  console.log('All workers finished');
} else {
  console.log('Waiting for workers to complete');
  mainThread.state = 'blocked';
}

markFinished(thread)

Marks a thread as finished and wakes up all threads waiting for it.
thread
Thread
required
The thread that has finished execution.
return
Array<Thread>
List of threads that should be awakened (both join waiters and awaitAll waiters whose conditions are now met).
Behavior:
  1. Adds thread’s name to the finished set
  2. Wakes all threads that were join()ing this specific thread
  3. Checks all awaitAll waiters - wakes those whose all targets are now finished
  4. Returns combined list of awakened threads
Source: joinManager.js:36
// Example: Thread finishing execution
const awakened = joinManager.markFinished(workerThread);

console.log(`${workerThread.name} finished`);
console.log(`Awakened ${awakened.length} waiting threads`);

awakened.forEach(thread => {
  thread.state = 'ready';
  thread.blockedBy = null;
});

Usage Example

import { JoinManager } from './core/joinManager.js';

const joinManager = new JoinManager();

// Scenario: Main thread waits for worker threads
function mainThreadCode(mainThread) {
  console.log('Main: Starting workers');
  
  // Wait for all workers to complete
  const allDone = joinManager.awaitAll(mainThread, ['Worker1', 'Worker2']);
  
  if (!allDone) {
    mainThread.state = 'blocked';
    return; // Must wait
  }
  
  console.log('Main: All workers done, proceeding');
}

// Worker 1 finishes
function worker1Finish() {
  const awakened = joinManager.markFinished(worker1Thread);
  // Main thread might not wake yet (Worker2 still running)
}

// Worker 2 finishes  
function worker2Finish() {
  const awakened = joinManager.markFinished(worker2Thread);
  // Now main thread wakes up (all targets done)
  console.log(`Awakened: ${awakened.map(t => t.name).join(', ')}`);
}

Join Pattern

Simple one-to-one dependency:
// Thread A depends on Thread B
const joined = joinManager.join(threadA, 'ThreadB');

if (!joined) {
  threadA.state = 'blocked';
  // ... ThreadA waits ...
}

// Later: ThreadB finishes
const awakened = joinManager.markFinished(threadB);
// awakened contains threadA
threadA.state = 'ready'; // Can continue now

AwaitAll Pattern

One thread waiting for multiple dependencies:
// Main thread depends on all workers
const allDone = joinManager.awaitAll(mainThread, ['W1', 'W2', 'W3']);

if (!allDone) {
  mainThread.state = 'blocked';
}

// Workers finish one by one
joinManager.markFinished(w1Thread); // Main still blocked
joinManager.markFinished(w2Thread); // Main still blocked  
const awakened = joinManager.markFinished(w3Thread); // Main awakens!

Wake-up Logic

When a thread finishes, the manager:
  1. Wakes join waiters - all threads waiting specifically for this thread
  2. Checks awaitAll waiters - for each, verifies if all its targets are done
  3. Returns both groups - combined list of threads to awaken
// Source: joinManager.js:36-56
markFinished(thread) {
  this.finished.add(thread.name);
  const awakened = [];

  // 1. Wake join waiters
  const joiners = this.joinWaiters.get(thread.name) ?? [];
  this.joinWaiters.delete(thread.name);
  joiners.forEach(waitingThread => {
    awakened.push(waitingThread);
  });

  // 2. Check awaitAll waiters
  const pending = [];
  this.awaitAllWaiters.forEach(entry => {
    const ready = entry.targets.every(target => this.finished.has(target));
    if (ready) awakened.push(entry.thread);
    else pending.push(entry);
  });
  this.awaitAllWaiters = pending;

  return awakened;
}

Multiple Waiters

Multiple threads can wait for the same target:
// Three threads wait for the same worker
joinManager.join(thread1, 'Worker');
joinManager.join(thread2, 'Worker');
joinManager.join(thread3, 'Worker');

// When worker finishes, all three wake up
const awakened = joinManager.markFinished(workerThread);
// awakened = [thread1, thread2, thread3]

Duplicate Handling

The manager handles duplicates gracefully:
// awaitAll removes duplicates
joinManager.awaitAll(thread, ['W1', 'W2', 'W1', 'W1']);
// Internally stored as: ['W1', 'W2']

// join prevents duplicate registrations
joinManager.join(thread, 'Worker');
joinManager.join(thread, 'Worker'); // Won't add duplicate

Timeline Example

const jm = new JoinManager();

// t0: Main waits for workers
jm.awaitAll(main, ['A', 'B', 'C']);
// main.state = 'blocked'
// awaitAllWaiters = [{ thread: main, targets: ['A', 'B', 'C'] }]

// t1: Worker A finishes
jm.markFinished(threadA);
// finished = {'A'}
// main still blocked (B and C not done)

// t2: Worker B finishes
jm.markFinished(threadB);
// finished = {'A', 'B'}
// main still blocked (C not done)

// t3: Worker C finishes
const awakened = jm.markFinished(threadC);
// finished = {'A', 'B', 'C'}
// awakened = [main]
// main.state = 'ready'

Use Cases

  1. Fork-join parallelism: Main thread spawns workers, then joins them
  2. Pipeline stages: Stage N waits for stage N-1 to complete
  3. Dependency graphs: Thread waits for all dependencies before starting
  4. Cleanup: Coordinator thread waits for all workers before cleanup
// Example: Parallel file processing
function processFiles(files) {
  const workers = files.map((file, i) => 
    spawnWorker(`Worker${i}`, () => processFile(file))
  );
  
  // Main thread waits for all workers
  const workerNames = workers.map(w => w.name);
  const allDone = joinManager.awaitAll(mainThread, workerNames);
  
  if (!allDone) {
    return; // Blocked until all workers finish
  }
  
  console.log('All files processed!');
}