Site

Threads — std::thread and mutex

Tutorial S9.0  •  C++ / Learn / StdLib

S9.0 What This Teaches

C++ threads let you run code concurrently. This tutorial covers:

S9.1 Creating a Thread

#include <thread>
#include <iostream>

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

int main() {
    std::thread t(say_hello, 1);
    t.join();   // wait for t to finish before continuing
    std::cout << "done\n";
}
std::thread starts immediately on construction. Call join() before the thread object is destroyed, or the program will terminate with an error.

S9.2 join and detach

std::thread t(say_hello, 2);

// Option 1: join - caller blocks until t finishes
t.join();

// Option 2: detach - t runs independently; caller does not wait
// std::thread t2(say_hello, 3);
// t2.detach();   // t2 is now a daemon thread; do NOT access t2 after this
Prefer join() for threads that need to finish before the program continues. Use detach() only for fire-and-forget background tasks, and ensure the thread does not reference local variables that will go out of scope.

S9.3 Passing Arguments

void add(int a, int b, int& result) {
    result = a + b;
}

int main() {
    int r = 0;
    std::thread t(add, 3, 4, std::ref(r));   // std::ref wraps reference args
    t.join();
    std::cout << r << "\n";   // 7
}
Thread arguments are copied by default. Wrap reference arguments with std::ref(). Use lambdas for cleaner capture of local state.

S9.4 mutex and lock_guard

#include <mutex>

std::mutex mtx;
int counter = 0;

void increment() {
    std::lock_guard<std::mutex> lock(mtx);   // locked on construction
    ++counter;
}   // lock released here (RAII)
std::mutex protects shared data. std::lock_guard acquires the mutex on construction and releases it on destruction - no manual unlock needed. Always use RAII wrappers rather than calling lock()/unlock() directly.

S9.5 Vector of Threads

std::mutex mtx;
int shared = 0;

void worker(int id) {
    std::lock_guard<std::mutex> lock(mtx);
    ++shared;
    std::cout << "thread " << id << " -> " << shared << "\n";
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i)
        threads.emplace_back(worker, i);
    for (auto& t : threads)
        t.join();
    std::cout << "final: " << shared << "\n";
}

S9.6 Example - All Together

// Threads - std::thread, join, mutex, lock_guard, shared data.

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <string>

std::mutex g_mutex;

void worker(int id, int& shared_count) {
    std::lock_guard<std::mutex> lock(g_mutex);
    ++shared_count;
    std::cout << "thread " << id << " incremented count to " << shared_count << "\n";
}

int main() {
    int count = 0;
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i)
        threads.emplace_back(worker, i, std::ref(count));
    for (auto& t : threads)
        t.join();
    std::cout << "final count: " << count << "\n";
    return 0;
}
thread 0 incremented count to 1
thread 1 incremented count to 2
thread 2 incremented count to 3
thread 3 incremented count to 4
final count: 4
Thread scheduling is non-deterministic. The order of output lines may vary between runs, but the final count is always 4 because each increment is protected by the mutex.

S9.7 Exercise

Exercise
  • Launch 5 threads, each of which appends its id to a std::vector<int> (protected by a mutex). After joining all threads, print the vector.
  • Remove the mutex from the example above and run it many times. Observe the data race. Re-add the mutex to fix it.
  • Write a function parallel_sum that splits a vector<int> in half, sums each half in a separate thread, and returns the total.

S9.8 Common Mistakes

Forgetting to join (or detach)

void bad() {
    std::thread t([]{ std::cout << "hi\n"; });
}   // destructor of t calls std::terminate() if t is joinable
Always call join() or detach() before the thread object is destroyed. Use a RAII wrapper or std::jthread (C++20) to automate this.

Accessing a moved-from thread

std::thread t(worker, 0, std::ref(count));
std::thread u = std::move(t);
t.join();   // error: t no longer owns the thread after move

Detached thread accessing destroyed local variables

void bad() {
    int local = 42;
    std::thread t([&local]{ std::this_thread::sleep_for(std::chrono::seconds(1));
                             std::cout << local; });   // dangling ref
    t.detach();
}   // local destroyed; thread still running
Capture by value when detaching, or ensure the thread's lifetime is bounded by the variable's lifetime.

S9.9 Key Terms

TermMeaning
std::threadRepresents a single OS thread; starts on construction
join()Block the calling thread until this thread finishes
detach()Allow thread to run independently; caller does not wait
std::ref()Wrap a reference so it can be passed to a thread constructor
std::mutexMutual-exclusion primitive; only one thread holds it at a time
std::lock_guardRAII mutex wrapper; locks on construction, unlocks on destruction
data raceTwo threads access the same memory concurrently, at least one writing, without synchronization - undefined behavior
std::jthreadC++20 thread that automatically joins on destruction