Home C++ C++ multithreading tutorial with code examples

C++ multithreading tutorial with code examples

In C++, multithreading enables the concurrent execution of multiple tasks or parts of a program, allowing for efficient utilization of system resources, particularly on multi-core systems.

C++ provides a threading library in <thread> which makes it easy to create and manage threads.

Along with <mutex> and <condition_variable>, you can coordinate between threads and handle data sharing safely.

Basic Multithreading Concepts in C++

  1. Thread Creation: Creating multiple threads to execute tasks concurrently.
  2. Thread Joining and Detaching: Managing thread execution by joining or detaching them.
  3. Mutex: Prevents data races by allowing only one thread to access shared data at a time.
  4. Condition Variables: Synchronize threads by blocking or notifying them.
  5. Atomic Operations: Perform thread-safe operations without requiring a mutex.

1. Basic Thread Creation and Execution

In this example, we create a simple thread to execute a task concurrently.

#include <iostream>
#include <thread>
using namespace std;

void printMessage() {
    cout << "Hello from thread!" << endl;
}

int main() {
    thread t(printMessage); // Create a thread to run printMessage
    t.join(); // Wait for the thread to finish
    cout << "Main thread ends." << endl;

    return 0;
}

Explanation:

  • thread t(printMessage); creates a thread t that executes printMessage.
  • t.join(); ensures the main thread waits for t to complete before continuing.

Output:

Hello from thread!
Main thread ends.

2. Passing Arguments to Threads

You can pass arguments to thread functions using the std::thread constructor.

#include <iostream>
#include <thread>
using namespace std;

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

int main() {
    thread t(addNumbers, 5, 3); // Pass 5 and 3 as arguments to addNumbers
    t.join();

    return 0;
}

Explanation:

  • thread t(addNumbers, 5, 3); creates a thread that calls addNumbers(5, 3).

Output:

Sum: 8

3. Lambda Functions in Threads

Lambda expressions provide an inline way to define thread functions.

#include <iostream>
#include <thread>
using namespace std;

int main() {
    int x = 10, y = 5;
    thread t([x, y]() { cout << "Difference: " << x - y << endl; });
    t.join();

    return 0;
}

Explanation:

  • thread t([x, y]() { … }); creates a thread with an inline lambda function.

Output:

Difference: 5

4. Detaching Threads

A detached thread runs independently of the main thread and does not need to be joined.

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void printMessage() {
    this_thread::sleep_for(chrono::seconds(2));
    cout << "Hello from detached thread!" << endl;
}

int main() {
    thread t(printMessage);
    t.detach(); // Detach the thread, allowing it to run independently
    cout << "Main thread ends before detached thread." << endl;

    // Adding a small sleep to ensure detached thread prints before program exit
    this_thread::sleep_for(chrono::seconds(3));

    return 0;
}

Explanation:

  • t.detach(); detaches the thread, allowing it to run independently of the main thread.

Output:

Main thread ends before detached thread.
Hello from detached thread!

5. Using Mutex for Thread Synchronization

Mutexes prevent data races by allowing only one thread to access shared data at a time.

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx;

void printNumber(int n) {
    lock_guard<mutex> lock(mtx); // Locks the mutex until the end of scope
    cout << "Number: " << n << endl;
}

int main() {
    thread t1(printNumber, 1);
    thread t2(printNumber, 2);

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

    return 0;
}

Explanation:

  • lock_guard<mutex> lock(mtx); locks mtx to ensure only one thread accesses cout at a time.

Output (order may vary):

Number: 1
Number: 2

6. Using std::unique_lock and Condition Variables

Condition variables allow threads to wait for a condition to be met before continuing.

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

mutex mtx;
condition_variable cv;
bool ready = false;

void printMessage() {
    unique_lock<mutex> lock(mtx);
    cv.wait(lock, [] { return ready; }); // Wait until ready is true
    cout << "Condition met, thread proceeds." << endl;
}

int main() {
    thread t(printMessage);

    this_thread::sleep_for(chrono::seconds(1)); // Simulate work in main
    {
        lock_guard<mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one(); // Notify the waiting thread

    t.join();

    return 0;
}

Explanation:

  • cv.wait(lock, [] { return ready; }); makes the thread wait until ready is true.
  • cv.notify_one(); wakes up one waiting thread.

Output:

Condition met, thread proceeds.

7. Using std::future and std::async for Asynchronous Tasks

std::async creates an asynchronous task that returns a std::future to retrieve the result.

#include <iostream>
#include <future>
using namespace std;

int calculateSquare(int x) {
    return x * x;
}

int main() {
    future<int> result = async(calculateSquare, 5); // Launch async task
    cout << "Square of 5: " << result.get() << endl; // Retrieve the result

    return 0;
}

Explanation:

  • async(calculateSquare, 5); runs calculateSquare(5) asynchronously.
  • result.get(); waits for the result and retrieves it.

Output:

Square of 5: 25

8. Shared Data with std::atomic

std::atomic provides a way to safely share data without using mutexes.

#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic<int> counter(0);

void incrementCounter() {
    for (int i = 0; i < 1000; ++i) {
        counter++;
    }
}

int main() {
    thread t1(incrementCounter);
    thread t2(incrementCounter);

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

    cout << "Final counter value: " << counter << endl;

    return 0;
}

Explanation:

  • counter++ is thread-safe without needing a mutex because counter is atomic.

Output:

Final counter value: 2000

9. Parallel Processing with Multiple Threads

Create multiple threads to perform a task in parallel.

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

void task(int id) {
    cout << "Thread " << id << " is executing." << endl;
}

int main() {
    vector<thread> threads;

    for (int i = 1; i <= 5; ++i) {
        threads.push_back(thread(task, i)); // Create and add threads to the vector
    }

    for (auto& t : threads) {
        t.join(); // Wait for all threads to finish
    }

    return 0;
}

Explanation:

  • Creates 5 threads, each executing task with a unique id.
  • t.join() waits for each thread in the vector to finish.

Output (order may vary):

Thread 1 is executing.
Thread 2 is executing.
Thread 3 is executing.
Thread 4 is executing.
Thread 5 is executing.

10. Thread Pool Example

A thread pool manages multiple threads that can execute tasks in parallel.

#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
using namespace std;

class ThreadPool {
    vector<thread> workers;
    queue<function<void()>> tasks;
    mutex mtx;
    condition_variable cv;
    bool stop;

public:
    ThreadPool(size_t threads) : stop(false) {
        for (size_t i = 0; i < threads; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    function<void()> task;
                    {
                        unique_lock<mutex> lock(this->mtx);
                        this->cv.wait(lock, [this] { return this->stop || !this->tasks.empty(); });
                        if (this->stop && this->tasks.empty()) return;
                        task = move(this->tasks.front());
                        this->tasks.pop();
                    }
                    task();
                }
            });
        }
    }

    template <class F>
    void enqueue(F&& f) {


        {
            unique_lock<mutex> lock(mtx);
            tasks.emplace(f);
        }
        cv.notify_one();
    }

    ~ThreadPool() {
        {
            unique_lock<mutex> lock(mtx);
            stop = true;
        }
        cv.notify_all();
        for (thread &worker : workers) worker.join();
    }
};

void task(int id) {
    cout << "Task " << id << " is executing." << endl;
}

int main() {
    ThreadPool pool(3); // Create a thread pool with 3 threads

    for (int i = 1; i <= 5; ++i) {
        pool.enqueue([i] { task(i); });
    }

    this_thread::sleep_for(chrono::seconds(1)); // Wait for tasks to finish
    return 0;
}

Explanation:

  • The thread pool manages 3 threads, each able to pick up tasks from a queue.
  • pool.enqueue([i] { task(i); }); adds tasks to the queue, which the threads then execute.

Output (order may vary):

Task 1 is executing.
Task 2 is executing.
Task 3 is executing.
Task 4 is executing.
Task 5 is executing.

Summary Table of Multithreading Examples

Example Description
Basic Thread Creation Creates and joins a thread
Passing Arguments Passes arguments to a thread function
Lambda Functions in Threads Uses a lambda expression in a thread
Detached Threads Creates a detached thread
Mutex for Synchronization Uses mutex to avoid data races
Condition Variables Synchronizes threads with condition variables
Asynchronous Tasks with std::async Uses std::async and std::future
Atomic Variables Thread-safe counter with std::atomic
Parallel Processing with Multiple Threads Runs tasks in parallel with multiple threads
Thread Pool Manages a pool of threads for task execution

Key Takeaways

  • Multithreading in C++ is managed with <thread>, allowing concurrent execution of tasks.
  • Mutexes and condition variables help synchronize shared resources and coordinate between threads.
  • Atomic variables perform thread-safe operations without locking mechanisms.
  • Thread pools offer an efficient way to manage a fixed number of threads that process multiple tasks.
  • Use std::async and lambda expressions to simplify asynchronous tasks and reduce function boilerplate.

 

You may also like