Welcome back! In the previous lesson, we explored the essentials of concurrency and multithreading. By now, you should have a basic understanding of what concurrency is and how it can help improve the performance and resource utilization of your programs. You've even had a taste of creating a simple thread in C++. Today, we'll delve into the lifecycle of a thread and the basic operations you can perform with threads. Understanding these concepts is crucial for managing threads effectively in your applications.
In this unit, we'll cover the fundamental aspects of the thread lifecycle and operations in C++. Here's what you can expect to learn:
- Creating Threads with Function Objects and Lambdas:
- We'll explore different ways to create threads in C++. Whether it's using function objects and lambdas, knowing these methods will give you flexibility in defining thread tasks.
- Thread Methods:
- We'll look into various operations you can perform on threads, such as
join()
,detach()
, and thread identification. These methods help in managing the execution of threads and ensuring that your program runs smoothly.
- We'll look into various operations you can perform on threads, such as
Now, let's take a closer look at each of these topics and see how they work in practice.
Let's start by defining a runnable class that can be used by threads to execute tasks:
C++1#include <iostream> 2#include <thread> 3#include <string> 4 5class RunnableDemo { 6public: 7 RunnableDemo(const std::string& name) : threadName(name) {} 8 9 // Overloaded operator() to make the object callable like a function 10 void operator()() const { 11 std::cout << "Running " << threadName << std::endl; 12 } 13 14private: 15 std::string threadName; 16};
In this code snippet, we have a class RunnableDemo
that defines a callable object. The class has a constructor that takes a std::string
argument and an overloaded operator()
method that prints a message indicating the thread is running. The operator()
method allows objects of this class to be called like a function, making them suitable for use with threads.
Why not use a regular function instead of a class? Using a class allows us to encapsulate data and behavior related to the thread, making it more modular and reusable. The operator()
method acts as the entry point for the thread's execution.
Now that we have a RunnableDemo
class that defines a callable object, let's create threads using this class and a lambda function:
C++1int main() { 2 std::thread t1(RunnableDemo("Thread-1")); 3 std::thread t2(RunnableDemo("Thread-2")); 4 std::thread t3([]() { 5 std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Sleep for 1 second 6 std::cout << "Lambda thread" << std::endl; 7 }); 8 t1.join(); 9 t2.join(); 10 t3.detach(); 11 // t3.join(); // Error: Cannot join a detached thread 12 13 std::cout << "End" << std::endl; 14 std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 15 16 return 0; 17}
In this code snippet, we have a class RunnableDemo
that defines a callable object. We create three threads t1
, t2
, and t3
using different methods:
t1
andt2
are created using theRunnableDemo
class, which is a function object.t3
is created using a lambda function. Within the lambda, we sleep for 1 second and then print a message.- The
std::this_thread
namespace provides utilities for working with threads, such assleep_for
to pause the current thread for a specified duration. Thestd::chrono
library is used to specify the duration in milliseconds.
- The
- We call
join()
ont1
andt2
to wait for their completion, anddetach()
ont3
to let it run independently. - Finally, we print "End" and wait for 1 second before exiting the program.
The output of the program will vary depending on the execution order of the threads. You might see something like this:
Plain text1Running Thread-1 2Running Thread-2 3End 4Lambda thread
In this example, t1
and t2
are joined, so they run to completion before the program prints "End." The lambda thread t3
is detached, so it runs independently and prints "Lambda thread" before or after "End" depending on the execution order. The main difference is that the joined threads must complete before the next statement is executed, while the detached thread can run independently.
Pay attention that you cannot join a detached thread. Attempting to join a detached thread will result in a runtime error. In order to check if a thread is joinable, you can use the joinable()
method. It will return true
if the thread is joinable and false
otherwise. Here's an example of how to check if a thread is joinable before joining it:
C++1if (t3.joinable()) { 2 t3.join(); 3}
Understanding the lifecycle and basic operations of threads is essential for writing robust and efficient multi-threaded programs. Here's why these concepts are important:
- Flexibility in Thread Creation: Being adept at creating threads using different methods gives you the flexibility to choose the best approach for your specific use case.
- Operations and Synchronization: Mastering thread methods such as
join()
anddetach()
is crucial for synchronization, preventing issues like deadlocks and ensuring that threads execute as intended.