Last modified: October 12, 2024

This article is written in: 🇺🇸

Multithreading

Multithreading refers to the capability of a CPU, or a single core within a multi-core processor, to execute multiple threads concurrently. A thread is the smallest unit of processing that can be scheduled by an operating system. In a multithreaded environment, a program, or process, can perform multiple tasks at the same time, as each thread runs in the same shared memory space. This can be useful for tasks that are IO-bound, as threads can be used to keep the CPU busy while waiting for IO operations to complete. However, because threads share the same memory, they must be carefully synchronized to avoid issues like race conditions, where two threads attempt to modify the same data concurrently, leading to unpredictable outcomes.

Thread Pool vs On-Demand Thread

+----------------+        +----------------+        +------------------+
 | Incoming Tasks |        |  Pool Manager  |        |   Thread Pool    |
 |                |        |                |        |                  |
 | +-----------+  |        |                |        |  +-----------+   |
 | | Task 1    |-------------> Assigns Task ---------> | Thread 1   |   |
 | +-----------+  |        |                |        |  +-----------+   |
 | +-----------+  |        |                |        |  +-----------+   |
 | | Task 2    |-------------> Assigns Task ---------> | Thread 2   |   |
 | +-----------+  |        |                |        |  +-----------+   |
 | +-----------+  |        |                |        |  +-----------+   |
 | | Task 3    |-------------> Assigns Task ---------> | Thread 3   |   |
 | +-----------+  |        |                |        |  +-----------+   |
 | +-----------+  |        |                |        |  +-----------+   |
 | | Task 4    |-------------> Assigns Task ---------> | Thread 4   |   |
 | +-----------+  |        |                |        |  +-----------+   |
 | +-----------+  |        |                |        +------------------+
 | | Task 5    |-------------> Waiting      | 
 | +-----------+  |        |                | 
 +----------------+        +----------------+

Worker Threads

A web server process, for example, receives a request and assigns it to a thread from its pool for processing. That thread then follows the main thread's instructions, completes the task, and returns to the pool, allowing the main thread to remain free for other tasks.

Advantages of Threads over Processes

Challenges with Multithreading

Data Race

funA()
funB()

Analogy: Imagine a busy kitchen where multiple chefs work on the same dish using shared tools and ingredients. Without coordination, they might interfere with each other, using the same tool or ingredient simultaneously, resulting in mistakes or confusion. Similarly, a data race happens when threads access shared memory without synchronization, leading to unpredictable outcomes and potential errors.

#include <iostream>
#include <thread>

int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter << std::endl;
    return 0;
}

Deadlock

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1;
std::mutex mutex2;

void thread1() {
    std::lock_guard<std::mutex> lock1(mutex1);
    std::this_thread::sleep_for(std::chrono::milliseconds(50)); // simulate some work
    std::lock_guard<std::mutex> lock2(mutex2);
}

void thread2() {
    std::lock_guard<std::mutex> lock1(mutex2);
    std::this_thread::sleep_for(std::chrono::milliseconds(50)); // simulate some work
    std::lock_guard<std::mutex> lock2(mutex1);
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    return 0;
}

Livelock

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1;
std::mutex mutex2;
bool is_done = false;

void thread1() {
    while (!is_done) {
        if (mutex1.try_lock()) {
            if (mutex2.try_lock()) {
                std::cout << "Thread 1 is done." << std::endl;
                is_done = true;
                mutex2.unlock();
            }
            mutex1.unlock();
        }
    }
}

void thread2() {
    while (!is_done) {
        if (mutex2.try_lock()) {
            if (mutex1.try_lock()) {
                std::cout << "Thread 2 is done." << std::endl;
                is_done = true;
                mutex1.unlock();
            }
            mutex2.unlock();
        }
    }
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    return 0;
}

Mutex

Analogy: Think of a public restroom with only one stall. If multiple people try to use the stall at the same time, chaos will ensue, with people pushing and shoving, and no one getting to use the restroom properly. To prevent this, a lock is installed on the door, which can only be opened by one person at a time. This ensures that only one person can use the stall at any given time, and others have to wait their turn. In the same way, a mutex is a lock that threads can use to access a shared resource in a mutually exclusive way.

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex counter_mutex;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(counter_mutex);
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter << std::endl;
    return 0;
}

Semaphore

Analogy: Imagine a busy street intersection with a traffic light. The traffic light controls the flow of traffic by changing colors at regular intervals, and different lanes of traffic have to take turns moving through the intersection. A semaphore works in a similar way, controlling access to a shared resource by allowing a certain number of threads to access it at a time, and blocking others until there is available capacity. The semaphore acts as a signal to the threads, letting them know when it is safe to access the shared resource.

#include <iostream>
#include <thread>
#include <semaphore>

std::binary_semaphore semaphore(1);

void task(int id) {
    semaphore.acquire();
    std::cout << "Task " << id << " is running" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // simulate some work
    semaphore.release();
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);

    t1.join();
    t2.join();

    return 0;
}

Common Misconceptions

Examples

Examples in C++

In C++, every application starts with a single default main thread, represented by the main() function. This main thread can create additional threads, which are useful for performing multiple tasks simultaneously. Since C++11, the Standard Library provides the std::thread class to create and manage threads. The creation of a new thread involves defining a function that will execute in parallel and passing it to the std::thread constructor, along with any arguments required by that function.

Creating Threads

A new thread in C++ can be created by instantiating the std::thread object. The constructor accepts a callable object (like a function, lambda, or function object) and optional arguments to be passed to the callable object.

#include <iostream>
#include <thread>

void printMessage(const std::string& message) {
    std::cout << message << std::endl;
}

int main() {
    std::thread t1(printMessage, "Hello from thread!");
    t1.join(); // Wait for the thread to finish
    return 0;
}

In this example, printMessage is called in a separate thread, and the main thread waits for t1 to complete using join().

Thread Joining

The join() function is called on a std::thread object to wait for the associated thread to complete execution. This blocks the calling thread until the thread represented by std::thread finishes.

t1.join(); // Main thread waits for t1 to finish

Thread Detaching

Using detach(), a thread is separated from the std::thread object and continues to execute independently. This allows the main thread to proceed without waiting for the detached thread to finish. However, once detached, the thread becomes non-joinable, meaning it cannot be waited on or joined, and it will run independently until completion.

std::thread t2(printMessage, "This is a detached thread");
t2.detach(); // Main thread does not wait for t2

Thread Lifecycle and Resource Management

Each thread has a lifecycle, beginning with creation, execution, and finally termination. Upon termination, the resources held by the thread need to be cleaned up. If a thread object goes out of scope and is still joinable (not yet joined or detached), the program will terminate with std::terminate because it is considered an error to destroy a std::thread object without properly handling the thread.

Passing Arguments to Threads

Arguments can be passed to the thread function through the std::thread constructor. The arguments are copied or moved as necessary. Special care must be taken when passing pointers or references, as these must refer to objects that remain valid throughout the thread's execution.

#include <iostream>
#include <thread>

void printSum(int a, int b) {
    std::cout << "Sum: " << (a + b) << std::endl;
}

int main() {
    int x = 5, y = 10;
    std::thread t(printSum, x, y); // Passing arguments by value
    t.join();
    return 0;
}

In this example, x and y are passed by value to the printSum function.

Using Lambdas with Threads

Lambda expressions provide a convenient way to define thread tasks inline. They can capture local variables by value or reference, allowing for flexible and concise thread management.

#include <iostream>
#include <thread>

int main() {
    int a = 5, b = 10;
    std::thread t([a, b]() {
        std::cout << "Lambda Sum: " << (a + b) << std::endl;
    });
    t.join();
    return 0;
}

In this case, the lambda captures a and b by value and uses them inside the thread.

Mutex for Synchronization

std::mutex is used to protect shared data from being accessed simultaneously by multiple threads. It ensures that only one thread can access the critical section at a time, preventing data races.

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int sharedCounter = 0;

void increment() {
    std::lock_guard<std::mutex> lock(mtx);
    ++sharedCounter;
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Shared Counter: " << sharedCounter << std::endl;
    return 0;
}

In this example, std::lock_guard automatically locks the mutex on creation and unlocks it on destruction, ensuring the increment operation is thread-safe.

Deadlocks and Avoidance

Deadlocks occur when two or more threads are waiting for each other to release resources, resulting in a standstill. To avoid deadlocks, it's crucial to lock multiple resources in a consistent order, use try-lock mechanisms, or employ higher-level concurrency primitives like std::lock or condition variables.

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1;
std::mutex mutex2;

void taskA() {
    std::lock(mutex1, mutex2);
    std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
    std::cout << "Task A acquired both mutexes\n";
}

void taskB() {
    std::lock(mutex1, mutex2);
    std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
    std::cout << "Task B acquired both mutexes\n";
}

int main() {
    std::thread t1(taskA);
    std::thread t2(taskB);
    t1.join();
    t2.join();
    return 0;
}

Here, std::lock locks both mutexes without risking a deadlock by ensuring that both mutexes are acquired in a consistent order.

Condition Variables

std::condition_variable is used for thread synchronization by allowing threads to wait until they are notified to proceed. This is useful for scenarios where a thread must wait for some condition to become true.

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });
    std::cout << "Thread " << id << "\n";
}

void set_ready() {
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    cv.notify_all();
}

int main() {
    std::thread t1(print_id, 1);
    std::thread t2(print_id, 2);
    std::thread t3(set_ready);

    t1.join();
    t2.join();
    t3.join();
    return 0;
}

In this example, cv.wait makes the threads wait until ready becomes true. set_ready changes the condition and notifies all waiting threads.

Semaphores

C++20 introduces std::counting_semaphore and std::binary_semaphore. Semaphores are synchronization primitives that control access to a common resource by multiple threads. They use a counter to allow a fixed number of threads to access a resource concurrently.

#include <iostream>
#include <thread>
#include <semaphore>

std::binary_semaphore semaphore(1);

void task(int id) {
    semaphore.acquire();
    std::cout << "Task " << id << " is running\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // simulate some work
    semaphore.release();
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);
    t1.join();
    t2.join();
    return 0;
}

Here, semaphore.acquire() ensures that only one thread can access the critical section at a time, and semaphore.release() signals that the resource is available again.

Thread Local Storage

C++ provides thread-local storage via the thread_local keyword, allowing data to be local to each thread. This is useful when each thread requires its own instance of a variable, such as when storing non-shared data.

#include <iostream>
#include <thread>

thread_local int localVar = 0;

void increment(int id) {
    ++localVar;
    std::cout << "Thread " << id << ": localVar = " << localVar << std::endl;
}

int main() {
    std::thread t1(increment, 1);
    std::thread t2(increment, 2);
    t1.join();


    t2.join();
    return 0;
}

In this example, each thread has its own instance of localVar, independent of the other threads.

Atomic Operations

For cases where synchronization is needed, but mutexes are too heavy-weight, C++ provides atomic operations via the std::atomic template. This allows for lock-free programming and can be used to implement simple data structures or counters safely in a multithreaded environment.

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> atomicCounter(0);

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++atomicCounter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Atomic Counter: " << atomicCounter << std::endl;
    return 0;
}

In this example, std::atomic<int> ensures that the increment operation is atomic, preventing data races.

Performance Considerations and Best Practices

Here are some example code snippets demonstrating various aspects of multithreading in C++:

# Example Description
1 single_worker_thread Introduce the concept of threads by creating a single worker thread using std::thread.
2 thread_subclass Demonstrate how to create a custom thread class by inheriting std::thread.
3 multiple_worker_threads Show how to create and manage multiple worker threads using std::thread.
4 race_condition Explain race conditions and their impact on multi-threaded applications using C++ examples.
5 mutex Illustrate the use of std::mutex to protect shared resources and avoid race conditions in C++ applications.
6 semaphore Demonstrate the use of std::counting_semaphore to limit the number of concurrent threads accessing a shared resource in C++ applications.
7 producer_consumer Present a classic multi-threading problem (Producer-Consumer) and its solution using C++ synchronization mechanisms like std::mutex and std::condition_variable.
8 fetch_parallel Showcase a practical application of multi-threading for parallel fetching of data from multiple sources using C++ threads.
9 merge_sort Use multi-threading in C++ to parallelize a merge sort algorithm, demonstrating the potential for performance improvements.
10 schedule_every_n_sec Show how to schedule tasks to run periodically at fixed intervals using C++ threads.
11 barrier Demonstrate the use of std::barrier to synchronize multiple threads at a specific point in the execution.
12 thread_local_storage Illustrate the concept of Thread Local Storage (TLS) and how it can be used to store thread-specific data.
13 thread_pool Show how to create and use a thread pool to efficiently manage a fixed number of worker threads for executing multiple tasks.
14 reader_writer_lock Explain the concept of Reader-Writer Locks and their use for efficient access to shared resources with multiple readers and a single writer.

Examples in Python

Python provides built-in support for concurrent execution through the threading module. While the Global Interpreter Lock (GIL) in CPython limits the execution of multiple native threads to one at a time per process, threading is still useful for I/O-bound tasks, where the program spends a lot of time waiting for external events.

Creating Threads

To create a new thread, you can instantiate the Thread class from the threading module. The target function to be executed by the thread is passed to the target parameter, along with any arguments required by the function.

import threading

def print_message(message):
    print(message)

# Create a thread
t1 = threading.Thread(target=print_message, args=("Hello from thread!",))
t1.start()
t1.join()  # Wait for the thread to finish

In this example, the print_message function is executed in a new thread.

Thread Joining

Using the join() method ensures that the main thread waits for the completion of the thread. This is important for coordinating threads, especially when the main program depends on the thread's results.

t1.join()  # Main thread waits for t1 to finish

Thread Detaching

Python threads do not have a direct detach() method like C++. However, once started, a thread runs independently. The main program can continue executing without waiting for the threads, similar to detached threads in C++. However, you should ensure that all threads complete before the program exits to avoid abrupt termination.

Thread Lifecycle and Resource Management

Python threads are automatically managed by the interpreter. However, you should still ensure that threads are properly joined or allowed to finish their tasks to prevent any issues related to resource management or incomplete executions.

Passing Arguments to Threads

Arguments can be passed to the thread function via the args parameter when creating the Thread object. This allows for flexible and dynamic argument passing.

import threading

def add(a, b):
    print(f"Sum: {a + b}")

# Create a thread
t2 = threading.Thread(target=add, args=(5, 10))
t2.start()
t2.join()

Using Lambdas with Threads

Lambda expressions can also be used with threads, providing a concise way to define thread tasks. This is particularly useful for simple operations.

import threading

# Create a thread with a lambda function
t3 = threading.Thread(target=lambda: print("Hello from a lambda thread"))
t3.start()
t3.join()

Mutex for Synchronization

The Lock class from the threading module is used to ensure that only one thread accesses a critical section of code at a time. This prevents race conditions by locking the shared resource.

import threading

counter = 0
counter_lock = threading.Lock()

def increment():
    global counter
    with counter_lock:
        counter += 1

# Create multiple threads
threads = [threading.Thread(target=increment) for _ in range(10)]

for t in threads:
    t.start()

for t in threads:
    t.join()

print(f"Counter: {counter}")

In this example, counter_lock ensures that only one thread modifies the counter variable at a time.

Deadlocks and Avoidance

Deadlocks can occur when multiple threads are waiting for each other to release resources. In Python, you can avoid deadlocks by carefully planning the order of acquiring locks or by using try-lock mechanisms.

import threading

lock1 = threading.Lock()
lock2 = threading.Lock()

def task1():
    with lock1:
        print("Task 1 acquired lock1")
        with lock2:
            print("Task 1 acquired lock2")

def task2():
    with lock2:
        print("Task 2 acquired lock2")
        with lock1:
            print("Task 2 acquired lock1")

# Create threads
t4 = threading.Thread(target=task1)
t5 = threading.Thread(target=task2)

t4.start()
t5.start()
t4.join()
t5.join()

In this example, care must be taken to avoid deadlocks by ensuring that locks are acquired in a consistent order.

Condition Variables

Condition variables allow threads to wait for some condition to be true before proceeding. This is useful in producer-consumer scenarios.

import threading

condition = threading.Condition()
item_available = False

def producer():
    global item_available
    with condition:
        item_available = True
        print("Producer produced an item")
        condition.notify()

def consumer():
    global item_available
    with condition:
        condition.wait_for(lambda: item_available)
        print("Consumer consumed an item")
        item_available = False

# Create threads
t6 = threading.Thread(target=producer)
t7 = threading.Thread(target=consumer)

t6.start()
t7.start()
t6.join()
t7.join()

Here, the consumer waits for the producer to produce an item before proceeding.

Semaphores

Python's threading module includes Semaphore and BoundedSemaphore for managing access to a limited number of resources.

import threading

sem = threading.Semaphore(2)  # Allows up to 2 threads to access the resource

def access_resource(thread_id):
    with sem:
        print(f"Thread {thread_id} is accessing the resource")
        # Simulate some work
        threading.Thread.sleep(1)

# Create multiple threads
threads = [threading.Thread(target=access_resource, args=(i,)) for i in range(5)]

for t in threads:
    t.start()

for t in threads:
    t.join()

In this example, the semaphore limits access to a resource, allowing only two threads to enter the critical section at a time.

Thread Local Storage

Python provides threading.local() to store data that should not be shared between threads.

import threading

local_data = threading.local()

def process():
    local_data.value = 5
    print(f"Thread {threading.current_thread().name} has value {local_data.value}")

# Create threads
t8 = threading.Thread(target=process, name="Thread-A")
t9 = threading.Thread(target=process, name="Thread-B")

t8.start()
t9.start()
t8.join()
t9.join()

In this example, each thread has its own local_data value, independent of the others.

Atomic Operations

While Python lacks built-in atomic operations, certain objects, like integers in the threading module, can be safely incremented without explicit locking due to Python's GIL. However, for non-atomic operations, locks should be used.

import threading

counter = 0
counter_lock = threading.Lock()

def safe_increment():
    global counter
    with counter_lock:
        temp = counter
        temp += 1
        counter = temp

# Create and start threads
threads = [threading.Thread(target=safe_increment) for _ in range(1000)]

for t in threads:
    t.start()

for t in threads:
    t.join()

print(f"Counter: {counter}")

In this example, counter_lock ensures that the increment operation is atomic, preventing race conditions.

Performance Considerations and Best Practices

Here are some example code snippets demonstrating various aspects of multithreading in Python:

# Example Description
1 single_worker_thread Introduce the concept of threads by creating a single worker thread.
2 thread_subclass Demonstrate how to create a custom thread class by subclassing Thread.
3 multiple_worker_threads Show how to create and manage multiple worker threads.
4 race_condition Explain race conditions and their impact on multi-threaded applications.
5 mutex Illustrate the use of mutexes to protect shared resources and avoid race conditions.
6 semaphore Demonstrate the use of semaphores to limit the number of concurrent threads accessing a shared resource.
7 producer_consumer Present a classic multi-threading problem (Producer-Consumer) and its solution using synchronization mechanisms like mutexes and condition variables.
8 fetch_parallel Showcase a practical application of multi-threading for parallel fetching of data from multiple sources.
9 merge_sort Use multi-threading to parallelize a merge sort algorithm, demonstrating the potential for performance improvements.
10 schedule_every_n_sec Show how to schedule tasks to run periodically at fixed intervals using threads.
11 barrier Demonstrate the use of barriers to synchronize multiple threads at a specific point in the execution.
12 thread_local_storage Illustrate the concept of Thread Local Storage (TLS) and how it can be used to store thread-specific data.
13 thread_pool Show how to create and use a thread pool to efficiently manage a fixed number of worker threads for executing multiple tasks.
14 reader_writer_lock Explain the concept of Reader-Writer Locks and their use for efficient access to shared resources with multiple readers and a single writer.

Examples in JavaScript (Node.js)

Node.js traditionally uses a single-threaded event loop to handle asynchronous operations. However, since version 10.5.0, Node.js has included support for worker threads, which allow for multi-threaded execution. This is particularly useful for CPU-intensive tasks, which can block the event loop and degrade performance in a single-threaded environment.

Worker threads in Node.js are provided by the worker_threads module, enabling the creation of additional JavaScript execution contexts. Each worker thread runs in its own isolated V8 instance and does not share any state with other worker threads or the main thread.

Creating Worker Threads

To create a new worker thread, you instantiate the Worker class from the worker_threads module. The worker is initialized with a script or code string to execute.

// main.js
const { Worker } = require('worker_threads');

const worker = new Worker('./worker.js'); // Separate file containing worker code

worker.on('message', (message) => {
  console.log(Received message from worker: ${message});
});

worker.on('error', (error) => {
  console.error(Worker error: ${error});
});

worker.on('exit', (code) => {
  console.log(Worker exited with code ${code});
});

// worker.js
const { parentPort } = require('worker_threads');

parentPort.postMessage('Hello from worker');

In this example, the main script creates a worker thread that runs the code in worker.js. The worker sends a message back to the main thread using parentPort.postMessage().

Handling Communication

Communication between the main thread and worker threads is done using message passing via postMessage and on('message', callback). This method ensures data is passed as serialized objects, preventing shared state issues.

// main.js continued
worker.postMessage({ command: 'start', data: 'example data' });

// worker.js continued
parentPort.on('message', (message) => {
  console.log(Worker received: ${JSON.stringify(message)});
  // Process message
  parentPort.postMessage('Processing complete');
});

Here, the main thread sends a message to the worker, which can then respond or perform actions based on the received data.

Worker Termination

Workers can be terminated from either the main thread or within the worker itself. The terminate() method stops the worker from the main thread, while process.exit() can be used inside the worker.

// Terminating from the main thread
worker.terminate().then((exitCode) => {
  console.log(Worker terminated with code ${exitCode});
});

// Inside worker.js
process.exit(0); // Graceful exit

Passing Data to Workers

Data can be passed to workers via the Worker constructor or by using the postMessage method. Complex data structures are serialized before being sent, and only simple data types (like strings, numbers, and objects) can be efficiently transferred.

// Passing initial data via constructor
const worker = new Worker('./worker.js', {
  workerData: { initialData: 'Hello' }
});

// Accessing workerData in worker.js
const { workerData } = require('worker_threads');
console.log(workerData); // Outputs: { initialData: 'Hello' }

Transferring Ownership of Objects

Certain objects, like ArrayBuffer, can be transferred to a worker thread, which moves the ownership of the object to the worker, preventing the main thread from using it.

const buffer = new SharedArrayBuffer(1024);
const worker = new Worker('./worker.js', { workerData: buffer });

In this example, a SharedArrayBuffer is transferred to the worker, allowing both the main thread and worker to share memory space efficiently.

Error Handling

Proper error handling is crucial in worker threads. The main thread should listen for error events and handle them appropriately to avoid crashes.

worker.on('error', (error) => {
  console.error('Worker error:', error);
});

Performance Considerations and Best Practices
Example: Prime Number Calculation

Below is a complete example of using worker threads to calculate prime numbers, demonstrating data passing, message handling, and worker management.

// main.js
const { Worker } = require('worker_threads');

function runService(workerData) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./primeWorker.js', { workerData });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) {
        reject(new Error(Worker stopped with exit code ${code}));
      }
    });
  });
}

runService(10).then((result) => console.log(result)).catch((err) => console.error(err));

// primeWorker.js
const { parentPort, workerData } = require('worker_threads');

function isPrime(num) {
  for (let i = 2, sqrt = Math.sqrt(num); i <= sqrt; i++) {
    if (num % i === 0) return false;
  }
  return num > 1;
}

const primes = [];
for (let i = 2; i <= workerData; i++) {
  if (isPrime(i)) primes.push(i);
}

parentPort.postMessage(primes);

In this example, the main thread delegates the task of finding prime numbers up to a certain limit to a worker thread. The worker calculates the primes and sends the results back to the main thread using parentPort.postMessage().

Here are some example code snippets demonstrating various aspects of multithreading in JavaScript (Node.js):

# Example Description
1 single_worker_thread Introduce the concept of threads by creating a single worker thread using Web Workers.
2 thread_subclass Demonstrate how to create a custom thread class by extending the Worker class.
3 multiple_worker_threads Show how to create and manage multiple worker threads using Web Workers.
4 race_condition Explain race conditions and their impact on multi-threaded applications using JavaScript examples.
5 mutex Illustrate the use of Atomics and SharedArrayBuffer to protect shared resources and avoid race conditions in JavaScript applications.
6 semaphore Demonstrate the use of semaphores to limit the number of concurrent threads accessing a shared resource in JavaScript applications using Atomics and SharedArrayBuffer.
7 producer_consumer Present a classic multi-threading problem (Producer-Consumer) and its solution using JavaScript synchronization mechanisms like Atomics and SharedArrayBuffer.
8 fetch_parallel Showcase a practical application of multi-threading for parallel fetching of data from multiple sources using Web Workers.
9 merge_sort Use multi-threading in JavaScript to parallelize a merge sort algorithm, demonstrating the potential for performance improvements.
10 schedule_every_n_sec Show how to schedule tasks to run periodically at fixed intervals using JavaScript and Web Workers.
11 barrier Demonstrate the use of barriers to synchronize multiple threads at a specific point in the execution.
12 thread_local_storage Illustrate the concept of Thread Local Storage (TLS) and how it can be used to store thread-specific data.
13 thread_pool Show how to create and use a thread pool to efficiently manage a fixed number of worker threads for executing multiple tasks.
14 reader_writer_lock Explain the concept of Reader-Writer Locks and their use for efficient access to shared resources with multiple readers and a single writer.

Table of Contents

    Multithreading
    1. Thread Pool vs On-Demand Thread
    2. Worker Threads
    3. Advantages of Threads over Processes
    4. Challenges with Multithreading
      1. Data Race
      2. Deadlock
      3. Livelock
      4. Mutex
      5. Semaphore
      6. Common Misconceptions
    5. Examples
      1. Examples in C++
      2. Examples in Python
      3. Examples in JavaScript (Node.js)