Threads in C++ start with std::thread from the <thread> header: you hand it a callable, it runs on a new operating system thread, and you must either join() or detach() it before the object is destroyed. Skip that step and your program calls std::terminate. Since C++20 there is a better default, std::jthread, which joins automatically and carries a cancellation token.
<thread>, construct std::thread t(func, args...), and call t.join() before t goes out of scope. Protect any shared mutable state with std::mutex plus std::lock_guard, or use std::atomic for simple counters. Compile with g++ -std=c++20 -Wall -Wextra -pthread. In C++20 prefer std::jthread, which joins in its destructor.Every sample below was compiled and run with GCC 13.3 using exactly g++ -std=c++20 -Wall -Wextra -pthread, with no warnings. The -pthread flag matters on Linux: without it you get link errors or, worse, a binary that builds and then aborts at run time.
Your first thread
The minimal program. Note that the output order is not deterministic, which is the whole point.
#include <iostream>
#include <thread>
void greet() {
std::cout << "Hello from a worker thread\n";
}
int main() {
std::thread worker(greet);
std::cout << "Hello from main\n";
worker.join();
return 0;
}g++ -std=c++20 -Wall -Wextra -pthread main.cpp -o main
./main
# Hello from main
# Hello from a worker threadRun it a few times and the two lines may swap. That is correct behavior: nothing orders them. join() blocks the calling thread until the worker finishes, which is the only reason main does not exit first.
join versus detach
Exactly one of these must be called on every joinable thread. The destructor of std::thread checks, and if neither happened it calls std::terminate rather than silently leaking.
| Call | Effect | Risk |
|---|---|---|
t.join() | Blocks until the thread ends | Deadlock if the thread never returns |
t.detach() | Releases ownership, thread runs on alone | Dangling references when main exits first |
| Neither | Destructor calls std::terminate | Immediate crash, no cleanup |
std::jthread | Requests stop and joins in its destructor | None of the above |
detach(), you almost certainly want std::jthread with a stop token instead.Passing arguments
Arguments to the std::thread constructor are copied by default, even when the function signature takes a reference. That is deliberate, and it is why you need std::ref when you really mean to share.
#include <iostream>
#include <thread>
#include <string>
#include <vector>
void report(int id, const std::string& tag, int& counter) {
counter += id;
std::cout << "worker " << id << " tag=" << tag << '\n';
}
int main() {
int counter = 0;
std::vector<std::thread> pool;
for (int i = 1; i <= 3; ++i) {
pool.emplace_back(report, i, "batch", std::ref(counter));
}
for (auto& t : pool) {
t.join();
}
std::cout << "counter = " << counter << '\n';
return 0;
}This program printed counter = 6 on my machine, but it contains a real data race: three threads write counter without synchronization. The answer happens to be right because the increments are fast and rarely overlap. Under a heavier load, or with -fsanitize=thread, the problem shows up immediately. That is the point of the next section.
Mutexes and lock_guard
Any time two threads touch the same object and at least one of them writes, you need synchronization. std::lock_guard takes the lock in its constructor and releases it in its destructor, so an early return or a thrown exception cannot leave the mutex held.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
int shared_total = 0;
std::mutex total_mutex;
void add_many(int n) {
for (int i = 0; i < n; ++i) {
std::lock_guard<std::mutex> guard(total_mutex);
++shared_total;
}
}
int main() {
std::vector<std::thread> pool;
for (int i = 0; i < 4; ++i) {
pool.emplace_back(add_many, 50000);
}
for (auto& t : pool) {
t.join();
}
std::cout << "total = " << shared_total << '\n';
return 0;
}Output: total = 200000, every single run. Remove the lock_guard line and the number drops below 200000 in a way that varies between runs. That is a lost update, and it is the classic demonstration that ++x is not atomic.
std::scoped_lock rather than std::lock_guard when you need more than one mutex at a time. It acquires them all with a deadlock avoidance algorithm, which is the one thing you cannot safely do by hand.Atomics for simple counters
Locking a mutex around a single increment is heavy. std::atomic gives you the same correctness with a hardware instruction instead of a kernel visit.
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> hits{0};
void bump(int n) {
for (int i = 0; i < n; ++i) {
hits.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> pool;
for (int i = 0; i < 4; ++i) {
pool.emplace_back(bump, 50000);
}
for (auto& t : pool) {
t.join();
}
std::cout << "hits = " << hits.load() << '\n';
return 0;
}Prints hits = 200000. std::memory_order_relaxed is safe here because the counter has no ordering relationship with any other data. If the counter were a flag guarding something else, you would need acquire and release ordering instead, and the default std::memory_order_seq_cst is the correct choice until you can prove otherwise.
jthread and stop tokens in C++20
This is the modern default and it fixes two problems at once. The destructor calls request_stop() and then join(), and a callable taking a std::stop_token receives cooperative cancellation for free.
#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token>
void poll(std::stop_token token) {
int ticks = 0;
while (!token.stop_requested()) {
++ticks;
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
std::cout << "stopped after " << ticks << " ticks\n";
}
int main() {
std::jthread worker(poll);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
worker.request_stop();
return 0;
}Output on my run: stopped after 5 ticks. The count varies slightly because sleeping is approximate, which is worth understanding on its own if you are timing work: see our guide on adding a delay in C++. Note that main never calls join. The jthread destructor does it.
Getting a value back with std::async
When you want a result rather than a side effect, std::async plus std::future is less code than a thread and a shared variable.
#include <iostream>
#include <thread>
#include <future>
#include <vector>
#include <numeric>
long long sum_range(const std::vector<int>& data, size_t lo, size_t hi) {
return std::accumulate(data.begin() + static_cast<long>(lo),
data.begin() + static_cast<long>(hi), 0LL);
}
int main() {
std::vector<int> data(1000000);
std::iota(data.begin(), data.end(), 1);
auto left = std::async(std::launch::async, sum_range, std::cref(data), 0, 500000);
auto right = std::async(std::launch::async, sum_range, std::cref(data), 500000, 1000000);
std::cout << "sum = " << left.get() + right.get() << '\n';
return 0;
}Prints sum = 500000500000, which is the correct sum of one through one million. Pass std::launch::async explicitly. Without it the implementation is allowed to run the task lazily on the calling thread when you call get(), which quietly removes all the parallelism you were after.
Choosing between the tools
Four mechanisms cover almost everything you will write, and picking the wrong one is the usual source of complexity.
| You want | Use | Header |
|---|---|---|
| A background task with a result | std::async and std::future | <future> |
| A long lived worker you can cancel | std::jthread with a stop token | <thread> |
| A shared counter or flag | std::atomic | <atomic> |
| A shared container or struct | std::mutex with std::scoped_lock | <mutex> |
Notice that raw std::thread is not on that list. It is the primitive the others are built from, and reaching for it directly is usually a sign that one of the four above would have been shorter and safer.
Troubleshooting
terminate called without an active exception. A std::thread was destroyed while still joinable. Find the scope it lives in and add a join() before the closing brace, or switch the type to std::jthread.
undefined reference to pthread_create. You forgot -pthread. Add it to both the compile and the link step. With CMake, use find_package(Threads REQUIRED) and link Threads::Threads rather than hard coding the flag.
Output lines are interleaved character by character. std::cout is thread safe against data races but does not guarantee that one insertion sequence completes before another starts. Build the whole line into a std::string first and write it with a single insertion, or guard the stream with a mutex.
The result is right in a debug build and wrong with -O2. That is the signature of a data race. The optimizer is allowed to assume no other thread touches a non atomic variable. Rebuild with -fsanitize=thread -g and run the test again; ThreadSanitizer will name both accesses and the stack that reached them.
Two threads deadlock on startup. Look for two mutexes locked in opposite orders. Replace both lock_guard pairs with a single std::scoped_lock(m1, m2), which takes them in a consistent internal order.
Frequently asked questions
How many threads should I create?
For work that is purely computational, start from std::thread::hardware_concurrency(), which reports the number of concurrent threads the machine supports. It returned 2 on the container I tested on. Creating far more than that adds context switching cost without adding throughput.
Is std::cout safe to use from multiple threads?
It will not corrupt memory, because the standard requires synchronized access to the stream. It will interleave your output, since each insertion operator is a separate operation. Assemble the full line first, or wrap the whole statement in a lock.
When should I use jthread instead of thread?
Almost always in new C++20 code. It joins in its destructor, so the terminate on destruction trap disappears, and it supports cooperative cancellation through std::stop_token. Reach for plain std::thread only when you must support an older standard.
What exactly is a data race?
Two threads accessing the same memory location, at least one of them writing, with no synchronization ordering the accesses. It is undefined behavior, not merely a wrong answer, so the compiler may optimize on the assumption it cannot happen. Fix it with a mutex or an atomic.
Do I still need -pthread on modern compilers?
Yes on Linux with GCC and Clang. It sets preprocessor macros and links the threading runtime. Leaving it out can produce a program that links but crashes at run time, which is far harder to diagnose than a link error.
The bottom line
The mechanical rules are short: include <thread>, join or detach every thread, compile with -pthread, and put a lock_guard or an atomic between any two threads that share mutable state. Prefer std::jthread in C++20 and most of the lifetime problems stop being your problem.
The harder discipline is deciding what actually needs to be shared. Threads that pass results back through std::future are far easier to reason about than threads that mutate globals. When you do have to share, run your tests once under ThreadSanitizer before you trust the result. For more C++ fundamentals, see our guides on joining two vectors in C++ and writing a file in C++, and the cppreference thread support library is the reference to keep open.
