In this chapter, we'll explore the concept of threads in C++, which allows programs to execute multiple tasks concurrently. Understanding threads is crucial for developing responsive and efficient applications, especially in scenarios where tasks can be executed simultaneously to improve performance.
A thread is a sequence of instructions that can execute independently of other threads within the same process. In C++, threads are managed using the <thread>
header.
#include
#include
// Function to be executed by the thread
void threadFunction() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// Create a thread and pass the function to execute
std::thread t(threadFunction);
// Join the thread with the main thread
t.join();
return 0;
}
// output //
Hello from thread!
threadFunction()
that will be executed by the thread.main()
, we create a thread t
and pass threadFunction
as an argument to execute.join()
function is called to wait for the thread to finish execution before continuing with the main thread.While the code suggests a specific order of output, the exact timing of thread execution can vary depending on the operating system and hardware. The key takeaway is that both threads will ultimately run, and the output will eventually appear.
One of the biggest challenges in multithreaded programming is ensuring data consistency when multiple threads access the same variables.
C++ provides mechanisms for coordinating access to shared data between threads. These are called synchronization primitives. Here are some common ones:
Mutexes (short for mutual exclusion) are synchronization primitives used to protect shared resources from being accessed simultaneously by multiple threads. In C++, mutexes are provided by the <mutex>
header.
#include
#include
#include
std::mutex mtx;
// Function to be executed by the thread
void threadFunction() {
mtx.lock(); // Acquire the mutex
std::cout << "Hello from thread!" << std::endl;
mtx.unlock(); // Release the mutex
}
int main() {
std::thread t(threadFunction);
mtx.lock(); // Acquire the mutex before printing
std::cout << "Hello from main!" << std::endl;
mtx.unlock(); // Release the mutex after printing
t.join();
return 0;
}
// output //
Hello from main!
Hello from thread!
mtx
to protect access to the shared std::cout
object.threadFunction()
, the thread acquires the mutex before printing, ensuring exclusive access to the std::cout
object.main()
, the main thread acquires the mutex before printing, preventing interleaved output with the thread.Condition variables are synchronization primitives used to coordinate the execution of multiple threads. They allow threads to wait for a certain condition to become true before proceeding.
#include
#include
#include
#include
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Function to be executed by the thread
void threadFunction() {
std::unique_lock lck(mtx);
while (!ready) {
cv.wait(lck);
}
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(threadFunction);
{
std::lock_guard lck(mtx);
ready = true;
}
cv.notify_one();
t.join();
return 0;
}
// output //
Hello from thread!
cv
along with a mutex mtx
to protect access to the shared variable ready
.threadFunction()
, the thread waits on the condition variable cv
until ready
becomes true.main()
, the main thread sets ready
to true and notifies the waiting thread using notify_one()
.Joining a thread allows the main thread to wait for the completion of the child thread before continuing its execution.
#include
#include
// Function to be executed by the thread
void threadFunction() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// Create a thread
std::thread t(threadFunction);
// Join the thread with the main thread
t.join();
std::cout << "Thread execution completed." << std::endl;
return 0;
}
// output //
Hello from thread!
Thread execution completed.
t
, the join()
function is called to wait for the thread to finish execution.Detaching a thread allows it to run independently without being joined by the main thread.
#include
#include
#include
// Function to be executed by the detached thread
void threadFunction() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Detached thread execution completed." << std::endl;
}
int main() {
// Create a detached thread
std::thread t(threadFunction);
t.detach();
std::cout << "Main thread continues execution." << std::endl;
// Ensure main thread does not terminate before detached thread
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
// output //
Main thread continues execution.
Detached thread execution completed.
t
, the detach()
function is called to detach it from the main thread.Exception handling in threads ensures that exceptions thrown within a thread are properly caught and handled.
#include
#include
#include
// Function to be executed by the thread
void threadFunction() {
throw std::runtime_error("Exception from thread!");
}
int main() {
try {
// Create a thread
std::thread t(threadFunction);
t.join();
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
// output //
Exception caught: Exception from thread!
threadFunction()
, an exception is thrown.main()
, the thread is created and joined within a try-catch block to catch any exceptions thrown by the thread.Parallelism: Threads enable parallel execution of tasks, allowing programs to take advantage of multi-core processors and improve performance.
Responsiveness: By executing tasks concurrently, threads enhance the responsiveness of applications, ensuring smooth user interactions and real-time processing.
Resource Utilization: Efficient use of resources is achieved through thread-based concurrency, maximizing CPU and I/O device utilization.
Modularity: Threads facilitate modular programming by allowing tasks to be divided into smaller units of execution, simplifying code organization and maintenance.
Scalability: Thread-based concurrency enables applications to scale dynamically with increasing computational demands, making them suitable for large-scale systems.
Complexity: Multi-threaded programming introduces complexity, including challenges such as race conditions, deadlocks, and synchronization overhead.
Resource Contentions: Threads may compete for shared resources, leading to contention and performance degradation if not managed properly.
Debugging Difficulty: Debugging multi-threaded programs can be challenging due to non-deterministic behavior and timing-dependent bugs.
Overhead: Thread creation and management incur overhead, including memory consumption and context switching, which may impact performance.
Portability: Thread behavior and performance characteristics may vary across different platforms and operating systems, requiring platform-specific optimizations.
Join or Detach Threads: Always join or detach threads to ensure proper synchronization and prevent resource leaks.
Handle Exceptions: Use try-catch blocks to handle exceptions thrown by threads and prevent program termination.
Threads in C++ provide a powerful mechanism for concurrent programming, enabling applications to execute multiple tasks simultaneously. By understanding thread management, synchronization, and error handling, you can develop robust and efficient multi-threaded applications. However, it's essential to follow best practices and handle threading-related complexities with care to avoid issues such as race conditions and deadlocks. With the knowledge gained from this chapter, you're well-equipped to harness the benefits of threads in your C++ projects and build responsive and scalable software.Happy coding !❤️