CppMultithreading (std::thread)

Multithreading (std::thread)

Every program you've written so far has run one instruction after another, on a single thread of execution. Modern CPUs have multiple cores, and modern C++ (since C++11) has a standard, portable way to run code concurrently across them: the <thread> header.
Creating and starting a thread
Constructing a std::thread object immediately starts running the given function on a new thread — there's no separate "start" call.

basic-thread.cpp

CPP
#include <iostream>
#include <thread>

void printMessage() {
  std::cout << "Hello from a background thread!\n";
}

int main() {
  std::thread t(printMessage); // starts running immediately

  std::cout << "Hello from main thread!\n";

  t.join(); // wait for t to finish before continuing
  return 0;
}

The two messages can print in either order — the operating system decides how to schedule the two threads. That non-determinism is a fundamental feature (and challenge) of concurrent programming.

join() waits for the thread to finish
Calling .join() blocks the calling thread until the target thread completes. It's how you make sure background work has actually finished before your program relies on its results, or exits.
A thread must be joined or detached before its destructor runs
If a std::thread object is still "joinable" (neither .join() nor .detach() has been called) when its destructor runs, the program calls std::terminate() and crashes immediately. This is one of the most common beginner mistakes with threading — forgetting to join a thread before it goes out of scope, often because of an early return or an exception skipping past the join() call.

forgotten-join.cpp

CPP
#include <thread>

void doWork() { /* ... */ }

void broken() {
  std::thread t(doWork);
  // Oops — no t.join() or t.detach() here.
} // t's destructor runs now, joinable == true -> std::terminate()!
detach() — letting a thread run independently
.detach() separates the std::thread object from the actual OS thread, letting it keep running in the background even after the std::thread object is destroyed. It's useful for fire-and-forget background tasks, but it comes with a cost: you lose the ability to ever join it or know when it finishes, and it must not outlive resources it depends on (like references to local variables of a function that has already returned).

detach-example.cpp

CPP
#include <chrono>
#include <iostream>
#include <thread>

void backgroundLogger() {
  std::this_thread::sleep_for(std::chrono::seconds(1));
  std::cout << "Logged in the background\n";
}

int main() {
  std::thread t(backgroundLogger);
  t.detach(); // main won't wait for this thread

  std::cout << "Main thread continues immediately\n";
  // Program may exit before backgroundLogger finishes printing.
  return 0;
}
Passing arguments to a thread function
Extra arguments to the std::thread constructor are forwarded to the function when the thread starts, just like calling the function directly.

thread-with-args.cpp

CPP
#include <iostream>
#include <string>
#include <thread>

void greet(const std::string& name, int times) {
  for (int i = 0; i < times; ++i) {
    std::cout << "Hello, " << name << "!\n";
  }
}

int main() {
  std::thread t(greet, "Alice", 3);
  t.join();
  return 0;
}
Note
To pass a reference (rather than a copy) into a thread function, wrap the argument in std::ref() — otherwise std::thread copies each argument by default, even if the target function's parameter is declared as a reference.
When threading helps — and what it costs
Threads shine when work is I/O-bound (waiting on disk, network, or another process) or when a CPU-heavy task can be split into independent chunks that run on separate cores at once. But running code concurrently introduces a new class of bug that single-threaded programs never face: multiple threads reading and writing the same shared data at the same time can corrupt it or produce wrong results, even though each thread's own code looks perfectly correct in isolation. That problem — and how to fix it safely — is exactly what the next page, Mutexes & Locks, covers.
  • Constructing a std::thread starts it running immediately.

  • .join() blocks until the thread finishes; .detach() lets it run independently.

  • Every joinable thread must be joined or detached before destruction, or the program terminates.

  • Extra constructor arguments are forwarded to the thread function; use std::ref() to pass by reference.

  • Shared mutable data accessed from multiple threads needs explicit synchronization — covered next.