Skip to content

Beginning of the lesson

Multithreading

Running code in parallel safely: threads, mutexes, condition variables, and deadlock avoidance

Big Idea: A thread is an independent flow of execution inside a single process. Every thread shares the same address space - the same globals, the same heap - which is exactly what makes threads powerful and dangerous at once. Sharing means threads can cooperate cheaply; it also means two threads touching the same data at the same time can corrupt it. Modern C++ (since C++11, refined through C++17 and C++20) gives you a portable threading library so you no longer need platform-specific APIs like pthreads or Win32 threads.
By the end you will be able to:
  • launch work on a std::thread and manage its lifetime with join/detach - and prefer C++20 std::jthread;
  • explain what a data race is and why it is undefined behaviour;
  • protect shared state with std::mutex and the RAII guards lock_guard and unique_lock;
  • coordinate threads with a std::condition_variable using the predicate-wait idiom;
  • recognise a deadlock and avoid it with lock ordering and std::scoped_lock.

1. Concurrency vs. parallelism, and std::thread

Concurrency is a structuring property: several tasks are in progress over overlapping periods. Parallelism is an execution property: tasks literally run at the same instant on different cores. Threads let you express concurrency; whether it becomes real parallelism depends on the hardware. Use std::thread::hardware_concurrency() as a hint for how many cores are available.

A std::thread (from <thread>) starts running its callable immediately upon construction. You pass any callable - a function pointer, a lambda, a functor - plus its arguments. Arguments are copied into the thread by default; to pass by reference you must wrap them in std::ref.

#include <thread>
#include <iostream>

void greet(int id) { std::cout << "hello from " << id << "\n"; }

int main() {
    std::thread t(greet, 42);   // starts running now
    t.join();                   // wait for it to finish
}

Before a std::thread is destroyed you must decide its fate: join() blocks until the thread finishes, while detach() lets it run independently and severs your handle. If you destroy a joinable thread without doing either, the destructor calls std::terminate() and your program aborts. This harsh rule exists to stop you accidentally leaking a running thread.

C++20: std::jthread. A jthread ("joining thread") fixes the footgun: its destructor automatically joins, so forgetting to join no longer terminates the program. It also carries a std::stop_token so the thread can be asked to stop cooperatively. Prefer std::jthread in new C++20 code.

Continue this lesson on YourAIZone

The full lesson has the complete explanation, worked examples, common mistakes, tests with instant checking and an AI tutor. Start right away as a guest, or create an account to keep your progress.

In the full lesson

  • The complete explanation
  • Worked examples
  • Common mistakes and how to avoid them
  • Test questions: 15
  • AI tutor that answers questions about the topic
  • Progress tracking for students, parents and teachers