πŸ•žThread in C++ 2023

Threads in C++: Multi-Threading for Concurrent Execution

Introduction: C++ provides a powerful multi-threading library that allows you to create and manage threads for concurrent execution. Threads enable you to perform multiple tasks simultaneously, improving the performance of your applications, especially in scenarios where certain tasks can be parallelized. In this article, we'll explore the basics of working with threads in C++.

Creating Threads:

To create a thread in C++, you need to include the <thread> header. The std::thread class is used to represent a thread. We'll show how to create threads using function pointers and lambda functions.

#include <iostream>
#include <thread>

// Function to be executed by the thread
void threadFunction(int id) {
    std::cout << "Thread " << id << " is running." << std::endl;
}

int main() {
    // Creating and launching a thread using a function pointer
    std::thread t1(threadFunction, 1);

    // Creating and launching a thread using a lambda function
    std::thread t2([]() {
        std::cout << "Thread 2 is running." << std::endl;
    });

    // Wait for the threads to finish their execution
    t1.join();
    t2.join();

    return 0;
}

Operator = in Thread

The operator= (equal) method can be used on std::thread objects to assign one thread to another.

For example:

void func1() { /* ... */ }
void func2() { /* ... */ }

int main() {
   std::thread t1(func1);
  
   std::thread t2;
  
   // Assign t1 to t2
   t2 = t1;  
}

Here we:

  • Create a started thread t1 executing func1

  • Create an empty thread t2

  • Assign t1 to t2 using operator=

After the assignment, t2 will have the same:

  • Native thread handle

  • Thread attributes

  • Running function (potentially)

As t1.

Like std::swap(), actually assigning the running function is implementation dependent. But at minimum, t2 will refer to the same native thread handle as t1.

So operator= on threads allows you to:

  • Copy a started thread

  • Reuse a thread handle

It's useful when you want to "move" a thread to another object.

The assignment operator returns a reference to the assigned thread (the left operand).

Hope this explanation of operator= on std::thread helps!

Passing Arguments to Threads:

You can pass arguments to threads using function parameters or lambda capture clauses. Remember to use std::ref() when passing non-copyable objects.

#include <iostream>
#include <thread>

void threadFunction(int a, int& b) {
    b = a * 2;
}

int main() {
    int num1 = 5;
    int num2 = 0;

    std::thread t(threadFunction, num1, std::ref(num2));
    t.join();

    std::cout << "Result: " << num2 << std::endl;
    return 0;
}

Detaching Threads:

Threads can be detached from the main thread using the detach() method. Detached threads continue to execute independently without blocking the main thread.

#include <iostream>
#include <thread>

void threadFunction() {
    for (int i = 0; i < 5; ++i) {
        std::cout << "Detached thread: " << i << std::endl;
    }
}

int main() {
    std::thread t(threadFunction);
    t.detach();

    // The main thread may terminate before the detached thread finishes
    // (resulting in incomplete output).
    return 0;
}

Joinable Thread :

You can check if a std::thread is joinable or not in C++ using the joinable() method.

A thread is joinable if it has been started (via the start() method or by passing a function to the std::thread constructor) and has not yet been joined.

For example:

#include <thread>
#include <iostream>

void func(){ /* ... */ }

int main() {
  
   std::thread t1(func); // Started thread
   
  if(t1.joinable()) {
    std::cout << "Thread is joinable" ;  
  }  
   
  t1.join();        // Join the thread
  
  if(t1.joinable()) {    
    std::cout << "Thread is not joinable";  
  }
}

In this example:

  • We create a started thread t1 by passing a function

  • We check if it's joinable using joinable()

  • We join the thread

  • We check joinable() again and see that it's no longer joinable

The joinable() method allows you to check the state of the thread and know if you can still join it.

Once a thread has been joined or detached, it is no longer joinable.

Hope this explanation of checking joinability in C++ threads helps!

Joining Threads:

The join() method allows the main thread to wait for a thread to finish its execution before proceeding further.

#include <iostream>
#include <thread>

void threadFunction() {
    std::cout << "Thread is running." << std::endl;
}

int main() {
    std::thread t(threadFunction);
    t.join();

    std::cout << "Main thread continues." << std::endl;
    return 0;
}

Thread Synchronization:

When multiple threads access shared resources simultaneously, race conditions can occur. C++ provides synchronization mechanisms like std::mutex to protect critical sections of code.

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

std::mutex mtx;

void threadFunction() {
    mtx.lock();
    std::cout << "Thread is running." << std::endl;
    mtx.unlock();
}

int main() {
    std::thread t(threadFunction);
    t.join();

    std::cout << "Main thread continues." << std::endl;
    return 0;
}

Thread ID

When working with multi-threaded applications in C++, it is often necessary to identify and differentiate threads. The C++ Standard Library provides a convenient method to obtain the unique ID of a thread using std::this_thread::get_id().

  1. Introduction to std::this_thread::get_id(): The std::this_thread::get_id() function, introduced in C++11, is part of the <thread> header and is used to retrieve the ID of the calling thread. The ID is represented by the data type std::thread::id, which is a unique identifier for each thread.

  2. Understanding Thread ID: The thread ID is essential when you need to differentiate between threads, especially when multiple threads are executing concurrently. Thread IDs are used to track thread execution, perform thread-specific actions, or debug and troubleshoot multi-threaded code.

  3. Usage of std::this_thread::get_id(): The std::this_thread::get_id() function is simple to use. By invoking it in a thread, you obtain a unique ID for that specific thread. You can store and compare these IDs to differentiate between threads.

#include <iostream>
#include <thread>

void threadFunction() {
    std::cout << "Thread ID: " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

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

    return 0;
}

Managing Threads with IDs:

Thread IDs are useful for managing threads in various scenarios. For example, you can use thread IDs to store threads in containers, monitor their progress, or implement thread-specific behaviors.

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

void threadFunction() {
    // Perform some task
}

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

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

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

    return 0;
}

Thread ID Comparison:

Thread IDs can be compared to determine if two threads are the same or different. This can be useful in scenarios where certain actions need to be taken based on a thread's identity.

#include <iostream>
#include <thread>

void threadFunction() {
    std::thread::id id = std::this_thread::get_id();
    if (id == std::this_thread::get_id()) {
        std::cout << "Same thread." << std::endl;
    } else {
        std::cout << "Different thread." << std::endl;
    }
}

int main() {
    std::thread t(threadFunction);
    t.join();

    return 0;
}

we explored the std::this_thread::get_id() function, which provides a simple yet powerful mechanism to obtain the unique ID of a thread. Understanding thread IDs is crucial when working with multi-threaded applications, as it enables you to differentiate threads, manage their execution, and implement thread-specific behaviors. By using std::this_thread::get_id() effectively, you can create more efficient and robust multi-threaded C++ applications.

hardware_concurrency

The std::thread::hardware_concurrency() method is a useful tool provided by the C++ Standard Library to determine the number of hardware-supported concurrent threads. It allows you to optimize thread creation in multi-threaded applications, ensuring you use an appropriate number of threads based on the available hardware capabilities. In this example code and explanation, we will demonstrate how to use std::thread::hardware_concurrency() effectively.

#include <iostream>
#include <thread>

void processTask(int taskNumber) {
    std::cout << "Processing Task " << taskNumber << " on Thread " << std::this_thread::get_id() << std::endl;
}

int main() {
    // Determine the number of hardware-supported threads
    unsigned int numThreads = std::thread::hardware_concurrency();

    std::cout << "Number of hardware-supported threads: " << numThreads << std::endl;

    // Create a vector of threads
    std::vector<std::thread> threads;

    // Start processing tasks using multiple threads
    for (unsigned int i = 0; i < numThreads; ++i) {
        threads.push_back(std::thread(processTask, i));
    }

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

    return 0;
}

The program starts by determining the number of hardware-supported threads using std::thread::hardware_concurrency(). This function returns an unsigned integer representing the number of concurrent threads that can be efficiently executed by the hardware. The actual number of threads supported may vary depending on the system's hardware capabilities.

Native Handler

The std::native_handler thread method is a low-level mechanism for creating threads in C++. It gives you more direct control over the underlying system APIs for creating threads.

Some key points about std::native_handle:

  • It returns a system-specific thread handle. This allows you to directly call OS-specific APIs for manipulating the thread.

  • You have to manually call the appropriate APIs to:

    • Set thread attributes like stack size, scheduling policy, etc.

    • Start the thread by passing the handle to the OS API.

    • Join the thread.

    • Terminate the thread.

  • This gives you more flexibility but requires you to write platform-specific code.

  • The std::thread abstraction handles all of this for you across platforms. std::native_handle is a lower-level alternative.

#include <thread>

void func() {
// ...
}

int main() {
std::thread::native_handle_type handle;

// Set thread attributes
// ...

handle = std::thread(func).native_handle();

// Start the thread passing 'handle'
// ...

// Join the thread using 'handle'
// ...
}

So in summary, std::native_handle gives you more control over threads at the cost of portability and simplicity. It's useful when you need that extra level of control over threads.

Hope this helps! Let me know if you have any other questions.

Swap Thread

The std::swap() method can be used to swap two std::thread objects. This effectively swaps the underlying native thread handles, allowing you to change which thread function a thread handle refers to.

For example:

#include <thread>

void func1(){ /* ... */ }
void func2(){ /* ... */ }

int main(){
  
   std::thread t1(func1);
   std::thread t2(func2);
   
   // Swap the thread functions 
   std::swap(t1, t2);  
   
   // Now t1 refers to func2 and t2 refers to func1!
   t1.join(); 
   t2.join();
}

This can be useful in some multithreading designs where you want to reuse thread handles but change the functions they execute.

By swapping the thread objects, all the associated thread attributes like scheduling policy, stack size, etc will also be swapped between the threads.

However, note that actually swapping the running thread functions itself is implementation dependent and may or may not actually happen - depending on the OS and threading library.

So in summary, std::swap() on threads allows you to swap:

  • The underlying native thread handles

  • All the thread attributes

  • Potentially the actual running thread functions (implementation dependent)

It's a useful technique when you want to reuse thread objects but change their functions.

Conclusion:

C++ threads enable you to harness the power of multi-core processors and execute tasks concurrently, leading to more efficient and responsive applications. However, working with threads requires careful consideration of synchronization to avoid race conditions. Understanding the fundamentals of threads and synchronization will empower you to write efficient, concurrent programs in C++.

Hope this explanation helps! Let me know if you have any other questions.

Last updated