C++ std::thread Primer | join, detach, and Three Mistakes to Avoid
이 글의 핵심
std::thread creation, join and detach, thread safety, mutex/atomic/condition_variable overview, and common pitfalls with fixes.
Introduction: One thread wasn’t enough
“Why so slow?” — I/O wait and single-core usage
Image batch processing ran sequentially—each file: load → resize → save. Disk waits left the CPU idle; 10+ files took 10+ seconds.
With std::thread per file (where safe), wall time dropped by more than half—overlap I/O and CPU.
Cause of single-thread slowness:
- All work on one thread
- CPU idle during disk I/O
- Multi-core machine using one core
After std::thread: parallelize independent file work (watch shared state—see mutex).
You must call join() or detach() before destroying a std::thread—otherwise std::terminate.
Table of contents
- What is a thread
- Creating std::thread
- join and detach
- RAII for threads
- Thread safety
- Mutex basics
- condition_variable basics
- Atomic basics
- jthread and stop_token
- Common mistakes
- Performance
- Best practices
- Checklist
1. What is a thread
Process: own address space. Thread: independent execution within a process—shares heap and globals; each has its own stack.
flowchart TB
subgraph proc["Process (one address space)"]
main[Main thread]
t1[Thread 1]
t2[Thread 2]
main --- t1
main --- t2
end
Concurrent writes to shared variables without synchronization → data race.
2. Creating std::thread
// g++ -std=c++17 -pthread -o thread_hello thread_hello.cpp && ./thread_hello
#include <thread>
#include <iostream>
void sayHello() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread t(sayHello); // pass function pointer, not sayHello()
t.join();
return 0;
}
Arguments: passed by value to the new thread; use std::ref for references (watch lifetimes).
3. join and detach
join(): block until thread finishes—use when you need completion or scoped data must outlive the worker.detach(): thread runs independently—you must ensure all data it touches outlives the thread (no dangling references to stack).
joinable() before double-join.
4. RAII — ThreadGuard / jthread
C++20 std::jthread: joins on destruction; can pass stop_token for cooperative shutdown.
5. Thread safety
Global counter++ from two threads without sync → data race (result often < 2 * iterations).
Fix: mutex or atomic.
6. Mutex basics
std::mutex m;
std::lock_guard<std::mutex> lock(m);
// critical section
7. condition_variable basics
Wait with predicate to handle spurious wakeups: cv.wait(lock, []{ return ready; });
8. Atomic basics
std::atomic<int> counter{0};
counter++; // no mutex for simple increments
9. jthread + stop_token
Use stop_requested() loops for clean shutdown.
10. Common mistakes
- Destroying
std::threadwithout join/detach → terminate - Double join
detach+ reference capture to stack locals → use-after-free- Shared mutation without mutex/atomic
std::thread t(sayHello())— wrong (passsayHello, notsayHello())
11. Performance
- Don’t create unbounded threads (e.g. one per file for thousands of files)—use a pool or cap with
hardware_concurrency(). - ThreadSanitizer:
-fsanitize=thread
12. Best practices
- Default to join; detach rarely and only with clear lifetime rules
- Prefer
jthread(C++20) to avoid forgotten joins - Protect shared data; minimize lock scope
13. Checklist
- join or detach before
std::threaddestruction - No detached threads referencing stack
- Mutex/atomic for shared mutable state
-
joinable()if unsure
Related posts
- Mutex
- Atomics
- Series index
Keywords
C++ thread, std::thread, join, detach, multithreading, data race, jthread
Summary
Threads share memory—synchronize shared writes. Always join or detach; prefer RAII/jthread; next read mutex.
Next: Mutex and synchronization #7-2
FAQ
pthread link error on Linux?
A. Link with -pthread: g++ -std=c++17 -pthread -o app app.cpp
join vs detach default?
A. Default join unless you have a long-lived daemon thread and clear lifetime rules for all captured data.
References
Related posts
- condition_variable
- Atomics
- Mutex
- Stack vs heap