How to Run Two Threads at Once in C++
Start your first concurrent program using std::thread and the safer C++20 std::jthread. Understand join() for clean execution and avoid common pitfalls.
Most C++ programs do one thing at a time. Your code starts at main(), executes line by line, and exits — all on a single thread.
For example:
#include <iostream>
void hello() {
for (int i = 0; i < 20; ++i) {
std::cout << "a";
}
}
int main() {
hello();
for (int i = 0; i < 20; ++i) {
std::cout << "B";
}
}
Output:
aaaaaaaaaaaaaaaaaaaaBBBBBBBBBBBBBBBBBBBB
This program always prints all as first, then all Bs. That’s because only one thread — the main thread — is doing all the work.
But what if you want to perform two tasks simultaneously — such as running the function hello() while the main thread is printing its own letters?
That’s where multithreading comes in.
Creating a second thread
C++11 introduced the <thread> library, which makes creating threads easy. Let’s modify the program to start a new thread that runs hello().
#include <iostream>
#include <thread>
void hello() {
for (int i = 0; i < 20; ++i) {
std::cout << "a";
}
}
int main() {
std::thread t(hello); // start a new thread
for (int i = 0; i < 20; ++i) {
std::cout << "B";
}
t.join(); // wait for the thread to finish
}
When you run this, the letters will now appear interleaved — in a different order each time:
// first run
BBBBBBBBBBaaaaaaaaaaaaBBBBBBBBBBaaaaaaaa
// second run
BaaaaaBBBBBaaaaaaaaaaaaaaaBBBBBBBBBBBBBB
That’s real concurrency: The main thread prints B while another thread prints a at the same time.
What’s happening here
-
The main thread starts as usual in
main(). -
An
std::threadobject namedtis constructed with thehello()function as its initial function. -
Both execute independently and share the same output stream.
Since both threads write to std::cout concurrently, the order of letters becomes unpredictable.
Note: On some systems, output might still look grouped (all
as thenBs). That’s because of console output buffering, not because threads didn’t run concurrently.
Understanding join()
We call t.join() after the main loop to make sure the main thread waits for t to finish before exiting.
If we moved join() immediately after creating the thread:
std::thread t(hello);
t.join(); // waits here
for (int i = 0; i < 20; ++i) {
std::cout << "B";
}
We’d see the same sequential output again:
aaaaaaaaaaaaaaaaaaaaBBBBBBBBBBBBBBBBBBBB
So the placement of join() determines where the main thread wants the worker thread to be done before executing the next code.
Common pitfall: forgetting to join()
If you forget to call join() (or detach()), the program may crash at runtime:
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread\n";
}
int main() {
std::thread t(hello);
// Forgot t.join();
}
Here's what you might see the output on Windows (it's OS-specific):
terminate called without an active exception
This happens because the thread object t is destroyed while its thread is still running. C++ requires that every thread must be joined or detached before destruction.
Fix
std::thread t(hello);
t.join(); // safe
C++20 Solution: std::jthread
C++20 introduced std::jthread, a safer alternative to std::thread. In fact, it's a RAII wrapper of std::thread.
A jthread automatically joins itself when it goes out of scope, so you don’t have to remember to call join() manually.
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from jthread\n";
}
int main() {
std::jthread t(hello); // automatically joined on destruction
std::cout << "Main continues without explicit join\n";
}
This code exits cleanly — no crashes, no forgotten join() bugs.
So, whenever you just want to fire and forget a thread that should finish before the end of its scope, std::jthread is the safer choice.
But it does not mean you will never call join() on std::jthread
There are cases where you want manual synchronization — for example, to wait for a thread to finish before continuing with other work.
#include <iostream>
#include <thread>
#include <chrono>
void slowTask() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Slow task done\n";
}
int main() {
std::jthread t(slowTask);
std::cout << "Doing something else...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Now waiting for slow task to complete...\n";
t.join(); // explicit join before continuing
std::cout << "All done!\n";
}
Output
Doing something else...
Now waiting for slow task to complete...
Slow task done
All done!
Here, we use std::jthread for safety but still explicitly call join() when we need to coordinate timing — for example, ensuring a result is ready before proceeding.
Key concepts
| Concept | Explanation |
|---|---|
| Thread | A lightweight unit of execution that runs independently. |
| Main thread | The thread that starts when main() begins. |
| New thread | Created using std::thread or std::jthread, runs a separate function. |
join() |
Makes one thread wait for another to finish. |
Takeaway
From this lesson, you learned:
-
How to create and start a new thread with
std::threadandstd::jthread. -
How
join()ensures that threads finish cleanly. -
How threads run concurrently, producing interleaved output.
-
Why forgetting
join()causes runtime termination. -
How
std::jthreadin C++20 automatically joins on destruction. -
That you can still call
join()manually when you need explicit synchronization.
Now you know how to run two threads — the main and a worker — side by side in a simple C++ program.
If your code use C++20, go ahead with std::jthread, otherwise always remember to join() on std::thread.