Chapter 32: Multithreading

1. Concurrency and Threads

Models of Concurrency

Reasons for Using Threads

Creating Threads in C++

#include <thread>
#include <iostream>
#include <string>

void func()
{
    std::cout << "Function executed in thread\n";
}

void f(int i, const std::string& s)
{
    std::cout << "Function executed with params: " << i << ", " << s << '\n';
}

int main()
{
    std::thread t(func);  // Call function in a new thread
    t.join();  // Main thread waits for thread t to finish
    std::thread t2(f, 3, "hello");  // Start f(3, "hello") in a new thread
    t2.join();  // Main thread waits for thread t2 to finish
    return 0;
}

2. Synchronization and Data Protection

Mutexes

Protect shared resources from concurrent access by multiple threads. Using lock_guard automatically manages locking and unlocking of mutexes.

#include <mutex>
#include <list>

std::mutex mtx;
std::list<int> some_list;

void add_to_list(int new_value)
{
    std::lock_guard<std::mutex> guard(mtx);
    some_list.push_back(new_value);
}

Deadlock

Always lock mutexes in the same order to avoid deadlock.

3. Condition Variables

Condition variables allow synchronization of threads waiting for a specific condition to be met.

#include <condition_variable>
#include <mutex>
#include <queue>

std::queue<int> data_queue;
std::mutex mtx;
std::condition_variable cv;

void data_preparation_thread()
{
    std::unique_lock<std::mutex> lk(mtx);
    data_queue.push(42);
    cv.notify_one();
}

void data_processing_thread()
{
    std::unique_lock<std::mutex> lk(mtx);
    cv.wait(lk, []{ return !data_queue.empty(); });  // Thread waits for condition
    int data = data_queue.front();
    data_queue.pop();
}

4. std::async and std::future

std::async allows functions to be executed asynchronously, returning a std::future object that can hold the function's result.

#include <future>
#include <iostream>

int find_the_answer()
{
    return 42;
}

int main()
{
    std::future<int> future_result = std::async(find_the_answer);
    int result = future_result.get();  // Wait for the result
    std::cout << "Result: " << result << '\n';
    return 0;
}

5. Atomic Types and Operations

Atomic types: std::atomic<int> ensures that operations on variables are atomic (indivisible).

std::atomic_flag: Used for simple synchronization mechanisms like spinlock.

#include <atomic>
#include <iostream>

std::atomic<int> counter;

void increment_counter()
{
    counter.fetch_add(1);  // Safe increment operation
}

int main()
{
    counter = 0;
    increment_counter();
    std::cout << "Counter: " << counter.load() << '\n';
    return 0;
}

6. Synchronization with Time Limits

Define a maximum wait time in synchronization functions.

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

int main()
{
    std::future<int> f = std::async([]{ return 42; });
    if(f.wait_for(std::chrono::milliseconds(100)) == std::future_status::ready) {
        std::cout << "Result: " << f.get() << '\n';
    } else {
        std::cout << "Timeout\n";
    }
    return 0;
}

7. chrono - Time Operations

The <chrono> header allows precise time-related operations.

#include <chrono>
#include <iostream>

void do_something()
{
    for (volatile int i = 0; i < 100000000; ++i);
}

int main()
{
    auto start = std::chrono::high_resolution_clock::now();
    do_something();
    auto stop = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration = stop - start;
    std::cout << "Duration: " << duration.count() << " seconds\n";
    return 0;
}

8. CSP Paradigm and Actors

A model for communication between threads without sharing data — each thread acts independently, and communication occurs through message passing.

9. Memory Models and Operation Ordering

Different memory models and ordering of atomic operations:

Memory Fences: Enforce a specific order of operations.

std::atomic_thread_fence(std::memory_order_release);

10. Advanced Examples

Spinlock Mutex

#include <atomic>

class spinlock_mutex {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while(flag.test_and_set(std::memory_order_acquire));
    }
    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

Example Using std::promise

#include <future>
#include <iostream>

void set_promise_value(std::promise<int>& p)
{
    p.set_value(42);
}

int main()
{
    std::promise<int> promise;
    std::future<int> future = promise.get_future();
    std::thread t(set_promise_value, std::ref(promise));
    t.join();
    std::cout << "Result: " << future.get() << '\n';
    return 0;
}

Example Using std::atomic_flag

#include <atomic>
#include <iostream>

std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;

void spinlock_lock(std::atomic_flag& lock)
{
    while(lock.test_and_set(std::memory_order_acquire));
}

void spinlock_unlock(std::atomic_flag& lock)
{
    lock.clear(std::memory_order_release);
}

int main()
{
    spinlock_lock(lock_flag);
    std::cout << "Critical section\n";
    spinlock_unlock(lock_flag);
    return 0;
}

Example Using std::condition_variable

#include <condition_variable>
#include <iostream>
#include <thread>
#include <queue>

std::condition_variable cv;
std::mutex cv_m;
std::queue<int> data_queue;

void data_preparation_thread()
{
    for (int i = 0; i < 5; ++i)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::lock_guard<std::mutex> lk(cv_m);
        data_queue.push(i);
        cv.notify_one();
    }
}

void data_processing_thread()
{
    while (true)
    {
        std::unique_lock<std::mutex> lk(cv_m);
        cv.wait(lk, []{ return !data_queue.empty(); });
        int data = data_queue.front();
        data_queue.pop();
        lk.unlock();
        std::cout << "Processed data: " << data << '\n';
        if (data == 4) break; // Exit condition
    }
}

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

Conclusion

Multithreading in C++ allows for concurrent execution of tasks, improving performance and responsiveness of applications. Understanding synchronization mechanisms such as mutexes, condition variables, atomic operations, and using tools like std::async and std::future is crucial for writing safe and efficient multithreaded code.