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
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
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.
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.
The thread that wants to wait.
Name of the thread to wait for.
true: Target has already finished, thread can proceed
false: Target still running, thread must wait
Behavior:
- Target finished: If target is in
finished set, immediately returns true
- 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.
The thread that wants to wait.
Array of thread names to wait for.
true: All targets have finished, thread can proceed
false: At least one target still running, thread must wait
Behavior:
- Sanitizes targets: Removes duplicates and null/undefined values
- All finished: If all targets are in
finished set, returns true
- 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.
The thread that has finished execution.
List of threads that should be awakened (both join waiters and awaitAll waiters whose conditions are now met).
Behavior:
- Adds thread’s name to the
finished set
- Wakes all threads that were
join()ing this specific thread
- Checks all
awaitAll waiters - wakes those whose all targets are now finished
- 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:
- Wakes join waiters - all threads waiting specifically for this thread
- Checks awaitAll waiters - for each, verifies if all its targets are done
- 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
- Fork-join parallelism: Main thread spawns workers, then joins them
- Pipeline stages: Stage N waits for stage N-1 to complete
- Dependency graphs: Thread waits for all dependencies before starting
- 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!');
}