Home Item 8(1/2) - Enhanced Multithreading Support(English)
Post
Cancel

Item 8(1/2) - Enhanced Multithreading Support(English)

Multithreading in C++

Before C++11, C++ did not have standardized support for multithreading, which meant developers had to rely on platform-specific libraries to implement multithreading. This approach was cumbersome and lacked cross-platform compatibility.

For example:

On Windows, you could use WinAPI, which required including the <windows.h> header file. This approach did not require additional libraries but was only suitable for Windows systems.

On POSIX systems (such as Linux and macOS), you could use POSIX Threads (pthread), which required including the <pthread.h> header file. POSIX Threads provided cross-platform capabilities but required installing the relevant libraries.

C++11 introduced new standard libraries to enhance multithreading support, making it easier and safer to write multithreaded programs.

Multithreading Example in C++11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>

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

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

    t1.join(); // Wait for t1 to finish
    t2.join(); // Wait for t2 to finish

    return 0;
}

Result:

Desktop View

Desktop View

Since the execution order of threads cannot be controlled, the output of the above example may vary with each run.

code with lock_guard

When multiple threads access and modify shared data, data races can occur, leading to unpredictable behavior and data corruption.

To prevent this, we use std::mutex and std::lock_guard to ensure that only one thread can access the critical section at a time.

Here is an example demonstrating the use of std::mutex and std::lock_guard:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

    std::mutex mtx;

    void print_message(int id) {
        std::cout << "Thread " << id << " is waiting." << std::endl;

        std::lock_guard<std::mutex> lock(mtx);

        std::cout << "Thread " << id << " is running." << std::endl;

        std::this_thread::sleep_for(std::chrono::milliseconds(1000));

        std::cout << "Thread " << id << " has finished." << std::endl;
    }

    int main() {
        std::vector<std::thread> threads;

        for (int i = 0; i < 5; ++i) {
            threads.push_back(std::thread(print_message, i));
        }

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

        return 0;
    }

Result:

Desktop View

Execution Order with std::lock_guard:

  1. t0 (thread 0) waits.
  2. t0 acquires the lock and executes.
  3. t3 waits.
  4. t2 & t4 wait.
  5. t1 waits.
  6. t0 completes and releases the lock.
  7. t3 acquires the lock and executes.
  8. t3 completes and releases the lock.
  9. t4 acquires the lock and executes.
  10. t4 completes and releases the lock.
  11. t2 acquires the lock and executes.
  12. t2 completes and releases the lock.
  13. t1 acquires the lock and executes.
  14. t1 completes and releases the lock.

code with unique_lock

std::unique_lock provides more flexibility than std::lock_guard, allowing manual lock and unlock operations, which can reduce thread waiting times and improve overall efficiency.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

    std::mutex mtx;

    void print_message(int id) {
        std::cout << "Thread " << id << " is waiting." << std::endl;

        std::unique_lock<std::mutex> lock(mtx);
        std::cout << "Thread " << id << " is running." << std::endl;
        // unlock by user
        lock.unlock();

        // no needed locked tasks
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));

        // lock again
        lock.lock();
        std::cout << "Thread " << id << " has finished." << std::endl;
    }

    int main() {
        std::vector<std::thread> threads;

        for (int i = 0; i < 5; ++i) {
            threads.push_back(std::thread(print_message, i));
        }

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

        return 0;
    }

Result:

Desktop View

Execution Order with std::unique_lock:

  1. t0 waits.
  2. t1 waits.
  3. t1 acquires the lock and executes.
  4. t3 waits.
  5. t4 waits. t2 waits. t0 acquires the lock and executes. t3 acquires the lock and executes. t4 acquires the lock and executes. t2 acquires the lock and executes. t3 completes and releases the lock. t0 completes and releases the lock. t2 completes and releases the lock. t1 completes and releases the lock. t4 completes and releases the lock.

Scope of Locks

The scope of a lock is determined by the lifetime of the std::unique_lock or std::lock_guard variable. When a std::unique_lock or std::lock_guard variable enters scope, it attempts to lock the given std::mutex, and it automatically unlocks the mutex when the variable goes out of scope.

Why Locks Are Automatically Released When Going Out of Scope This automatic unlocking is due to the RAII (Resource Acquisition Is Initialization) principle. RAII is a common C++ programming practice that ensures resources (such as locks, file handles, memory, etc.) are correctly allocated and released during their lifetime.

RAII Principle: With RAII, resource management is tied to object lifetime. When an object is created, it acquires resources, and when it is destroyed, it releases those resources.

Destructor Behavior: std::unique_lock and std::lock_guard call the unlock() method in their destructors to release the lock. Thus, when the std::unique_lock or std::lock_guard object goes out of scope (e.g., when a function returns or the scope ends), the destructor is called, and the lock is automatically released.

When to Use Multithreading Tools

std::thread: Use std::thread to create new threads when you need to perform multiple tasks concurrently. This is especially useful for improving program performance and making full use of multi-core processors.

std::mutex and std::lock_guard/std::unique_lock: Use std::mutex to protect shared resources when multiple threads need to access them, preventing data races.

☝ツ☝

This post is licensed under CC BY 4.0 by the author.

👈 ツ 👍