Expert · Exceptions, concurrency and building projects with CMake · topic 35 of 42 · 35 min
Concurrency 1: threads, mutexes and data races
Explanation
Why concurrency?
Modern computers and phones have several CPU cores. A normal program uses one. Concurrency means running several tasks at overlapping times, for example resizing 1,000 photos on 8 cores, or keeping an app's screen responsive while a download runs. In C++, the basic tool is a thread: an independent sequence of instructions inside the same program, sharing the same memory.
#include <iostream>
#include <thread>
void download(int fileId) {
std::cout << "downloading " << fileId << '\n';
}
int main() {
std::thread t1(download, 1);
std::thread t2(download, 2);
t1.join(); // wait for t1 to finish
t2.join();
std::cout << "all done\n";
}Data races
int counter = 0;
void addMany() { for (int i = 0; i < 100000; ++i) ++counter; }
std::thread a(addMany), b(addMany);
a.join(); b.join();
std::cout << counter << '\n'; // often LESS than 200000, and different each runA data race happens when two threads access the same memory at the same time, at least one of them writes, and nothing orders the accesses. In C++, a data race is undefined behaviour: wrong totals, crashes, or bugs that appear only on some machines and only sometimes. These are among the hardest bugs to find.
Protecting shared data with a mutex
#include <mutex>
#include <thread>
int counter = 0;
std::mutex counterMutex;
void addMany() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(counterMutex); // locks here
++counter;
} // unlocks here (RAII)
}
// Now counter is always 200000.Deadlock
A deadlock happens when thread A holds mutex 1 and waits for mutex 2, while thread B holds mutex 2 and waits for mutex 1. Both wait forever. Avoid it by always locking mutexes in the same order, or by locking several at once with std::scoped_lock lock(m1, m2); (C++17), which uses a deadlock-avoidance algorithm.
Good habits
- Share as little as possible. Give each thread its own data and combine the results at the end.
- Keep locked sections short; never do slow I/O while holding a lock if you can avoid it.
- Never call unknown code (callbacks) while holding a lock.
- Use thread-safe tools from the standard library instead of inventing your own.
- Compile with -fsanitize=thread (ThreadSanitizer, GCC and Clang) while testing: it reports data races with the lines involved.
How many threads?
std::thread::hardware_concurrency() returns a hint of how many threads can truly run at once (often the number of logical cores). Creating thousands of threads is wasteful: each has its own stack and switching between them costs time. For many small jobs, real projects use a thread pool (a fixed set of worker threads taking tasks from a queue), usually from a library.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Concurrency 1: threads, mutexes and data races · 1/6
Threads let one program use several CPU cores at the same time.
Prefer a video by a teacher? Find free videos on “c++ std::thread mutex lock_guard data race tutorial” on YouTube ↗
In real life: Processing photos on all cores
A photo-printing shop's software must make thumbnails of 800 wedding photos. On an 8-core computer, splitting the list into 8 parts and giving each part to a thread finishes the job several times faster than one thread. Each thread writes only its own part of the results vector, so no lock is needed for the work itself.
#include <iostream>
#include <thread>
#include <vector>
int makeThumbnail(int photoId) { return photoId * 2; } // stands in for real image work
int main() {
const int photos = 800;
unsigned workers = std::thread::hardware_concurrency();
if (workers == 0) workers = 4; // the hint may be unavailable
std::vector<int> results(photos);
std::vector<std::jthread> pool;
for (unsigned w = 0; w < workers; ++w) {
pool.emplace_back([&results, w, workers] {
for (int i = static_cast<int>(w); i < photos; i += static_cast<int>(workers)) {
results[i] = makeThumbnail(i); // each index written by one thread only
}
});
}
pool.clear(); // jthreads join here
std::cout << results[799] << '\n'; // 1598
}Practice (0/4 done)
What does t.join() do?
Two threads each run ++counter 100,000 times on a shared int with no lock. What is guaranteed about the final value?
A std::thread object is destroyed while still joinable (never joined or detached). What happens?
Write a program where 4 threads each add the numbers in their quarter of a vector of 1,000,000 ints (all 1s). First let them all add into one shared total protected by a mutex; then change it so each thread writes its partial sum into its own slot of a vector, and main adds the 4 partial sums. Measure both with <chrono>.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Concurrency 2: atomics, async, futures and condition variablesExceptions, concurrency and building projects with CMake · same module
- Building real projects: CMake, warnings, debuggers and sanitizersExceptions, concurrency and building projects with CMake · same module
- Shared state: Arc, Mutex, Send and SyncRust Programming · Concurrency and async
- ThreadsRust Programming · Concurrency and async
- Race conditions and synchronizedJava Programming (Core to Spring Boot) · Multithreading and concurrency
- Concurrency interview questions and coding roundsGo Programming (backend and cloud) · Careers and interviews
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is a mutex for?
Answer: Allowing only one thread at a time into a section that touches shared data
Mutual exclusion prevents simultaneous access.
Why use std::lock_guard instead of calling m.lock() and m.unlock() yourself?
Answer: It unlocks automatically, even when an exception is thrown (RAII)
Manual unlock is easy to miss on an early return or exception.
What is a deadlock?
Answer: Threads waiting for each other's locks forever
Circular waiting means none can proceed.
Which tool detects data races at run time?
Answer: -fsanitize=thread (ThreadSanitizer)
ThreadSanitizer instruments memory accesses and reports races.
What does a Mutex guarantee?Related
Answer: Only one thread accesses the data at a time
Mutual exclusion is its purpose.
What is special about std::thread::scope?Related
Answer: Threads in it can borrow local data because they must finish inside the scope
Scoped threads are joined before the scope ends.
From Threads
Two bank balances must be updated together by several threads. Which tool?Related
Answer: One std::mutex locked around both updates
Only a lock makes the combined change appear all at once.
From Concurrency 2: atomics, async, futures and condition variables
How do you prevent the classic two-lock deadlock?Related
Answer: Always acquire locks in the same order
A global lock order breaks the cycle.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Exceptions, concurrency and building projects with CMake” before its exam: Concurrency 2: atomics, async, futures and condition variables, Building real projects: CMake, warnings, debuggers and sanitizers.
- Module 9 exam: Exceptions, concurrency and CMake · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Competitive programming, interviews and careers. 42 topics left in the course before the final exam.