Skip to content
Theory · Multithreading: An Overview · lesson 1 of 12

Threads: why and how

What you'll learn

What you'll learn:

  • start a function in a separate thread and wait for it with join();
  • explain what a data race is and protect shared data with a mutex;
  • choose between a mutex and std::atomic;
  • collect a result from another thread with std::async and measure time with the types from <chrono>.
Two cooks at one counter work at once from the cards main and worker and share a fridge; the join() step on the main card waits for the salad

Picture a restaurant kitchen. A cook makes soup from a recipe card: step by step, one line at a time. While the cook chops vegetables, nobody makes the salad, even though there is a free spot at the counter. A second cook could stand there and make the salad at the same time as the soup. Each cook has their own card, but they share one fridge.

A program works the same way. It executes its statements one at a time: while a function computes, the rest of the work waits. Yet a processor usually has several cores, which are the spots at the counter. Each core executes its own sequence of statements at the same time as the others.

A thread is a cook with their own card: a sequence of statements that executes independently of the others. Every program has at least one thread, the one that runs main. To run two tasks at once, you need a second thread. The std::thread object from the <thread> header starts it:

C++
#include <iostream>
#include <thread>

void print_squares() {
    for (int i = 1; i <= 3; ++i) {
        std::cout << i * i << ' ';  // 1 4 9
    }
}

int main() {
    std::thread worker(print_squares);
    worker.join();
    std::cout << "done\n";  // done
}

This is where the counter-spot analogy ends: you can start more threads than there are cores. A core then switches between threads quickly, and from the outside their work still looks simultaneous.

Starting and waiting

Let's go through main line by line:

  1. std::thread worker(print_squares); creates a thread and immediately starts print_squares in it. Meanwhile, main does not wait: the next line runs in the main thread alongside the new one.
  2. worker.join(); stops main until the worker thread finishes. This is the head cook waiting for the second one to finish the salad.
  3. std::cout << "done\n"; runs only after that, so done is always printed after the squares.

If main printed done before join(), the output of the two threads could mix. The order in which threads execute their statements is not known in advance.

Arguments and shared memory

The function's arguments go to the constructor after its name: std::thread t(f, 1, 2) calls f(1, 2) in the new thread. You can pass a lambda instead of a function, too.

All threads of a program share its memory, like the cooks share the fridge: a global variable is one and the same for all of them.

The course's exercises have no threads: code with std::thread or std::async does not work there, so run this chapter's examples on your own computer.

detach and jthread

Instead of join(), you can call detach(): the thread keeps running on its own, but you can no longer wait for it. That is risky. If such a thread reads local variables of the function that started it, it may access them after they are destroyed. That is undefined behavior.

C++20 added std::jthread. It is started the same way as std::thread, but the std::jthread destructor calls join() by itself. So you cannot forget to wait for such a thread.

Common mistake

The thread is started but never waited for:

C++
int main() {
    std::thread worker(print_squares);
    std::cout << "done\n";
}  // ✗
TEXT
terminate called without an active exception

The program builds without warnings. But on exit from main, the worker object is destroyed although neither join() nor detach() has been called on it. In that case the std::thread destructor calls std::terminate, and the program ends abnormally. Call join() before the end of the scope, or use std::jthread.

Key points

  • A thread is a sequence of statements that runs at the same time as others; std::thread starts a function in a new thread.
  • join() waits until the thread finishes; the order of different threads' statements is not known in advance.
  • All threads of a program share one memory.
  • A std::thread object destroyed without join() or detach() calls std::terminate; std::jthread calls join() by itself.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way