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::thread object named t is constructed with the hello() 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 then Bs). 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::thread and std::jthread.

  • How join() ensures that threads finish cleanly.

  • How threads run concurrently, producing interleaved output.

  • Why forgetting join() causes runtime termination.

  • How std::jthread in 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.

Subscribe to Modern, High-Performance C++ Tutorials and Insights

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe