C++ Multithreading & Concurrency
std::thread and join discipline, mutex and lock_guard, condition variables, atomics, thread pools and common data races.
Threads and join
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "worker " << id << '\n';
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // 等待结束(必须 join 或 detach)
t2.join(); // 否则 std::terminate
// RAII 封装:join 在析构自动执行
return 0;
}
A thread that is neither joined nor detached before destruction calls std::terminate and kills the process. Wrap threads in a joining guard or always join at scope exit.
Mutex and Condition Variables
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex m;
std::condition_variable cv;
std::queue<Task> q;
// 生产者
void push(Task t) {
{
std::lock_guard lk(m); // 离开作用域解锁
q.push(std::move(t));
}
cv.notify_one(); // 唤醒一个等待者
}
// 消费者
Task take() {
std::unique_lock lk(m); // 配合 cv 需可解锁
cv.wait(lk, [] { return !q.empty(); }); // 谓语防虚假唤醒
Task t = std::move(q.front());
q.pop();
return t;
}
Prefer lock_guard for plain critical sections and unique_lock only when you need to unlock within the block (condition variables, deferred locks). Never lock two mutexes in different orders across threads — that is a textbook deadlock.
Atomics
#include <atomic>
std::atomic<long> counter{0}; // 无锁计数
void hit() {
counter.fetch_add(1, std::memory_order_relaxed); // 不需要顺序保证
}
std::atomic<bool> ready{false};
// 发住 once 或广播
ready.store(true, std::memory_order_release); // 发布数据
while (!ready.load(std::memory_order_acquire)); // 看到 ready 即可见数据
// 指针/标志的 CAS 循环:无锁结构基础
current.compare_exchange_strong(expected, desired);
Relaxed for counters, acquire/release for publishing data, seq_cst (default) when in doubt. A wrong memory order compiles and passes tests while failing on other CPUs.
Thread Pools in Practice
- Creating a thread per task is wasteful at high QPS — size the pool once, typically hardware_concurrency() for CPU-bound work.
- Hand-rolled pools are fine to learn, but production uses asio, Intel TBB or a vetted header-only pool.
- Use thread_local for per-thread caches (connections, scratch buffers) to avoid contention.
- Profile with perf or VTune: lock contention, not computation, is usually the bottleneck.
线程与 join
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "worker " << id << '\n';
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // 等待结束(必须 join 或 detach)
t2.join(); // 否则 std::terminate
// RAII 封装:join 在析构自动执行
return 0;
}
线程既未 join 也未 detach 就被析构会触发 std::terminate 直接杀进程。用 RAII 包装或总是在作用域出口 join。
互斥量与条件变量
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex m;
std::condition_variable cv;
std::queue<Task> q;
// 生产者
void push(Task t) {
{
std::lock_guard lk(m); // 离开作用域解锁
q.push(std::move(t));
}
cv.notify_one(); // 唤醒一个等待者
}
// 消费者
Task take() {
std::unique_lock lk(m); // 配合 cv 需可解锁
cv.wait(lk, [] { return !q.empty(); }); // 谓语防虚假唤醒
Task t = std::move(q.front());
q.pop();
return t;
}
普通临界区用 lock_guard;需要在块内解锁(条件变量、延迟加锁)才用 unique_lock。多个线程以不同顺序锁两个互斥量就是教科书级死锁。
原子操作
#include <atomic>
std::atomic<long> counter{0}; // 无锁计数
void hit() {
counter.fetch_add(1, std::memory_order_relaxed); // 不需要顺序保证
}
std::atomic<bool> ready{false};
// 发住 once 或广播
ready.store(true, std::memory_order_release); // 发布数据
while (!ready.load(std::memory_order_acquire)); // 看到 ready 即可见数据
// 指针/标志的 CAS 循环:无锁结构基础
current.compare_exchange_strong(expected, desired);
计数器用 relaxed,发布数据用 acquire/release,拿不准就用默认 seq_cst。错误的内存序能编译通过、本机测试也过,换个 CPU 就出问题。
实际项目中的线程池
- 高 QPS 下逐任务建线程很浪费:池只建一次,CPU 密集型取 hardware_concurrency()。
- 手写池可以学习,生产用 asio、Intel TBB 或成熟的开源池。
- thread_local 存放线程私有缓存(连接、临时缓冲),减少竞争。
- 用 perf / VTune 分析:瓶颈往往不是计算而是锁竞争。