C++: Multithreading Basics
Last updated: 2026-08-26
In lesson 42, we learned about regular expressions.
Now, we'll enter the advanced territory of C++ — multithreaded programming.
Modern computers are all multi-core, but single-threaded programs can only use one core — what a waste.
Multithreading lets you do multiple things at once, dramatically improving performance.
1. Multithreading Overview
(1) 1.1 What Is a Thread?
A thread is the smallest unit of program execution.
Process vs Thread:
- Process: Resource allocation unit (independent memory space)
- Thread: Execution unit (shares process memory)
Real-life analogy:
- Process = Factory
- Thread = Worker (multiple workers share factory resources)
(2) 1.2 Why Use Multithreading?
| Advantage | Description |
|---|---|
| Improved performance | Multi-core parallel computing |
| Better responsiveness | UI thread doesn't block |
| Simplified design | Assign different tasks to different threads |
2. Creating Threads
(1) 2.1 Basic Usage
C++11 provides the std::thread class in the thread header file.
Example: Creating a thread (Difficulty ⭐)
▶ Example 1: Multithreading programming demo (Difficulty ⭐)
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(hello); // Create thread
t.join(); // Wait for thread to finish
std::cout << "Main thread ends" << std::endl;
return 0;
}
Output:
Main thread: starting
Main thread: waiting for child thread
Child thread: Hello from thread
Main thread: ended
Run result:
Hello from thread!
Main thread ends
(2) 2.2 join vs detach
| Function | Purpose | Description |
|---|---|---|
join() |
Wait for thread to finish | Blocks the current thread |
detach() |
Detach thread | Thread runs independently, can no longer be joined |
Example: Waiting with join (Difficulty ⭐)
#include <iostream>
### ▶ Example 2: Multithreading programming demo (Difficulty ⭐)
#include <thread>
#include <chrono>
void worker(int id) {
for (int i = 0; i < 3; i++) {
std::cout << "Worker " << id << " working..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
std::cout << "All workers done" << std::endl;
return 0;
}
3. Passing Arguments to Threads
(1) 3.1 Passing Parameters
The std::thread constructor can accept any callable object and arguments.
Example: Passing parameters (Difficulty ⭐⭐)
#include <iostream>
#include <thread>
#include <string>
void printMessage(std::string msg, int count) {
for (int i = 0; i < count; i++) {
std::cout << msg << std::endl;
}
}
int main() {
std::thread t(printMessage, "Hello", 3);
t.join();
return 0;
}
(2) 3.2 Passing by Reference
By default, arguments are passed by value. To pass by reference, you must use std::ref.
Example: Passing by reference (Difficulty ⭐⭐)
#include <iostream>
#include <thread>
#include <functional>
void increment(int& x) {
x++;
}
int main() {
int counter = 0;
std::thread t(increment, std::ref(counter));
t.join();
std::cout << "Counter: " << counter << std::endl; // Output: 1
return 0;
}
4. Mutexes
(1) 4.1 Why Do We Need Mutexes?
Problem: Multiple threads accessing shared data simultaneously causes data races.
Example: Data race (Difficulty ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
counter++; // Multiple threads modifying simultaneously, result is uncertain
}
}
int main() {
std::vectorstd::thread threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << counter << std::endl; // Expected 10000, actual may be less
return 0;
}
(2) 4.2 Protecting Shared Data with a Mutex
A mutex (Mutex) ensures that only one thread accesses shared data at a time.
Example: Protecting with mutex (Difficulty ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 1000; i++) {
mtx.lock(); // Lock
counter++;
mtx.unlock(); // Unlock
}
}
int main() {
std::vectorstd::thread threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << counter << std::endl; // Always 10000
return 0;
}
(3) 4.3 lock_guard — RAII Style
Recommended: Use std::lock_guard for automatic locking/unlocking.
void increment() {
for (int i = 0; i < 1000; i++) {
std::lock_guardstd::mutex lock(mtx); // Locks on construction, unlocks on destruction
counter++;
} // Automatically unlocks
}
5. Condition Variables
(1) 5.1 Why Do We Need Condition Variables?
Problem: A thread needs to wait for a condition to become true (e.g., queue is not empty).
Solution: std::condition_variable
(2) 5.2 Example: Producer-Consumer (Difficulty ⭐⭐⭐)
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
std::queueint q;
std::mutex mtx;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 10; i++) {
std::lock_guardstd::mutex lock(mtx);
q.push(i);
std::cout << "Produced: " << i << std::endl;
cv.notify_one(); // Notify consumer
}
}
void consumer() {
for (int i = 0; i < 10; i++) {
std::unique_lockstd::mutex lock(mtx);
cv.wait(lock, { return !q.empty(); }); // Wait until queue is not empty
int value = q.front();
q.pop();
std::cout << "Consumed: " << value << std::endl;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
Output:
Produced:
Consumed:
6. Asynchronous Tasks
(1) 6.1 std::async
std::async is used to launch an asynchronous task and returns a std::future.
Example: Asynchronous computation (Difficulty ⭐⭐)
#include <iostream>
#include <future>
int calculate(int x) {
return x * x;
}
int main() {
std::futureint result = std::async(calculate, 10);
std::cout << "Result: " << result.get() << std::endl; // Output: 100
return 0;
}
❓ FAQ
Q: How many threads should I use? A: Typically equal to the number of CPU cores. Too many threads cause context switching overhead.
Q: What is a deadlock? A: Two threads waiting for each other to release locks, so neither can continue.
How to avoid:
- Lock in a fixed order
- Use
std::lock()to lock multiple mutexes simultaneously - Use
std::scoped_lock(C++17)
std::thread: Flexible, cross-platform - OpenMP: Simple, good for scientific computing▶ Example 3: Creating threads (Difficulty ⭐)
#include <iostream>
#include <thread>
void printNumbers(int start, int end) {
for (int i = start; i <= end; i++) {
std::cout << i << " ";
}
std::cout << std::endl;
}
int main() {
std::thread t1(printNumbers, 1, 5);
std::thread t2(printNumbers, 10, 15);
t1.join();
t2.join();
return 0;
}
Output:
Example output:
1 10 2 11 3 12 4 13 5 14 15
std::thread creates threads, join() waits for them to finish. Output order may vary due to thread scheduling.
| Key Point | Summary |
|---|---|
| std::thread | Create threads |
| join/detach | Wait/detach threads |
| std::mutex | Mutex, protects shared data |
| std::lock_guard | RAII-style locking |
| std::condition_variable | Condition variable, inter-thread communication |
| std::async | Asynchronous tasks |
📖 Summary
- std::thread: Create threads
- join(): Wait for thread to finish
- detach(): Detach thread
- Thread functions: Can pass function pointers, lambdas, or function objects
📝 Exercises
-
Basic (Difficulty ⭐): Create two threads that output "Thread A" and "Thread B" respectively, and observe the randomness of the output order.
-
Intermediate (Difficulty ⭐⭐): Create 4 threads, each computing a cumulative sum over a range of numbers (e.g., 1-2500, 2501-5000...), then combine the results at the end.
-
Challenge (Difficulty ⭐⭐⭐): Use
std::asyncandstd::futureto implement a concurrent download simulator: create 3 asynchronous tasks, each simulating downloading a different-sized file, and wait for all to complete before summarizing.
- std::thread creates threads by passing callable objects
- join waits for thread to finish, detach detaches the thread
- Shared global variables between threads require synchronization
- std::this_thread::sleep_for puts threads to sleep
- Thread count should not exceed hardware support (hardware_concurrency)
Next lesson: Multithreading Synchronization (#44)