Lesson 55
Multithreading (part 1)
Covers what a thread is, process vs thread, the std::thread library, thread lifecycle, creating and joining threads, and detaching threads.
Lesson content
What is multithreading in C++?
Multithreading in C++ is a programming technique that allows a program to execute multiple threads simultaneously. A thread is the smallest unit of execution within a process. By running multiple threads, a program can perform several tasks at the same time, improving responsiveness and making better use of modern multi-core processors.
Imagine you're preparing dinner. Instead of waiting for rice to cook before chopping vegetables, you can do both at the same time. This saves time because multiple tasks are progressing concurrently. Multithreading works in a similar way—different parts of a program can execute concurrently.
Since C++11, the Standard Library provides built-in support for multithreading through the <thread> header, making it easier to write concurrent applications without relying on platform-specific APIs.
Why does multithreading exist?
Many applications need to perform more than one task simultaneously. Without multithreading, a program executes one instruction after another, which can lead to poor responsiveness and underutilized CPU resources.
- Keeping user interfaces responsive while performing background work
- Downloading files while allowing users to interact with the application
- Processing multiple client requests on a server
- Performing calculations in parallel
- Improving performance on multi-core CPUs
Real-world use cases
- Web servers handling multiple clients
- Video and image processing
- Game development (rendering, physics, AI)
- Web browsers (tabs running independently)
- Database systems
- File compression tools
- Scientific simulations
- Machine learning workloads
Prerequisites
- Variables and data types
- Functions
- Loops and conditional statements
- Classes and objects (basic knowledge)
- Functions with parameters
- Basic Standard Template Library (STL)
- Lambda expressions (helpful but not required)
What is a thread?
A thread is an independent sequence of instructions that can run concurrently with other threads in the same process. A process may contain one thread (single-threaded) or multiple threads (multithreaded).
Each thread has its own program counter, stack, and execution path. Threads within the same process share memory, global variables, heap, and open files. This shared access enables efficient communication but also introduces synchronization challenges, which are covered in a later lesson.
Process vs thread
- Process: independent program, own memory space, more expensive to create, slower communication, can contain multiple threads
- Thread: smallest unit of execution, shares process memory, lightweight, faster communication, exists within a process
Single-threaded vs multithreaded programs
In a single-threaded program, each task waits for the previous one to finish: Task A, then Task B, then Task C. In a multithreaded program, Task A, Task B, and Task C can make progress concurrently, depending on the hardware and operating system.
The <thread> library
To use multithreading in C++, include the <thread> header. The primary class is std::thread.
#include <thread>
std::thread threadName(functionName);The thread starts executing as soon as the std::thread object is created.
Thread lifecycle
A thread typically goes through the following stages: create, running, waiting (optional), finished, and joined or detached.
Understanding how threads end is important because every std::thread must be either joined or detached before it is destroyed.
Example 1: Creating a thread
#include <iostream>
#include <thread>
// Function executed by the new thread
void printMessage()
{
std::cout << "Hello from thread!\n";
}
int main()
{
std::thread worker(printMessage);
worker.join();
std::cout << "Main thread finished.\n";
return 0;
}
// Possible Output:
// Hello from thread!
// Main thread finished.std::thread creates a new thread. The new thread starts running printMessage(). join() blocks the main thread until the worker thread finishes.
Joining threads
A thread must be joined if you want the main thread to wait for it. Think of join() as saying, "Don't continue until this thread has completed." Without calling join() (or detach()), destroying a std::thread object results in program termination.
worker.join();Example 2: Running two threads
#include <iostream>
#include <thread>
void task1()
{
std::cout << "Task 1 running\n";
}
void task2()
{
std::cout << "Task 2 running\n";
}
int main()
{
std::thread first(task1);
std::thread second(task2);
first.join();
second.join();
std::cout << "Both threads completed.\n";
return 0;
}
// Possible Output:
// Task 1 running
// Task 2 running
// Both threads completed.
// Note: The order of the first two lines is not guaranteed. Thread
// scheduling is controlled by the operating system.Detaching threads
Instead of waiting for a thread, you can detach it. A detached thread runs independently.
worker.detach();After detaching, you cannot call join(), the thread continues running in the background, and the main program does not wait for it. Use detached threads only when you are certain they do not depend on objects that may be destroyed before they finish.
Example 3: Detached thread
#include <iostream>
#include <thread>
#include <chrono>
void backgroundTask()
{
std::this_thread::sleep_for(
std::chrono::seconds(2)
);
std::cout << "Background task finished.\n";
}
int main()
{
std::thread worker(backgroundTask);
worker.detach();
std::cout << "Main thread continues...\n";
std::this_thread::sleep_for(
std::chrono::seconds(3)
);
return 0;
}
// Possible Output:
// Main thread continues...
// Background task finished.join() vs detach()
- join(): main thread waits, safe for shared resources, thread is synchronized, can only be called once, commonly used
- detach(): main thread continues immediately, requires careful lifetime management, thread runs independently, can only be called once, used only for specific scenarios
Important notes
- A std::thread object represents an active thread of execution
- Every thread must be either joined or detached before its std::thread object is destroyed
- Calling join() twice on the same thread is an error
- Calling detach() after join() is also an error
- Thread execution order is generally non-deterministic