Thread programming - GitMasterNikanjam/C_WiKi GitHub Wiki
Thread programming in C++ involves working with threads, which are independent sequences of instructions that can run concurrently within a program. Threads are useful for parallelizing tasks, improving performance, and creating responsive applications. C++ provides support for multithreading through the header and related libraries.
Here are the key concepts and features related to thread programming in C++:
C++11 introduced the header, which provides facilities for creating and managing threads.
You can create a thread by instantiating an object of the std::thread class and passing a function or callable object to be executed in the new thread.
Here's a simple example:
#include <iostream>
#include <thread>
void myFunction() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread myThread(myFunction);
// Do other work in the main thread
myThread.join(); // Wait for the thread to finish
return 0;
}
The join()
method is used to wait for a thread to finish its execution. Alternatively, you can use detach()
to allow the thread to run independently. Be cautious when using detach because it can lead to undefined behavior if the main thread exits before the detached thread finishes.
You can pass arguments to a thread by including them in the thread constructor. Remember to use std::ref()
for reference arguments:
#include <iostream>
#include <thread>
void printMessage(const std::string& message) {
std::cout << message << "\n";
}
int main() {
std::string msg = "Hello from thread!";
std::thread myThread(printMessage, std::ref(msg));
myThread.join();
return 0;
}
When multiple threads access shared data concurrently, you need to ensure proper synchronization to avoid data races. C++ provides synchronization primitives such as std::mutex
, std::lock_guard
, and std::unique_lock
to manage access to shared resources safely.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex myMutex;
void sharedResource() {
std::lock_guard<std::mutex> lock(myMutex);
// Access shared resource safely
std::cout << "Thread " << std::this_thread::get_id() << " accessing shared resource.\n";
}
int main() {
std::thread t1(sharedResource);
std::thread t2(sharedResource);
t1.join();
t2.join();
return 0;
}
The <atomic>
header provides facilities for performing atomic operations, which are operations that are guaranteed to be executed without interruption.
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 10000; ++i) {
counter++;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter value: " << counter << "\n";
return 0;
}
When designing multithreaded applications, consider the principles of thread safety and avoid shared mutable state when possible. Use synchronization mechanisms to protect shared resources and prevent data races.
C++11 introduced the thread_local keyword, allowing you to declare variables that have a separate instance for each thread.
#include <iostream>
#include <thread>
thread_local int threadSpecificValue = 0;
void threadFunction() {
threadSpecificValue++;
std::cout << "Thread-specific value: " << threadSpecificValue << "\n";
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
These are the basic concepts of thread programming in C++. When working with threads, it's crucial to handle synchronization properly and design your program to avoid common concurrency issues. C++ provides a variety of tools and features to make multithreading manageable and efficient.
Passing arguments to threads in C++ can be done in several ways. The appropriate method depends on the nature of the data you want to pass and whether you need the thread to take ownership of the data or just reference it. Here are some common approaches:
You can pass arguments to a thread by value. The data is copied into the thread, and changes in the thread do not affect the original data.
#include <iostream>
#include <thread>
void printNumber(int num) {
std::cout << "Number: " << num << "\n";
}
int main() {
int value = 42;
std::thread myThread(printNumber, value);
myThread.join();
return 0;
}
The disadvantage of passing by value is that it involves copying data, which might be inefficient for large objects.
Passing arguments by reference allows threads to share data. However, you need to be careful about potential data races and ensure proper synchronization.
#include <iostream>
#include <thread>
void modifyNumber(int& num) {
num += 10;
}
int main() {
int value = 42;
std::thread myThread(modifyNumber, std::ref(value));
myThread.join();
std::cout << "Modified value: " << value << "\n";
return 0;
}
In this example, std::ref is used to create a reference wrapper to pass value by reference.
If you want to transfer ownership of a resource to a thread, you can use std::move
. This is particularly useful for passing large objects or resources like std::vector
, std::string
, etc.
#include <iostream>
#include <thread>
#include <vector>
void processVector(std::vector<int>&& data) {
// Work with the moved vector
}
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
std::thread myThread(processVector, std::move(data));
myThread.join();
// 'data' is in a valid but unspecified state after std::move
return 0;
}
After moving data to the thread, the original vector is in a valid but unspecified state.
You can pass multiple arguments to a thread by using std::tuple or std::pair and std::tie to unpack the values.
#include <iostream>
#include <thread>
#include <tuple>
void printValues(int a, float b, const std::string& c) {
std::cout << "Values: " << a << ", " << b << ", " << c << "\n";
}
int main() {
int a = 42;
float b = 3.14;
std::string c = "hello";
std::thread myThread(printValues, a, b, std::cref(c));
myThread.join();
return 0;
}
Here, std::cref
is used to create a reference wrapper for the string c.
Remember to be mindful of the lifetime of the data you pass to threads and use proper synchronization mechanisms when sharing data among multiple threads to avoid data races and other concurrency issues.
A mutex, short for mutual exclusion, is a synchronization primitive used in multithreading to protect shared resources from concurrent access. The term "mutex" is often used as a generalization, referring to various types of locks or synchronization mechanisms. In C++, the header provides facilities for working with mutexes.
Here are the key aspects of using mutexes in C++:
The std::mutex
class is a basic mutex type provided by the C++ Standard Library. It offers a simple way to protect shared data by ensuring that only one thread can lock the mutex at a time.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex myMutex;
void sharedResource() {
std::lock_guard<std::mutex> lock(myMutex);
// Access shared resource safely
std::cout << "Thread " << std::this_thread::get_id() << " accessing shared resource.\n";
}
int main() {
std::thread t1(sharedResource);
std::thread t2(sharedResource);
t1.join();
t2.join();
return 0;
}
In this example, std::lock_guard is a convenient wrapper that locks the mutex in its constructor and unlocks it in its destructor. This ensures that the mutex is always properly released, even if an exception occurs.
std::unique_lock
is another type of lock that provides more flexibility than std::lock_guard
. It allows manual locking and unlocking, and it can be used with condition variables for more advanced synchronization scenarios.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex myMutex;
void sharedResource() {
std::unique_lock<std::mutex> lock(myMutex);
// Access shared resource safely
std::cout << "Thread " << std::this_thread::get_id() << " accessing shared resource.\n";
}
int main() {
std::thread t1(sharedResource);
std::thread t2(sharedResource);
t1.join();
t2.join();
return 0;
}
-
std::lock
: This function allows you to lock multiple mutexes simultaneously, preventing potential deadlocks.
std::mutex mutex1, mutex2;
std::lock(mutex1, mutex2);
// Critical section
-
std::try_lock
: This function tries to lock a set of mutexes but returns immediately if any of them cannot be locked.
if (std::try_lock(mutex1, mutex2) == std::lock_status::success) {
// Critical section
}
In C++, there is no standalone std::unlock
function. Instead, unlocking is typically handled implicitly when the lock object goes out of scope. When you use a lock, like std::unique_lock
or std::lock_guard
, the mutex is automatically released when the lock object is destructed.
Here's a quick example to illustrate this:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex myMutex;
void criticalSection(int threadId) {
std::unique_lock<std::mutex> lock(myMutex); // Lock the mutex
// Access shared resource safely
for (int i = 0; i < 5; ++i) {
std::cout << "Thread " << threadId << ": " << i << "\n";
}
// The mutex is automatically released when 'lock' goes out of scope
} // 'lock' is destructed here, releasing the mutex
int main() {
std::thread t1(criticalSection, 1);
std::thread t2(criticalSection, 2);
t1.join();
t2.join();
return 0;
}
In this example:
The std::unique_lock
object lock is created and locks the mutex when constructed.
When the lock object goes out of scope at the end of the criticalSection function or when the thread function exits, it is destructed, and the mutex is automatically released.
The use of std::unique_lock
provides additional flexibility compared to std::lock_guard
because you can explicitly unlock the mutex before the end of the scope if needed. For example:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex myMutex;
void criticalSection(int threadId) {
std::unique_lock<std::mutex> lock(myMutex); // Lock the mutex
// Access shared resource safely
for (int i = 0; i < 5; ++i) {
std::cout << "Thread " << threadId << ": " << i << "\n";
}
// Explicitly unlock the mutex before the end of the scope
lock.unlock();
// Additional work without holding the mutex
} // 'lock' is destructed here, releasing the mutex
int main() {
std::thread t1(criticalSection, 1);
std::thread t2(criticalSection, 2);
t1.join();
t2.join();
return 0;
}
Remember that it's generally a good practice to let the lock object handle the unlocking implicitly, as it helps ensure that the mutex is released in case of exceptions or other unforeseen issues.
By default, a thread cannot lock the same mutex multiple times (non-recursive locking) because it can lead to deadlocks. However, you can use std::recursive_mutex
for recursive locking, allowing the same thread to lock the mutex multiple times.
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex myMutex;
void sharedResource(int depth) {
std::unique_lock<std::recursive_mutex> lock(myMutex);
// Access shared resource safely
std::cout << "Thread " << std::this_thread::get_id() << " accessing shared resource at depth " << depth << ".\n";
if (depth > 0) {
sharedResource(depth - 1);
}
}
int main() {
std::thread t1(sharedResource, 3);
t1.join();
return 0;
}
In this example, the std::recursive_mutex allows the same thread to lock the mutex multiple times, as long as it releases the locks in the reverse order.
A deadlock is a situation in concurrent programming where two or more threads are unable to proceed because each is waiting for the other to release a resource. This creates a cyclic dependency where none of the threads can continue execution. Deadlocks can severely impact the stability and performance of a concurrent system.
Deadlocks typically occur when the following four conditions hold simultaneously:
1- Mutual Exclusion: At least one resource must be held in a non-shareable mode. This means that only one thread can use the resource at a time.
2- Hold and Wait: A thread must hold at least one resource and must be waiting to acquire additional resources that are currently held by other threads.
3- No Preemption: Resources cannot be forcibly taken away from a thread holding them; they must be released voluntarily.
4- Circular Wait: There must be a circular chain of two or more threads, each of which is waiting for a resource held by the next thread in the chain.
To avoid or solve deadlocks, several strategies can be employed:
1- Avoidance: Prevent the conditions that lead to deadlocks from occurring. This can be achieved by carefully designing the system to avoid having circular dependencies between resources. For example, always acquiring resources in a consistent order can prevent circular wait conditions.
2- Detection and Recovery: Implement mechanisms to detect deadlocks when they occur and take corrective action to recover from them. This might involve periodically checking for deadlocks and aborting or restarting threads to break the deadlock.
3- Prevention: Use techniques such as timeouts or resource allocation strategies to prevent threads from waiting indefinitely. For example, if a thread cannot acquire a resource within a certain time limit, it may release the resources it currently holds and retry later.
4- Resource Ordering: Define a strict ordering of resource acquisition and ensure that all threads acquire resources in the same order to prevent circular wait conditions.
5- Lock Hierarchies: Establish a hierarchy for resource acquisition and ensure that threads always acquire resources in a top-down manner. This prevents lower-level resources from being locked while higher-level resources are still being held.
6- Avoidance of Nested Locks: Avoid acquiring multiple locks in nested fashion where one lock is acquired while holding another lock. If you need to acquire multiple locks, ensure that they are acquired in a consistent order across all threads.
7- Use Deadlock-Free Data Structures and Algorithms: Prefer data structures and algorithms that are inherently deadlock-free or less prone to deadlocks. For example, consider using lock-free or wait-free data structures when possible.
By understanding the conditions that lead to deadlocks and implementing appropriate strategies to prevent or mitigate them, you can ensure the reliability and robustness of concurrent software systems.
let's consider a simple example involving two threads, each holding a lock on a resource the other thread needs:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutexA;
std::mutex mutexB;
void threadFunctionA() {
std::lock_guard<std::mutex> lockA(mutexA);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate some work
std::cout << "Thread A acquired lock on mutexA" << std::endl;
std::lock_guard<std::mutex> lockB(mutexB); // Thread A attempts to acquire lock on mutexB
std::cout << "Thread A acquired lock on mutexB" << std::endl;
}
void threadFunctionB() {
std::lock_guard<std::mutex> lockB(mutexB);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate some work
std::cout << "Thread B acquired lock on mutexB" << std::endl;
std::lock_guard<std::mutex> lockA(mutexA); // Thread B attempts to acquire lock on mutexA
std::cout << "Thread B acquired lock on mutexA" << std::endl;
}
int main() {
std::thread threadA(threadFunctionA);
std::thread threadB(threadFunctionB);
threadA.join();
threadB.join();
return 0;
}
In this example, threadFunctionA and threadFunctionB represent two threads. Each thread acquires a lock on one mutex (mutexA for threadFunctionA and mutexB for threadFunctionB). After acquiring the first lock, each thread attempts to acquire the lock on the other mutex.
Now, if threadFunctionA and threadFunctionB are executed concurrently, they can lead to a deadlock. Here's how:
threadFunctionA acquires mutexA and then attempts to acquire mutexB.
At the same time, threadFunctionB acquires mutexB and then attempts to acquire mutexA.
If threadFunctionA and threadFunctionB run concurrently, they can reach a point where each thread holds one lock and waits indefinitely for the other lock to be released, resulting in a deadlock.
One common solution to prevent deadlocks is to ensure that threads always acquire locks in a consistent order. This prevents the circular wait condition that leads to deadlocks. Let's modify the previous example to ensure that both threads acquire locks in the same order:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutexA;
std::mutex mutexB;
void threadFunctionA() {
std::lock(mutexA, mutexB); // Acquire both locks in a consistent order
std::lock_guard<std::mutex> lockA(mutexA, std::adopt_lock);
std::lock_guard<std::mutex> lockB(mutexB, std::adopt_lock);
std::cout << "Thread A acquired locks on mutexA and mutexB" << std::endl;
}
void threadFunctionB() {
std::lock(mutexA, mutexB); // Acquire both locks in a consistent order
std::lock_guard<std::mutex> lockA(mutexA, std::adopt_lock);
std::lock_guard<std::mutex> lockB(mutexB, std::adopt_lock);
std::cout << "Thread B acquired locks on mutexA and mutexB" << std::endl;
}
int main() {
std::thread threadA(threadFunctionA);
std::thread threadB(threadFunctionB);
threadA.join();
threadB.join();
return 0;
}
In this modified example, both threadFunctionA and threadFunctionB use std::lock to acquire both locks mutexA and mutexB, but they do so in a consistent order. This consistent order ensures that if one thread has acquired mutexA, the other thread will wait until mutexA is released before attempting to acquire it. This prevents the circular wait condition and avoids the possibility of deadlock.
Additionally, std::adopt_lock is used in the std::lock_guard constructors to indicate that the mutexes are already locked and owned by the current thread, preventing them from being locked again.
std::lock_guard
is designed to provide exception safety. If an exception is thrown inside the critical section, the lock will be automatically released when the std::lock_guard
goes out of scope, preventing resource leaks.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex myMutex;
void criticalSection() {
std::lock_guard<std::mutex> lock(myMutex);
// Access shared resource safely
throw std::runtime_error("An error occurred in the critical section.");
}
int main() {
try {
criticalSection();
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
// The lock is automatically released here
}
return 0;
}
Mutexes are essential for managing access to shared resources in a multithreaded environment. Understanding and correctly implementing mutexes are crucial for avoiding data races, deadlocks, and other concurrency-related issues. Additionally, when using mutexes, it's essential to consider the performance impact, as excessive locking and contention can lead to reduced parallelism and scalability.
A condition variable is a synchronization primitive used in multithreading to enable threads to wait for a certain condition to be satisfied before proceeding with their execution. Condition variables are part of the C++ Standard Library and are defined in the <condition_variable>
header.
Condition variables are often used in conjunction with mutexes to implement more sophisticated forms of synchronization. The basic idea is that one or more threads can wait on a condition variable until some other thread signals or notifies them that a particular condition has been met. This allows threads to synchronize their activities based on shared data or events.
Here's a complete example demonstrating the use of a condition variable to synchronize threads:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex myMutex;
std::condition_variable myConditionVariable;
bool conditionSatisfied = false;
void waitForCondition() {
std::unique_lock<std::mutex> lock(myMutex);
myConditionVariable.wait(lock, []{ return conditionSatisfied; });
std::cout << "Condition satisfied. Continue with the work.\n";
}
void notifyCondition() {
{
std::lock_guard<std::mutex> lock(myMutex);
conditionSatisfied = true;
}
myConditionVariable.notify_one();
}
int main() {
std::thread t1(waitForCondition);
std::thread t2(notifyCondition);
t1.join();
t2.join();
return 0;
}
In this example, waitForCondition
waits on the condition variable until conditionSatisfied
becomes true, and notifyCondition
sets conditionSatisfied
to true and notifies the waiting thread.
Condition variables are useful for scenarios where threads need to coordinate based on the state of shared data. They provide a way to avoid busy-waiting and improve the efficiency of thread synchronization. When using condition variables, it's crucial to ensure proper locking using mutexes to avoid race conditions.
In C++, a functor is a class or a struct that behaves like a function. Functors are often used in conjunction with standard library algorithms, such as std::transform, std::sort, std::for_each, etc., where they serve as customizable function objects.
When used with multithreading in C++, functors can be passed to threading APIs like std::thread to execute parallel tasks. Here's a basic example:
#include <iostream>
#include <thread>
// Functor to be executed in a separate thread
class MyFunctor {
public:
void operator()() {
for (int i = 0; i < 5; ++i) {
std::cout << "Thread executing: " << i << std::endl;
}
}
};
int main() {
MyFunctor functor;
// Create a thread and execute the functor
std::thread t(functor);
// Join the thread with the main thread
t.join();
return 0;
}
In this example, MyFunctor is a functor class with operator() overloaded. This operator allows objects of MyFunctor to be called as if they were functions. The std::thread constructor accepts a functor object, and when the thread is started, the functor's operator() is invoked in a separate thread of execution.
Functors are often preferred over plain function pointers or function objects because they can encapsulate state along with behavior, allowing for more flexible and reusable code. Additionally, they can be easily passed to algorithms and threading APIs without needing to modify existing code extensively.
Let's delve deeper into the benefits of functors in C++:
1- Encapsulation of State: Functors can encapsulate state along with behavior. Unlike regular functions, which cannot store state between calls unless using global variables or static variables, functors can maintain internal state as member variables. This makes them useful for maintaining context or configuration across multiple invocations.
2- Flexibility: Functors provide greater flexibility compared to regular functions. They can be customized through constructor parameters or member variables, allowing for different behavior based on the functor's internal state. This flexibility makes them versatile for various use cases.
3- Customizability: Functors can be tailored to specific requirements. You can define different functor classes with different behavior, allowing you to reuse the same interface with varying implementations. This makes your code more modular and easier to maintain.
4- Compatibility with Standard Library Algorithms: Functors are compatible with standard library algorithms such as std::sort, std::transform, std::for_each, etc. These algorithms often accept function objects or functors as arguments, allowing you to apply custom behavior to collections or ranges of data.
5- Thread Safety: Functors can encapsulate thread-safe behavior. By managing internal state within the functor and ensuring proper synchronization mechanisms are in place, you can create thread-safe functors. This is particularly useful in multithreaded environments where shared resources need to be accessed concurrently.
6- Better Performance: In some cases, using a functor can lead to better performance compared to passing a regular function pointer. Functors can be inlined by the compiler, potentially reducing function call overhead and improving execution speed.
Overall, functors offer a powerful mechanism for encapsulating behavior and state, providing flexibility, customizability, and compatibility with standard library utilities and multithreading scenarios. They are a fundamental concept in modern C++ programming, enabling cleaner and more expressive code.
functors can have other member functions and variables just like any other class in C++. This is one of the key advantages of functors - they can encapsulate both behavior (via the operator() or other member functions) and state (via member variables).
std::thread::hardware_concurrency() is a function in the C++ standard library, specifically in the header, introduced in the C++11 standard. It is used to query the number of concurrent threads supported by the underlying hardware. The function returns an unsigned integer representing the number of hardware threads, which generally correspond to the number of CPU cores available on the system.
Here's a brief example of how you might use it:
#include <iostream>
#include <thread>
int main() {
unsigned int num_threads = std::thread::hardware_concurrency();
if (num_threads == 0) {
std::cout << "Couldn't determine the number of hardware threads." << std::endl;
} else {
std::cout << "Number of hardware threads: " << num_threads << std::endl;
}
return 0;
}
Keep in mind that the return value of std::thread::hardware_concurrency() is just a hint, and the actual number of threads you can effectively use in your program may be limited by factors such as operating system constraints, resource availability, and scheduling policies. Additionally, some platforms may not provide accurate or meaningful information, in which case the function may return 0. Therefore, it's usually used as guidance for designing thread-based algorithms rather than as a strict limitation.
you can use more threads than the value returned by std::thread::hardware_concurrency(). This function simply provides a hint regarding the number of hardware threads available on the system, typically corresponding to the number of CPU cores.
Using more threads than the reported hardware concurrency is possible, but it doesn't necessarily mean that your program will execute faster. If you spawn more threads than there are physical cores, the operating system will time-slice the available cores among the threads, resulting in context switching overhead and potentially degraded performance due to contention for CPU resources.
However, there are scenarios where using more threads than hardware concurrency can be beneficial, such as when threads are waiting on I/O operations (disk, network, etc.), or when you have a workload that can be parallelized across multiple threads but doesn't saturate the CPU cores.
It's important to consider factors such as the nature of your workload, the capabilities of your hardware, and any constraints imposed by the operating system when deciding on the optimal number of threads for your application. Experimentation and profiling are often necessary to determine the best threading strategy for your specific use case.
When you use multiple threads in a program, the operating system scheduler is responsible for assigning those threads to CPU cores for execution. Whether each thread runs on a separate CPU core depends on various factors including the operating system's scheduling algorithm, the workload of the system, and the specific instructions in your code.
In general, modern operating systems employ sophisticated scheduling algorithms that aim to efficiently utilize CPU resources by dynamically assigning threads to available CPU cores. This means that even if you have four threads in your program, they may not necessarily be pinned to individual CPU cores. Instead, the operating system may decide to time-slice the threads across the available cores, allowing them to share the CPU resources.
However, you can use techniques such as thread affinity to influence how threads are scheduled onto CPU cores. Thread affinity allows you to bind a thread to a specific CPU core, which can sometimes improve performance by reducing cache misses and improving CPU cache utilization. Keep in mind though, that manually managing thread affinity should be done judiciously, as it can also lead to suboptimal performance in certain scenarios.
In summary, while it's possible that each thread runs on a separate CPU core, it's not guaranteed. The operating system scheduler dynamically manages thread execution across CPU cores based on system workload and scheduling policies.