Lesson 2
Introduction to Thread Lifecycle and Basic Operations
Introduction to Thread Lifecycle and Basic Operations

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.

What You'll Learn

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:

  1. 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.
  2. 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.

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 and t2 are created using the RunnableDemo 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 as sleep_for to pause the current thread for a specified duration. The std::chrono library is used to specify the duration in milliseconds.
  • We call join() on t1 and t2 to wait for their completion, and detach() on t3 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 text
1Running 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}
Why It Matters

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() and detach() is crucial for synchronization, preventing issues like deadlocks and ensuring that threads execute as intended.
Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.