Loading...

Go Back

Next page
Go Back Course Outline

C++ Full Course


Concurrency & Multithreading in C++

Concurrency & Multithreading in C++

Concurrency and multithreading are essential for modern software development, allowing programs to perform multiple tasks seemingly simultaneously, improving responsiveness and efficiency. C++ provides a rich set of tools in its standard library (primarily introduced in C++11) to support concurrent programming.

Threads (std::thread)

The std::thread class represents a single thread of execution. You can create new threads to execute functions concurrently.

Joining and Detaching

  • join(): The calling thread waits for the thread associated with the std::thread object to complete its execution. You should typically call join() before the std::thread object goes out of scope to ensure proper resource management.
  • detach(): The std::thread object is detached from the calling thread, and the new thread continues to run independently in the background. You lose direct control over its execution and need to ensure that the detached thread manages its resources properly.
#include <iostream>
#include <thread>
#include <chrono>

void workerFunction(int id) {
    std::cout << "Worker thread " << id << " started." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Worker thread " << id << " finished." << std::endl;
}

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

    std::cout << "Main thread continues." << std::endl;

    t1.join(); // Wait for t1 to finish
    std::cout << "Thread 1 joined." << std::endl;

    t2.detach(); // t2 will run independently
    std::cout << "Thread 2 detached." << std::endl;

    // Be cautious with detached threads; ensure they don't access invalid memory
    std::this_thread::sleep_for(std::chrono::seconds(3)); // Allow detached thread to potentially finish
    std::cout << "Main thread finished." << std::endl;

    return 0;
}
Main thread continues.
Worker thread 1 started.
Worker thread 2 started.
Worker thread 1 finished.
Thread 1 joined.
Thread 2 detached.
Worker thread 2 finished.
Main thread finished.


Mutexes & Locks

Mutexes are fundamental synchronization primitives used to protect shared data from race conditions by ensuring that only one thread can access a critical section of code at a time.

std::mutex

A basic mutex that can be locked and unlocked.

std::lock_guard

A RAII (Resource Acquisition Is Initialization) wrapper for a mutex. It automatically locks the mutex upon construction and automatically unlocks it when the std::lock_guard object goes out of scope, even if exceptions are thrown.

std::unique_lock

A more flexible RAII wrapper for a mutex than std::lock_guard. It allows deferred locking, time-constrained locking attempts, recursive locking (for std::recursive_mutex), and transfer of ownership of the lock.

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

std::mutex mtx;
int sharedCounter = 0;

void incrementWithLockGuard() {
    std::lock_guard<std::mutex> lock(mtx); // Lock acquired upon construction, released on destruction
    sharedCounter++;
    std::cout << "Counter (lock_guard) incremented by thread " << std::this_thread::get_id() << " to: " << sharedCounter << std::endl;
}

void incrementWithUniqueLock() {
    std::unique_lock<std::mutex> lock(mtx); // Lock acquired upon construction
    sharedCounter++;
    std::cout << "Counter (unique_lock) incremented by thread " << std::this_thread::get_id() << " to: " << sharedCounter << std::endl;
    lock.unlock(); // Explicitly unlock if needed
    // ... perform operations without the lock ...
    lock.lock(); // Re-acquire the lock
    sharedCounter++;
    std::cout << "Counter (unique_lock) incremented again by thread " << std::this_thread::get_id() << " to: " << sharedCounter << std::endl;
}

int main() {
    std::thread t1(incrementWithLockGuard);
    std::thread t2(incrementWithUniqueLock);

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

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

    return 0;
}
Counter (lock_guard) incremented by thread 140735708293888 to: 1
Counter (unique_lock) incremented by thread 140735700197632 to: 2
Counter (unique_lock) incremented again by thread 140735700197632 to: 3
Final counter value: 3

Asynchronous Operations

C++ provides mechanisms to perform tasks asynchronously, allowing the main thread to continue execution without waiting for the result immediately.

std::async

std::async runs a function (or callable object) in a separate thread and returns a std::future that can be used to retrieve the result.

std::future

A std::future represents the result of an asynchronous operation. It provides a way to wait for the result to become available and to retrieve it (using get()).

std::promise

A std::promise provides a way to set a value (or an exception) that will be made available to a std::future associated with the same promise.

#include <iostream>
#include <future>
#include <thread>
#include <chrono>

int calculateSquare(int num, std::promise<int> resultPromise) {
    std::cout << "Calculating square of " << num << " in another thread." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    int result = num * num;
    resultPromise.set_value(result); // Set the result for the future
    return result;
}

int main() {
    std::promise<int> squarePromise;
    std::future<int> squareFuture = squarePromise.get_future();

    std::thread calculationThread(calculateSquare, 5, std::move(squarePromise));

    std::cout << "Main thread waiting for the square." << std::endl;
    int square = squareFuture.get(); // Wait for and get the result from the promise
    std::cout << "Square is: " << square << std::endl;

    calculationThread.join();

    std::future<int> asyncFuture = std::async(std::launch::async, [](int x){
        std::cout << "Calculating cube of " << x << " asynchronously." << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return x * x * x;
    }, 3);

    std::cout << "Main thread waiting for the cube." << std::endl;
    int cube = asyncFuture.get();
    std::cout << "Cube is: " << cube << std::endl;

    return 0;
}
Main thread waiting for the square.
Calculating square of 5 in another thread.
Square is: 25
Main thread waiting for the cube.
Calculating cube of 3 asynchronously.
Cube is: 27


Atomic Operations (std::atomic<T>)

Atomic operations are operations that are performed indivisibly, meaning they cannot be interrupted by other threads. The std::atomic template provides atomic versions of built-in types, allowing for thread-safe operations without the need for explicit locking in simple cases.

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

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

void incrementAtomicCounter(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        atomicCounter++; // Atomic increment
    }
}

int main() {
    std::vector<std::thread> threads;
    int numThreads = 4;
    int iterations = 10000;

    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(incrementAtomicCounter, iterations);
    }

    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "Final atomic counter value: " << atomicCounter << std::endl;
    std::cout << "Expected value: " << numThreads * iterations << std::endl;

    return 0;
}
Final atomic counter value: 40000
Expected value: 40000

Understanding and utilizing these concurrency and multithreading tools in C++ is crucial for building high-performance and responsive applications.

Go Back

Next page