Multi-Threading

Multi-threading allows you to execute multiple threads concurrently, enabling you to perform tasks simultaneously and take advantage of multi-core processors for increased performance. C++11 and later versions introduced standardized thread support to the language, making it easier to create and manage threads.

Here’s a detailed explanation of multi-threading in Modern C++ along with examples:

Creating Threads

You can create threads using the std::thread class from the <thread> header. It takes a callable object (like a function or a lambda) as an argument that will be executed by the thread.

Example:

#include <iostream>
#include <thread>

// Function to be executed by the thread
void threadFunction() {
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread function executing..." << std::endl;
    }
}

int main() {
    // Create a thread and launch it by calling the function
    std::thread t(threadFunction);

    // Wait for the thread to finish
    t.join();

    std::cout << "Main function executing..." << std::endl;

    return 0;
}

In this example, we create a new thread t that will execute the threadFunction. The join() function is called on the thread object to wait for the thread to finish before the main thread continues.

Passing Arguments to Threads

You can pass arguments to the thread function by simply providing them as additional arguments when creating the thread.

Example:

#include <iostream>
#include <thread>

void printNumber(int number) {
    std::cout << "Number: " << number << std::endl;
}

int main() {
    int num = 42;

    // Create a thread and pass the argument
    std::thread t(printNumber, num);

    t.join();

    return 0;
}

Using Lambda Functions with Threads

Lambda functions provide a concise way to define small functions without the need for separate function declarations. You can use them with threads to define the thread’s behavior inline.

Example:

#include <iostream>
#include <thread>

int main() {
    int x = 10;
    int y = 20;

    // Create a thread with a lambda function
    std::thread t([&]() {
        int result = x + y;
        std::cout << "Result: " << result << std::endl;
    });

    t.join();

    return 0;
}

Thread Synchronization with Mutex

When multiple threads access shared resources simultaneously, it can lead to data races and undefined behavior. To avoid this, you can use std::mutex (mutual exclusion) to protect shared data.

Example:

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

std::mutex mtx;

void printMessage(const std::string& message) {
    std::lock_guard<std::mutex> lock(mtx); // Locks the mutex
    for (int i = 0; i < 5; ++i) {
        std::cout << message << std::endl;
    }
    // The mutex will be automatically unlocked when 'lock' goes out of scope
}

int main() {
    std::thread t1(printMessage, "Hello from thread 1");
    std::thread t2(printMessage, "Hello from thread 2");

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

    return 0;
}

In this example, two threads are created, and they both call the printMessage function. We use a std::lock_guard to lock the mutex mtx while accessing the shared resource (the std::cout statement) and automatically unlock it when the function exits.

Handling Data Sharing with Shared Pointers

When multiple threads need to share data and manage its lifetime, std::shared_ptr can be used to ensure proper memory management.

Example:

#include <iostream>
#include <thread>
#include <memory>

void threadFunction(std::shared_ptr<int> sharedPtr) {
    (*sharedPtr)++;
}

int main() {
    auto sharedPtr = std::make_shared<int>(42);

    std::thread t1(threadFunction, sharedPtr);
    std::thread t2(threadFunction, sharedPtr);

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

    std::cout << "Final value: " << *sharedPtr << std::endl;

    return 0;
}

In this example, we use std::shared_ptr to share an integer value between two threads safely. The std::shared_ptr ensures proper reference counting and memory management.

Async and Future

C++ also provides std::async and std::future for handling asynchronous tasks and obtaining results from those tasks.

Example:

#include <iostream>
#include <future>

int addNumbers(int a, int b) {
    return a + b;
}

int main() {
    // Launch a task asynchronously and get a future object to retrieve the result later
    std::future<int> resultFuture = std::async(addNumbers, 10, 20);

    // Do other work concurrently...

    // Wait for the result and get the value from the future
    int result = resultFuture.get();

    std::cout << "Result: " << result << std::endl;

    return 0;
}

Here, the std::async function launches the addNumbers function asynchronously and returns a std::future object representing the result of the computation. The get() method on the future is used to obtain the result once it’s ready.

These are some of the basics of multi-threading in Modern C++. Keep in mind that multi-threading introduces complexities, like race conditions and deadlocks, so it’s crucial to handle synchronization and shared resources carefully. Always test your multi-threaded code thoroughly to ensure correctness and performance.