目录
一、基础使用
1. 创建线程
2. join() vs detach()
3.this_thread 命名空间
二、线程安全与同步
1. 互斥锁
mutex 普通互斥锁
RAII 管理锁避免死锁
lock_guard
unique_lock
lock_guard 与 unique_lock 的区别
recursive_mutex 递归互斥锁
timed_mutex 超时互斥锁
recursive_timed_mutex递归超时互斥锁
自旋锁
atomic
CAS 无锁编程
2、条件变量
最简单的生产者-消费者
核心 API 详解
使用注意事项
保证某个线程先运行(锁 + 条件变量 + flag)
一、基础使用
1. 创建线程
#include <thread> #include <iostream> void func(int x) { std::cout << "Thread: " << x << std::endl; } int main() { std::thread t; // 创建一个线程,但这个线程什么也不做 std::thread t1(func, 42); // 函数指针 std::thread t2([](int x) { // Lambda std::cout << "Lambda: " << x << std::endl; }, 100); t1.join(); // 等待线程结束 t2.join(); return 0; }注意:std::thread 的带参构造与普通类的带参构造不同:
std::thread 必须存储参数的副本,因为线程可能在你调用 join() 之前执行,父作用域传递的参数可能已经被销毁
所以如果要在std::thread 的带参构造传递引用类型,比如:
void modify(int& x) { x = 100; } int main() { int a = 42; std::thread t(modify, a); // 传递引用 t.join(); std::cout << a << std::endl; return 0; }就要用std::ref明确告诉编译器我传递的是引用,否则上面的代码会编译失败,原因是类型不匹配,下面是正确示例:
void modify(int& x) { x = 100; } int main() { int a = 42; std::thread t(modify, ref(a)); // 传递引用 t.join(); std::cout << a << std::endl; return 0; }C++ thread 类不支持拷贝构造和拷贝赋值,但支持移动构造和移动赋值。创建一批线程:
void func(int x) { std::cout << "Thread: " << x << std::endl; } int main() { vector<thread> threads(5); // 默认构造 for (int i = 0; i < 5; i++) { threads[i] = thread(func, 10); // 移动赋值 } for (int i = 0; i < 5; i++) { threads[i].join(); } return 0; }2. join() vs detach()
join():阻塞主线程,等待子线程执行完毕(推荐,资源安全)detach():分离线程,让其在后台独立运行(需确保线程不会访问已销毁的资源)
std::thread t(func); t.detach(); // 主线程不再等待,t 不再代表任何线程重要:线程对象析构前必须调用join()或detach(),否则程序会std::terminate。
3.this_thread 命名空间
std::this_thread是 C++11 引入的一个命名空间,提供了当前线程的操作函数,让线程能够控制自身的行为。
namespace std::this_thread { // 获取当前线程 ID thread::id get_id() noexcept; // 让出 CPU 时间片 void yield() noexcept; // 暂停当前线程(阻塞) template<class Rep, class Period> void sleep_for(const chrono::duration<Rep, Period>& sleep_duration); template<class Clock, class Duration> void sleep_until(const chrono::time_point<Clock, Duration>& sleep_time); }二、线程安全与同步
1. 互斥锁
mutex 普通互斥锁
int main() { // 两个线程同时共同对一个变量++,有线程安全问题,要加锁 int x = 0; mutex mtx; int cnt1 = 0; int cnt2 = 0; cin >> cnt1 >> cnt2; thread t1([&]() mutable { for (int i = 0; i < cnt1; i++) { mtx.lock(); x++; mtx.unlock(); } }); thread t2([&]() mutable { for (int i = 0; i < cnt2; i++) { mtx.lock(); x++; mtx.unlock(); } }); t1.join(); t2.join(); std::cout << x << endl; return 0; }RAII 管理锁避免死锁
上面的 t1 线程和 t2 线程,只要有一个线程在 mtx.lock(); 和 mtx.unlock(); 之间出异常退出,都会导致 mtx 死锁,程序永远不会退出。使用 RAII 技术可以避免这个问题
void func() { srand(time(0)); if (rand() % 2 == 0) { throw exception("异常"); } else { cout << "func()" << endl; } } template<typename T> class my_lock_guard { public: lock_guard(T& mtx):_mtx(mtx) { _mtx.lock(); } ~lock_guard() { _mtx.unlock(); } private: T& _mtx; }; int main() { // 两个线程同时共同对一个变量++,有线程安全问题,要加锁 int x = 0; mutex mtx; int cnt1 = 0; int cnt2 = 0; cin >> cnt1 >> cnt2; thread t1([&]() mutable { try { for (int i = 0; i < cnt1; i++) { my_lock_guard<mutex> lock(mtx); x++; func(); // 模拟异常退出 } } catch (const exception& e) { cout << e.what() << endl; } }); thread t2([&]() mutable { for (int i = 0; i < cnt2; i++) { mtx.lock(); x++; mtx.unlock(); } }); t1.join(); t2.join(); std::cout << x << endl; return 0; }lock_guard
{ std::lock_guard<std::mutex> lock(mtx); // 构造函数自动加锁 // 临界区代码 // ... // 离开临界区调用析构函数自动解锁 }unique_lock
std::unique_lock是 C++11 引入的互斥锁包装器,相比std::lock_guard提供了更大的灵活性,但代价是轻微的性能开销
| 特性 | std::lock_guard | std::unique_lock |
|---|---|---|
| RAII 自动加解锁 | ✅ | ✅ |
| 延迟加锁 | ❌ | ✅ |
| 手动解锁/重新加锁 | ❌ | ✅ |
| 尝试加锁 (try_lock) | ❌ | ✅ |
| 条件变量配合 | ❌ (不够灵活) | ✅ (必须) |
| 所有权转移 | ❌ | ✅ (移动语义) |
| 性能开销 | 极小 | 稍大(维护状态标志) |
构造方式
#include <mutex> std::mutex mtx; // 1. 默认构造:立即加锁(最常用) std::unique_lock<std::mutex> lock1(mtx); // 2. 延迟加锁:构造时不锁,后续手动锁 std::unique_lock<std::mutex> lock2(mtx, std::defer_lock); lock2.lock(); // 手动加锁 // 3. 尝试加锁:不阻塞 std::unique_lock<std::mutex> lock3(mtx, std::try_to_lock); if (lock3.owns_lock()) { // 成功获得锁 } // 4. 假设已加锁:接管已锁定的互斥量 std::unique_lock<std::mutex> lock4(mtx, std::adopt_lock); // 此时 mtx 必须已被当前线程锁定核心成员函数
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); lock.lock(); // 加锁(阻塞) lock.try_lock(); // 尝试加锁(非阻塞) lock.try_lock_for(duration);// 尝试加锁,等待一段时间(需支持 timed_mutex) lock.try_lock_until(time); // 尝试加锁,直到某时间点 lock.unlock(); // 手动解锁 lock.owns_lock() // 返回 bool,当前是否拥有锁 lock.mutex() // 返回关联的互斥量指针 lock.operator bool()// 同 owns_lock() std::unique_lock<std::mutex> lock1(mtx); std::unique_lock<std::mutex> lock2 = std::move(lock1); // 转移所有权 // lock1 不再拥有锁 std::mutex* ptr = lock.release(); // 放弃所有权,但互斥量仍处于锁定状态 // 需要手动 unlock ptr->unlock();lock_guard 与 unique_lock 的区别
| 特性 | lock_guard | unique_lock |
|---|---|---|
| 设计定位 | 简单 RAII 包装器 | 灵活锁管理器 |
| 加锁时机 | 构造时立即加锁 | 可延迟、可提前 |
| 手动解锁 | ❌ 不支持 | ✅ 支持 |
| 重新加锁 | ❌ 不支持 | ✅ 支持 |
| try_lock | ❌ 不支持 | ✅ 支持 |
| 超时等待 | ❌ 不支持 | ✅ 支持(配合 timed_mutex) |
| 所有权转移 | ❌ 不支持 | ✅ 支持(移动语义) |
| 条件变量配合 | ❌ 不支持 | ✅必须配合 |
| 性能开销 | 零开销(近乎) | 有额外状态维护 |
| 代码简洁性 | ✅ 极简 | 稍复杂 |
recursive_mutex 递归互斥锁
普通 mutex vs recursive_mutex
#include <mutex> std::mutex m1; std::recursive_mutex m2; // 普通 mutex:同一线程二次锁定 → 死锁 void test_normal() { m1.lock(); m1.lock(); // 💀 死锁!同一线程等待自己释放锁 m1.unlock(); m1.unlock(); } // recursive_mutex:同一线程可多次锁定 void test_recursive() { m2.lock(); m2.lock(); // ✅ 允许,锁计数变为 2 m2.unlock(); // 计数 1 m2.unlock(); // 计数 0,真正释放 }工作原理
维护一个锁定计数(lock count)
同一线程每次
lock()计数 +1每次
unlock()计数 -1只有当计数归零时,其他线程才能获取锁
timed_mutex 超时互斥锁
std::timed_mutex是 C++11 引入的超时互斥量,在普通互斥量基础上增加了超时等待功能,允许线程在指定时间内尝试获取锁,避免无限阻塞。
// 普通 mutex:只有阻塞和非阻塞 std::mutex mtx; mtx.lock(); // 阻塞直到获取 mtx.try_lock(); // 立即返回 bool // timed_mutex:增加了超时功能 std::timed_mutex tmtx; tmtx.lock(); // 阻塞直到获取 tmtx.try_lock(); // 立即返回 bool tmtx.try_lock_for(duration); // 等待一段时间 tmtx.try_lock_until(time_point); // 等到某个时间点try_lock_for() - 等待一段时间
#include <mutex> #include <chrono> #include <thread> std::timed_mutex tmtx; void worker(int id) { // 尝试在 100ms 内获取锁 if (tmtx.try_lock_for(std::chrono::milliseconds(100))) { std::cout << "Thread " << id << " acquired lock" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(50)); tmtx.unlock(); } else { std::cout << "Thread " << id << " timeout, giving up" << std::endl; } }try_lock_until() - 等到指定时间点
void worker_until(int id) { auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); if (tmtx.try_lock_until(deadline)) { std::cout << "Thread " << id << " acquired lock before deadline" << std::endl; tmtx.unlock(); } else { std::cout << "Thread " << id << " missed deadline" << std::endl; } }recursive_timed_mutex递归超时互斥锁
std::recursive_timed_mutex是 C++11 引入的递归超时互斥量,结合了recursive_mutex的递归特性和timed_mutex的超时功能,是 C++ 标准库中最强大的互斥量类型。
自旋锁
在 mutex 普通互斥锁的两个线程对一个变量++的例子中,临界区代码只有一行对变量++的操作,如果采用普通互斥锁,会有较大的线程上下文切换开销(线程刚进入睡眠状态,其他线程就把临界区代码执行完了),降低了效率。这种临界区代码很短的情况适合使用自旋锁,自旋锁:如果锁被占用,当前线程不会睡眠,而是原地忙等(一直循环检测锁状态)。如果使用自旋锁,又对 cpu 的占用太高了。解决方式可以是使用下面介绍的原子变量
atomic
std::atomic是 C++11 引入的原子操作库,提供了无锁(lock-free)的线程安全数据类型,是实现高性能并发编程的关键工具。
什么是原子操作?
// ❌ 非原子操作(可能被中断) int counter = 0; counter++; // 实际是三步操作: // 1. 读取 counter 到寄存器 // 2. 寄存器 + 1 // 3. 写回 counter // 多个线程同时执行时,数据可能不一致 // ✅ 原子操作(不可中断) std::atomic<int> counter{0}; counter++; // 一步完成,线程安全原子性保证
非原子操作(可能出错): 线程A: 读取(0) → 计算(1) → 写回(1) 线程B: 读取(0) → 计算(1) → 写回(1) 结果:counter = 1(丢失一次递增) 原子操作(正确): 线程A: 读取(0) → 计算(1) → 写回(1) [不可中断] 线程B: 读取(1) → 计算(2) → 写回(2) 结果:counter = 2(正确)基本用法
1. 创建原子变量
#include <atomic> // 基本类型 std::atomic<int> ai{0}; // 初始化为 0 std::atomic<bool> flag{false}; std::atomic<long long> big{0}; // 指针类型 int value = 42; std::atomic<int*> ptr{&value}; // 自定义类型(必须是 trivially copyable) struct Point { int x, y; }; std::atomic<Point> p{Point{1, 2}};2. 基本操作
std::atomic<int> counter{0}; // 存储和加载 counter.store(10); // 写入 int value = counter.load(); // 读取 // 交换 int old = counter.exchange(20); // 交换并返回旧值 // 比较并交换(CAS) int expected = 20; bool success = counter.compare_exchange_strong(expected, 30); // 如果 counter == 20,设置为 30,返回 true // 否则 expected 被设置为 counter 的当前值,返回 false // 算术操作 counter++; // 后置++ ++counter; // 前置++ counter += 5; // 加 counter.fetch_add(3); // 加并返回旧值 counter.fetch_sub(2); // 减并返回旧值3、应用举例
在 mutex 普通互斥锁的两个线程对一个变量++的例子中,我们就可以将这个变量定义为原子变量,使得它的++操作是原子的。
int main() { // 两个线程同时共同对一个变量++,有线程安全问题,将这个变量设置为原子变量 atomic<int> x = 0; mutex mtx; int cnt1 = 10000; int cnt2 = 10000; thread t1([&]() mutable { for (int i = 0; i < cnt1; i++) { //mtx.lock(); x++; //mtx.unlock(); } }); thread t2([&]() mutable { for (int i = 0; i < cnt2; i++) { //mtx.lock(); x++; //mtx.unlock(); } }); t1.join(); t2.join(); std::cout << x << endl; return 0; }对于智能指针 shared_ptr,如果两个线程同时调用智能指针的拷贝构造,用智能指针指向同一个对象,也会有线程安全问题,因为涉及到对同一个引用计数++的操作,有了上面的原子变量类,我们就可以将智能指针的引用计数设置为原子变量。
CAS 无锁编程
AS全称Compare-And-Swap(比较并交换),是一条CPU原子指令。它的工作模式像一个乐观的“检查-执行”事务:
“我认为内存地址
V当前的值应该是A。如果是,我就把它更新为B;如果不是,我什么都不做,并告诉我失败了。”
V:要操作的内存位置(变量)
A:预期的旧值(期望值)
B:要设置的新值(更新值)
这个操作由硬件保证是原子的,不可中断。因此,我们利用它来实现“无锁”的并发更新,避免了线程阻塞和上下文切换的开销。
上面的原子变量实现对变量++操作的原子性,就是基于 CAS 指令完成的。对变量++操作的汇编代码:
mov eax,dword ptr [x] add eax,1 mov dword ptr [x],eax我们定义 CAS(V,A,B),V、A、B 的含义如上,修改上面的汇编代码如下:
do { mov old,dword ptr [x] // 存储旧值 mov eax,dword ptr [x] // eax 存储修改后的值 add eax,1 }while(!CAS(&x,old,eax));就可以实现对变量++的原子操作(读者可以分析两个线程分别执行以上汇编代码时的各种情况,可以发现是线程安全的,并且全程没有加锁,如果一个线程对变量++失败了,顶多多执行几条cpu指令)。
除了对变量++的无锁编程以外,还有对链表尾插的无锁编程:
EnQueue(Q,data) { n = new node(); n->value = data; n->next = NULL; do { p = Q->tail; // 取链表尾指针的快照 }while(!CAS(p->next,NULL,n)); CAS(Q->tail,p,n); // 更新尾结点 }C++11引入了std::atomic<T>模板类,其中最关键的两个CAS成员函数是:
compare_exchange_weak(弱比较交换)compare_exchange_strong(强比较交换)
bool compare_exchange_strong(T& expected, T desired, memory_order order = memory_order_seq_cst); bool compare_exchange_weak(T& expected, T desired, memory_order order = memory_order_seq_cst);2、条件变量
std::condition_variable是 C++ 线程同步的核心工具,用于实现线程间的事件通知和等待/唤醒机制。
最简单的生产者-消费者
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> std::mutex mtx; std::condition_variable cv; std::queue<int> queue; const int MAX_SIZE = 10; bool done = false; // 生产者 void producer() { for (int i = 0; i < 20; ++i) { std::unique_lock<std::mutex> lock(mtx); // 等待队列未满 cv.wait(lock, []{ return queue.size() < MAX_SIZE; }); queue.push(i); std::cout << "Produced: " << i << ", queue size: " << queue.size() << std::endl; // 通知消费者 cv.notify_one(); } // 生产完成 std::lock_guard<std::mutex> lock(mtx); done = true; cv.notify_all(); // 唤醒所有消费者 } // 消费者 void consumer(int id) { while (true) { std::unique_lock<std::mutex> lock(mtx); // 等待队列非空或生产完成 cv.wait(lock, []{ return !queue.empty() || done; }); if (queue.empty() && done) { std::cout << "Consumer " << id << " exiting" << std::endl; break; } int value = queue.front(); queue.pop(); std::cout << "Consumer " << id << " consumed: " << value << std::endl; cv.notify_one(); // 通知生产者 } } int main() { std::thread prod(producer); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod.join(); cons1.join(); cons2.join(); return 0; }核心 API 详解
wait() 系列
// 基础版本:无谓词 void wait(std::unique_lock<std::mutex>& lock); // 行为: // 1. 原子地释放锁并进入等待 // 2. 被唤醒后重新获取锁 // 3. 可能会被虚假唤醒 // 推荐版本:带谓词 template<typename Predicate> void wait(std::unique_lock<std::mutex>& lock, Predicate pred); // 行为: // 1. 检查 pred(),如果为 true,立即返回 // 2. 如果为 false,释放锁并等待 // 3. 被唤醒后重新获取锁,再次检查 pred() // 4. 直到 pred() 为 true 才返回 // 超时版本 template<typename Predicate> bool wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration& timeout, Predicate pred); template<typename Predicate> bool wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point& timeout, Predicate pred);notify() 系列
// 唤醒一个等待线程(如果存在) void notify_one() noexcept; // 注意:不保证唤醒哪个线程 // 唤醒所有等待线程 void notify_all() noexcept; // 所有线程被唤醒,竞争获取锁使用注意事项
1. 必须使用 unique_lock,因为要支持手动解锁
// ❌ 错误:lock_guard 不能用于条件变量 std::lock_guard<std::mutex> lock(mtx); cv.wait(lock); // 编译错误! // ✅ 正确:必须使用 unique_lock std::unique_lock<std::mutex> lock(mtx); cv.wait(lock);2. 必须使用谓词检查条件
// ❌ 危险:可能虚假唤醒 cv.wait(lock); if (condition) { // 可能因为虚假唤醒而条件不满足 } // ✅ 安全:使用谓词 cv.wait(lock, []{ return condition; }); // 自动处理虚假唤醒3. 锁的持有时间
// ❌ 错误:持锁通知 void bad_notify() { std::unique_lock<std::mutex> lock(mtx); // 修改数据 cv.notify_one(); // 锁一直持有到函数结束 // 等待线程被唤醒后无法立即获取锁 } // ✅ 正确:解锁后通知 void good_notify() { { std::lock_guard<std::mutex> lock(mtx); // 修改数据 } // 锁释放 cv.notify_one(); // 通知时锁已释放 } // ✅ 或者使用 unique_lock 提前解锁 void good_notify_v2() { std::unique_lock<std::mutex> lock(mtx); // 修改数据 lock.unlock(); // 提前释放 cv.notify_one(); }4. 虚假唤醒(Spurious Wakeup)
// 操作系统可能在没有通知的情况下唤醒线程 // 这就是为什么必须使用谓词或循环检查 // 手动处理版本 while (!condition) { cv.wait(lock); } // 使用谓词版本(推荐) cv.wait(lock, []{ return condition; }); // 内部实现就是上面的循环保证某个线程先运行(锁 + 条件变量 + flag)
题目:有两个线程 t1 和 t2,让 t1 和 t2 交替打印,t1 打印奇数,t2 打印偶数,并且保证 t1 先打印
解决方案:互斥锁 + 条件变量 + flag 标志控制
int main() { mutex mtx; condition_variable cv; bool flag = false; int x = 1; thread t1([&]() { for (int i = 0; i < 5; i++) { unique_lock<mutex> lock(mtx); while (flag) cv.wait(lock); cout << "t1: " << x << endl; x++; flag = true; lock.unlock(); cv.notify_one(); } }); thread t2([&]() { for (int i = 0; i < 5; i++) { unique_lock<mutex> lock(mtx); while (!flag) cv.wait(lock); cout << "t2: " << x << endl; x++; flag = false; lock.unlock(); cv.notify_one(); } }); t1.join(); t2.join(); return 0; }