Multithreading in C++
Multithreading in C++
Multithreading is an essential feature of modern programming languages, including C++. It allows developers to write programs that can execute multiple tasks concurrently, leading to more efficient and responsive applications. In this guide, we’ll cover the fundamentals of multithreading in C++, including threads, synchronization, and communication between threads.
Table of Contents
- Introduction to Multithreading
- Creating Threads
- Joining and Detaching Threads
- Mutexes and Locks
- Condition Variables
- Futures and Promises
- Thread-Local Storage
- Conclusion
1. Introduction to Multithreading
A thread is the smallest unit of execution in a process. A single-threaded program executes one task at a time, whereas a multithreaded program can execute multiple tasks concurrently. Multithreading can help improve the performance of programs by allowing them to take advantage of multiple CPU cores or by overlapping I/O-bound and CPU-bound tasks.
C++11 introduced support for multithreading in the C++ Standard Library, providing a high-level abstraction for working with threads, synchronization, and communication. The <thread>
header includes the std::thread
class, which represents a thread of execution. The <mutex>
, <condition_variable>
, and <future>
headers provide synchronization and communication primitives.
2. Creating Threads
To create a new thread, construct an std::thread
object and provide a callable object (such as a function, a functor, or a lambda) that the thread will execute. The thread begins executing as soon as it’s created:
|
You can also pass arguments to the thread function:
|
3. Joining and Detaching Threads
When a thread finishes executing, its resources must be cleaned up. You can either join or detach a thread to accomplish this.
Joining a thread means waiting for it to complete before continuing execution in the calling thread. To join a thread, call the join()
method on the std::thread
object:
std::thread t(print_hello); |
Detaching a thread means allowing it to execute independently of the calling thread. The detached thread’s resources will be cleaned up automatically when it finishes executing. To detach a thread, call the detach()
method on the std::thread
object:
std::thread t(print_hello); |
Note that a thread must be either joined or detached before its std::thread
object is destroyed. If a thread is neither joined nor detached, the program will terminate.
4. Mutexes and Locks
When multiple threads access shared data, data races can occur, leading to undefined behavior. To avoid data races, you must synchronize access to shared data using mutexes and locks.
A mutex (short for “mutual exclusion”) is an object that can be locked and unlocked by threads. When a thread locks a mutex, it gains exclusive access to the shared data protected by the mutex. If another thread tries to lock the mutex while it’s already locked, the second thread will block until the mutex is unlocked.
C++ provides the std::mutex
class for this purpose:
|
In the example above, two threads increment a shared counter. The std::unique_lock
class is used to lock and unlock the mutex automatically, ensuring that the shared data is accessed safely. When a lock is acquired, it locks the mutex; when the lock goes out of scope or is explicitly unlocked, the mutex is unlocked.
5. Condition Variables
Condition variables are used for signaling between threads. They allow one or more threads to wait for a specific condition to be met before continuing execution. C++ provides the std::condition_variable
class for this purpose:
|
In the example above, the print_hello
thread waits for the ready
flag to be set before printing its message. The cv.wait()
function blocks until the condition is met, and the cv.notify_one()
function signals the waiting thread to continue.
6. Futures and Promises
Futures and promises are another way to synchronize and communicate between threads. A promise is an object that stores a value or an exception, while a future is an object that can retrieve the value or exception set by the promise.
C++ provides the std::promise
and std::future
classes for this purpose:
|
In the example above, a worker thread computes a result and stores it in a promise. The main thread waits for the result using a future. When the worker thread sets the value in the promise, the main thread retrieves the value using the fut.get()
function.
You can also use std::async
to create a future that automatically executes a function in a separate thread:
|
In this example, std::async
creates a future that automatically manages the worker thread, so you don’t need to explicitly create a thread or join it.
7. Thread-Local Storage
Thread-local storage (TLS) is a mechanism that allows each thread to have its own instance of a variable. TLS is useful when you want to maintain separate state for each thread without using synchronization mechanisms.
To declare a thread-local variable, use the thread_local
keyword:
|
In the example above, each thread increments its own thread-local counter. The tls_counter
variable has separate instances for each thread, so no synchronization is needed.
8. Conclusion
In this comprehensive guide, we’ve covered the fundamentals of multithreading in C++, including creating threads, joining and detaching threads, mutexes and locks, condition variables, futures and promises, and thread-local storage. By understanding and applying these concepts, you can write more efficient and responsive applications that take advantage of modern hardware.
As you continue to explore C++ and its features, keep in mind that multithreading can introduce complexity and potential issues, such as data races, deadlocks, and contention. Be sure to practice safe synchronization techniques and test your programs thoroughly to ensure correct behavior.
With a solid foundation in C++ multithreading, you’re now ready to tackle more advanced topics, such as parallel algorithms, asynchronous I/O, and lock-free data structures. Happy coding!