std::thread basics

A std::thread starts running as soon as it's constructed — there's no separate "start" call. Every thread must be either joined (the creating thread blocks until it finishes) or detached (it runs independently, and the std::thread object stops representing it) exactly once before the std::thread object is destroyed; skipping both calls std::terminate on destruction.

void printMessage(const std::string &msg) { std::cout << msg << '\n'; }

std::thread t(printMessage, "hello from a thread");
t.join();   // blocks here until t finishes -- required before t is destroyed
            
detach() is worth treating with real suspicion rather than as a convenient alternative to join(): a detached thread can keep running after main() returns, or after the objects it references have already been destroyed — there's no mechanism left to wait for it or to know when it's done. Reach for it only when the detached work is genuinely fire-and-forget and touches nothing that could be destroyed out from under it.

data races and std::mutex

A data race is two threads accessing the same memory concurrently, with at least one of them writing, and no synchronization between them — the behavior is undefined, not just "the wrong answer sometimes." A std::mutex (mutual exclusion) is the basic tool for preventing one: only one thread can hold it locked at a time, so wrapping every access to shared state in the same mutex's lock/unlock serializes those accesses.
Locking a mutex directly and unlocking it manually is exactly the kind of resource management RAII exists to avoid — an exception thrown between lock() and unlock() would leave the mutex locked forever. std::lock_guard ties the lock to a stack object's lifetime instead, the same idea as a smart pointer for a mutex:

std::mutex m;
int sharedCounter = 0;

void increment() {
  std::lock_guard<std::mutex> guard(m);   // locks m here
  ++sharedCounter;
}                                            // guard's destructor unlocks m here -- even if this threw
            

lock_guard vs. unique_lock

Can unlock early / relock?OverheadUse it when
std::lock_guardno — locks on construction, unlocks on destruction, that's the whole interfaceminimalthe default choice for a plain critical section
std::unique_lockyes — supports unlock()/lock() again, deferred locking, and ownership transferslightly moreneeded by std::condition_variable::wait, or when the lock must be released before the end of scope

avoiding deadlock: std::scoped_lock

Deadlock's classic cause: thread A locks mutex 1 then waits for mutex 2, while thread B has locked mutex 2 and is waiting for mutex 1 — neither can proceed. Locking mutexes one at a time in a fixed order avoids it if every thread agrees on the order, but std::scoped_lock (C++17) removes the need to coordinate an order at all: it locks any number of mutexes together, atomically, using a deadlock-avoidance algorithm internally.

std::mutex mA, mB;

void transfer() {
  std::scoped_lock guard(mA, mB);   // locks both, in whatever order avoids deadlock with other scoped_lock calls
  // ... work with both protected resources ...
}                                      // both unlocked here
            

condition variables

A mutex alone can't make one thread wait for another to produce something — that's what std::condition_variable is for. A waiting thread calls wait() (which atomically unlocks its unique_lock and blocks), and a producing thread calls notify_one()/notify_all() after changing the shared state the waiter is checking for.

std::mutex m;
std::condition_variable cv;
bool ready = false;

void waiter() {
  std::unique_lock<std::mutex> lock(m);
  cv.wait(lock, [] { return ready; });   // re-checks the predicate each time it's woken, to guard against spurious wakeups
  std::cout << "proceeding now that ready is true\n";
}

void setter() {
  { std::lock_guard<std::mutex> lock(m); ready = true; }
  cv.notify_one();
}
            
The predicate passed to wait() matters even though it looks optional — without it, a spurious wakeup (the OS is allowed to wake a waiting thread with no corresponding notify call) would let the thread proceed before ready actually became true.

std::atomic for simple shared state

For a single primitive value (a counter, a flag), std::atomic<T> gives lock-free (on most platforms, for the built-in types) safe concurrent access, without a separate mutex to manage — ++counter on a plain int shared across threads is a data race, but ++counter on a std::atomic<int> is a single indivisible operation by definition.

std::atomic<int> counter{0};

void worker() {
  for (int i = 0; i < 100000; ++i) ++counter;   // safe -- no mutex needed for a single atomic increment
}
            
std::atomic only protects the single operation it wraps — "check the flag, then act on it" across two separate atomic reads is still a race between the check and the act, even though each individual read is atomic. Reach for a mutex once more than one piece of state needs to change together consistently.

std::async and std::future, briefly

std::async runs a function (possibly on a new thread, possibly deferred until you ask for the result — which one is implementation-defined unless you pass a launch policy explicitly) and hands back a std::future representing its eventual result. Calling .get() on the future blocks until the result is ready and returns it — a simpler model than manually managing a thread plus a shared variable for the return value.

std::future<int> result = std::async(std::launch::async, [] { return 6 * 7; });
// ... do other work here while it runs ...
std::cout << result.get();   // blocks until the lambda finishes, then yields 42
            

traced: a protected counter vs. an unprotected one

Four threads incrementing a plain int a few hundred thousand times each, compared against four threads doing the same thing through a std::mutex — run it and the unprotected version reliably comes up short of the expected total, while the protected one doesn't: TopNotchNote/cpp/concurrency_counter_race_demo.cpp

where to go from here

C++ Best Practices — RAII, the pattern lock_guard and unique_lock both apply to mutexes.
Smart Pointers — shared_ptr's control block uses atomic reference counting internally.

reference

cppreference — thread support library
cppreference — std::atomic