Modern computers have several processor cores, often 4, 8 or more. A normal program uses just one of them: it does one thing at a time, from top to bottom. If a task can be split into independent pieces, such as resizing a hundred photos or adding up different parts of a huge list, the other cores sit idle while one does all the work.
Threads let one program run several pieces of code at the same time. C++11 added threads to the standard library, so the same code runs on Windows, macOS and Linux. Threads can make heavy work much faster and keep programs responsive, for example by doing slow work in the background while the user interface keeps reacting.
Threads also bring a new kind of bug, when two threads change the same data at once. In this lesson you'll start and join threads, protect shared data with mutexes and atomics, and use std::async for results.
Starting and joining a thread
#include <thread>
void work(int id) { /* ... */ }
thread t(work, 1); // starts running work(1) now
// main carries on here at the same time
t.join(); // wait for t to finish- A
std::threadstarts running as soon as it's created. Pass the function and its arguments; a lambda works too. join()waits for the thread to finish. Every thread must be joined (or detached) before itsthreadobject is destroyed, otherwise the program terminates.- C++20's
std::jthreadjoins automatically when it goes out of scope, which is safer. thread::hardware_concurrency()tells you how many threads the computer can run at once.
Data races
A data race happens when two threads access the same variable at the same time and at least one of them writes to it. Even count++ isn't a single step: it reads the value, adds one, and writes it back. If two threads read the same old value, one increment is lost.
Data races are undefined behaviour. The program might give the right answer on your computer and a wrong one on a server, or only on Tuesdays. The rule is simple: if threads share data and any of them changes it, that access must be protected.
The safest design avoids sharing altogether: give each thread its own data to work on, and combine the results when they've finished.
mutex and atomic
A mutex (mutual exclusion) is a lock that only one thread can hold at a time. Wrap it in a lock_guard, which locks in its constructor and unlocks automatically when it goes out of scope:
mutex m;
void addOrder(vector<int>& orders, int id) {
lock_guard<mutex> lock(m);
orders.push_back(id); // only one thread at a time
}For a single number such as a counter, std::atomic<int> is simpler and faster: count++ on an atomic is one indivisible step. Use atomics for simple counters and flags, and mutexes when several values or a container must change together.
std::async and futures
Often you just want to run a calculation in the background and collect its answer later. std::async does exactly that and returns a future, a placeholder for the result:
#include <future>
future<long long> f = async(launch::async, sumRange, 1, 50000000);
// do other work...
long long result = f.get(); // waits if necessary, then returns the valueIf the function throws an exception, get() re-throws it in the calling thread, so error handling works naturally. For many tasks, async is easier and safer than managing threads yourself.
C++ Threads Syntax
Concurrency essentials
#include <thread>
#include <mutex>
#include <atomic>
#include <future>
thread t(func, args...); t.join();
mutex m; { lock_guard<mutex> lock(m); /* protected */ }
atomic<int> counter{0}; counter++;
future<T> f = async(launch::async, func, args...);
T result = f.get();
unsigned cores = thread::hardware_concurrency();C++ Threads Summary
std::threadruns a function concurrently; calljoin()to wait for it to finish.- A data race, two threads touching the same data with at least one writing, is undefined behaviour.
- Protect shared data with a
mutexandlock_guard, or usestd::atomicfor simple values. - Prefer giving each thread its own data and combining results afterwards.
std::asyncreturns a future;get()waits for and returns the result.
C++ Threads Examples
Each example shows the code, the output it produces and what each part does.
Splitting work between threads
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void sumRange(const vector<int>& data, size_t from, size_t to, long long& result) {
long long total = 0;
for (size_t i = from; i < to; i++) total += data[i];
result = total;
}
int main() {
vector<int> data(1000000);
for (size_t i = 0; i < data.size(); i++) data[i] = i % 10;
long long firstHalf = 0, secondHalf = 0;
size_t mid = data.size() / 2;
thread a(sumRange, cref(data), 0, mid, ref(firstHalf));
thread b(sumRange, cref(data), mid, data.size(), ref(secondHalf));
a.join();
b.join();
cout << "First half: " << firstHalf << endl;
cout << "Second half: " << secondHalf << endl;
cout << "Total: " << firstHalf + secondHalf << endl;
return 0;
}First half: 2250000
Second half: 2250000
Total: 4500000Line-by-line explanation
- Lines 6 to 10Each thread sums its own part and writes to its own result variable, so nothing is shared for writing.
- Lines 18 to 19Two threads start immediately. ref and cref pass references; without them, thread copies its arguments.
- Lines 20 to 21Wait for both before reading the results.
- Lines 23 to 25Printing happens in main after joining, so the output is always in the same order.
Counting safely with atomic and mutex
#include <atomic>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace std;
int main() {
atomic<int> visits{0};
vector<string> log;
mutex logLock;
auto worker = [&](int id) {
for (int i = 0; i < 100000; i++) {
visits++;
}
lock_guard<mutex> lock(logLock);
log.push_back("worker " + to_string(id) + " done");
};
vector<thread> threads;
for (int id = 1; id <= 4; id++) threads.emplace_back(worker, id);
for (auto& t : threads) t.join();
cout << "Visits: " << visits << endl;
cout << "Log entries: " << log.size() << endl;
return 0;
}Visits: 400000
Log entries: 4Line-by-line explanation
- Line 10An atomic counter: each increment happens as one indivisible step.
- Lines 18 to 19lock_guard makes sure only one thread at a time adds to the log vector.
- Line 23Four threads all run the same lambda.
- Line 26Exactly 400,000 every time. With a plain int, it would often be less, because increments would be lost.
Background work with async
#include <future>
#include <iostream>
using namespace std;
long long countPrimes(int limit) {
long long count = 0;
for (int n = 2; n <= limit; n++) {
bool prime = true;
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) { prime = false; break; }
}
if (prime) count++;
}
return count;
}
int main() {
future<long long> small = async(launch::async, countPrimes, 10000);
future<long long> large = async(launch::async, countPrimes, 100000);
cout << "Working in the background..." << endl;
cout << "Primes up to 10,000: " << small.get() << endl;
cout << "Primes up to 100,000: " << large.get() << endl;
return 0;
}Working in the background...
Primes up to 10,000: 1229
Primes up to 100,000: 9592Line-by-line explanation
- Lines 18 to 19Both calculations start straight away, each in its own thread.
- Line 21main keeps running while they work.
- Lines 22 to 23get() waits for each result if it isn't ready yet, then returns it.
Common C++ Threads Mistakes
Forgetting to join
int main() {
thread t(work);
return 0; // std::terminate: t was never joined
}int main() {
thread t(work);
t.join();
return 0;
}Sharing a plain counter
int count = 0;
// two threads each run: for (...) count++; data race, wrong totalsatomic<int> count{0};
// two threads each run: for (...) count++; always correctLocking by hand and forgetting to unlock
m.lock();
if (orders.empty()) return; // returns while still locked: deadlock later
orders.pop_back();
m.unlock();lock_guard<mutex> lock(m);
if (orders.empty()) return; // unlocked automatically
orders.pop_back();C++ Threads in Real Projects
Threads in real software
Game engines run physics, audio and rendering on separate threads, and web servers handle many requests at once. C++17's parallel algorithms, such as sort(execution::par, ...), build on the same ideas.
C++ Threads FAQ
What is the difference between join and detach?
join() waits for the thread to finish, so you know its work is done before you carry on. detach() lets the thread keep running on its own with no way to wait for it or get a result; if main ends first, the program exits and the thread is stopped part-way. Prefer join, or std::jthread, and use detach only for true fire-and-forget work.
What is a deadlock and how do I avoid it?
A deadlock happens when two threads each hold a lock the other is waiting for, so both wait forever. The classic case is thread 1 locking a then b while thread 2 locks b then a. Always lock mutexes in the same order, or lock them together with C++17's scoped_lock lock(a, b);, which takes both using a built-in deadlock-avoidance algorithm.
What is a condition variable in C++?
A std::condition_variable lets a thread sleep until another thread tells it something has changed, instead of checking a flag in a busy loop. The waiting thread calls cv.wait(lock, [&] { return ready; }); with a unique_lock on a mutex, and the other thread sets ready under the same mutex and calls cv.notify_one(). The condition in the lambda protects against waking up when nothing has actually happened.
Do I need -pthread to compile threads with g++?
On Linux it's safest to add -pthread, as in g++ -std=c++20 -pthread main.cpp. Recent versions of the GNU C library build thread support into the main library, so many programs link without it, but older systems give "undefined reference to pthread_create" errors when it's missing. CMake handles it with find_package(Threads) and Threads::Threads.
What does thread_local mean?
A variable declared thread_local gets a separate copy in each thread, and each copy is destroyed when its thread ends. Because no other thread can see your copy, it needs no mutex. It's handy for per-thread caches, counters or random number engines.
C++ Threads Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the code so main waits for the thread to finish.
thread t(downloadFile);
t.();
When does a std::thread start running its function?
What is a data race?
What does lock_guard do?
Four threads each increment an atomic<int> 1,000 times. What's the final value?
C++ Threads Quiz
5 questions to check what you learned.
Which header provides std::thread?
- <process>
- <async>
- <mutex>
- <thread>
What does
f.get()do on a future?- Starts the task
- Cancels the task
- Waits for the result if needed, then returns it
- Returns immediately with 0
When is std::atomic a good choice?
- For protecting a whole vector
- For simple shared counters and flags
- For reading files
- Never
Why use ref() when passing a variable to std::thread?
- It makes the thread faster
- It locks the variable
- It's required for every argument
- std::thread copies its arguments, so ref is needed to pass a real reference
What's the safest overall design for multithreaded work?
- Share everything and add locks later
- Use as many threads as possible
- Give each thread its own data, then combine results after joining
- Avoid join