自 C++11 标准引入以来,C++ 终于拥有了跨平台的原生多线程支持,不再需要依赖操作系统的 API(如 Linux 的 pthreads 或 Windows 的 CreateThread)。
本指南涵盖从基础到 C++20 的高级特性,分为八大模块。
基础架构与头文件
线程管理 (std::thread)
互斥与死锁防护 (std::mutex 家族)
线程同步与通信 (std::condition_variable)
异步编程与返回值 (std::future)
原子操作与无锁编程 (std::atomic)
C++20 新特性 (std::jthread, 信号量, 屏障)
最佳实践与常见陷阱
基础架构与头文件
C++ 并发编程的功能分散在以下几个标准头文件中:
|---------------------------------|-------------|---------------------------------------------------------------|
| 头文件 | 核心功能 | 关键类/关键字 |
| <thread> | 线程创建与管理 | std::thread, std::this_thread |
| <mutex> | 互斥量(锁) | std::mutex, std::lock_guard, std::unique_lock, std::call_once |
| <condition_variable> | 条件变量(等待/通知) | std::condition_variable |
| <future> | 异步任务与结果获取 | std::future, std::promise, std::async |
| <atomic> | 原子操作(无锁) | std::atomic |
| <semaphore> (C++20) | 信号量 | std::counting_semaphore |
| <latch> / <barrier> (C++20) | 线程屏障 | std::latch, std::barrier |
一、 线程主体 <thread>
1. std::thread 类
构造函数 (Constructors)
cpp#include <thread> template< class Function, class... Args > explicit thread( Function&& func, Args&&... args );作用:创建新线程,指定执行函数及参数(对应 pthread_create)。
特性:
- 构造函数:
std::thread t(func, args...),func为线程执行函数,args为传递给函数的参数;- func几乎可以接受任何形式的函数, func可以带返回值但是会被忽略
- 强制规则:线程对象创建后必须调用
join()或detach(),否则析构时会调用std::terminate()终止整个程序;- 可移动不可拷贝:
std::thread是移动类型(std::move转移所有权),不能直接赋值 / 拷贝。基础示例
cpp#include <iostream> #include <thread> // 1. 普通函数 void func(int x) { std::cout << "Func: " << x << "\n"; } // 2. 仿函数 (Functor) struct Task { void operator()() { std::cout << "Functor\n"; } }; int main() { // 启动方式 1: 函数指针 std::thread t1(func, 10); // 启动方式 2: Lambda (最常用) std::thread t2([](int x) { std::cout << "Lambda: " << x << "\n"; }, 20); // 启动方式 3: 仿函数对象 std::thread t3(Task()); // 启动方式 4: 类的成员函数 (必须传递对象指针) struct Worker { void work(int n) { std::cout << "Member: " << n << "\n"; } }; Worker w; std::thread t4(&Worker::work, &w, 30); // &w 充当 this 指针 t1.join(); t2.join(); t3.join(); t4.join(); }1. 普通函数 (无参数)
cppvoid hello() { /*...*/ } // Function = void(*)() // Args 为空 std::thread t(hello);2. 带参数的函数 (值传递)
cppvoid add(int a, int b) { /*...*/ } // Function = void(*)(int, int) // Args = int, int std::thread t(add, 10, 20);3. 引用传递 (必须使用 std::ref)
这是新手最容易踩的坑。 因为 std::thread 内部默认会拷贝参数,如果你想传引用,必须用 std::ref 包装。
cppvoid update(int& n) { n++; } int x = 0; // 错误!编译报错或无法修改 x,因为 thread 内部拷贝了一份 x // std::thread t1(update, x); // 正确:使用 std::ref 告诉 thread 这是一个引用 std::thread t2(update, std::ref(x));4. 类成员函数 (绑定 this 指针)
由于成员函数隐含一个 this 指针参数,所以必须把对象的地址传进去。
cppclass Worker { public: void doWork(int id) { /*...*/ } }; Worker w; // Function = void(Worker::*)(int) // Args = Worker*, int // 参数1:成员函数地址 // 参数2:对象指针 (即 this) // 参数3:函数的参数 std::thread t(&Worker::doWork, &w, 99);5. 独占指针 (Move Only 类型)
如果参数是 std::unique_ptr 这种不能拷贝只能移动的类型,std::thread 会自动处理移动语义。
cppvoid process(std::unique_ptr<int> ptr) { /*...*/ } auto p = std::make_unique<int>(100); // p 被移动进线程,主线程中 p 变为空 std::thread t(process, std::move(p));
成员函数 (Member Functions)
- join(): 阻塞。当前线程停下来,等待该 thread 对象代表的线程执行完毕。
- detach(): 分离。切断对象与内核线程的联系。内核线程在后台运行,对象不再控制它。
- joinable(): 检查。返回 bool。如果线程还在运行且没被 join/detach,返回 true。析构前必须确保此函数返回 false。
- get_id(): 获取该线程的唯一 ID (std::thread::id)。
- native_handle(): 返回底层句柄(如 Linux 的 pthread_t 或 Windows 的 HANDLE),用于调用系统原生 API。
- swap(thread& other): 交换两个线程对象的底层句柄。
等待线程终止:
join()作用:阻塞调用线程(如主线程),直到目标线程执行完毕,回收线程资源(对应 pthread_join);虽然线程函数返回清理了栈内存,但线程对象本身(OS 内核层面的线程描述符)需要通过 join() 来确认并回收。如果不 join,std::thread 析构时会崩溃。
cppvoid std::thread::join();关键特性:
- 阻塞(Block):调用 t.join() 的线程(通常是主线程)会被卡住,直到线程 t 执行完毕(正常返回)。
- 异常安全:若主线程抛出异常,可能导致
join()未调用,建议用 RAII 封装(如std::scoped_thread,C++20);- 状态变化:调用后线程对象变为 "非可连接状态",
joinable()返回false。示例(异常安全的 join)
cpp#include <iostream> #include <thread> #include <stdexcept> using namespace std; void risky_task() { throw runtime_error("子线程抛出异常"); } // RAII 封装:自动 join 线程 class ThreadGuard { public: explicit ThreadGuard(thread& t) : t_(t) {} ~ThreadGuard() { if (t_.joinable()) { // 检查是否可 join t_.join(); } } // 禁止拷贝(避免多次 join) ThreadGuard(const ThreadGuard&) = delete; ThreadGuard& operator=(const ThreadGuard&) = delete; private: thread& t_; }; int main() { thread t(risky_task); ThreadGuard guard(t); // 析构时自动 join return 0; }设置分离状态:
detach()作用:将线程设置为 "分离状态",线程结束后由系统自动回收资源(对应 pthread_detach);
cppvoid std::thread::detach();作用与机制:
分离(Sever Connection):切断 std::thread 对象与底层 OS 线程的联系。
守护线程(Daemon):该线程会在后台继续运行,直到函数结束或者进程退出时被强制终止。
自动回收:当该后台线程运行结束时,C++ 运行时库和操作系统会自动回收其资源,不需要主线程操心。
资源安全 :分离线程禁止访问主线程的局部变量(生命周期可能先结束);
cpp#include <iostream> #include <thread> #include <chrono> using namespace std; void background_task() { for (int i = 0; i < 5; i++) { this_thread::sleep_for(chrono::seconds(1)); cout << "分离线程运行中..." << endl; } } int main() { thread t(background_task); t.detach(); // 设置为分离状态 cout << "主线程继续执行,无需等待分离线程" << endl; // 主线程休眠 6 秒,确保分离线程执行完毕 this_thread::sleep_for(chrono::seconds(6)); return 0; }风险(为什么推荐度低):
生命周期失控:如果分离的线程引用了主线程的局部变量(例如 int&),而主线程先结束了,该变量被销毁,子线程就会访问非法内存(悬空引用),导致难以调试的崩溃。
无法通过常规手段控制停止:一旦分离,你就失去了它的句柄(Handle),无法再 join 它或控制它。
强制终止: std::terminate()
这是"核选项",通常不是用来终止线程的,而是用来杀死整个进程的。
cpp[[noreturn]] void std::terminate() noexcept;作用与机制:
立即崩溃:它会调用当前的 terminate_handler,默认情况下是调用 std::abort()。这会导致整个程序(Process)立即退出,不仅是当前线程。
不清理资源 :此时栈上的对象不会调用析构函数,打开的文件可能没关闭,缓冲区数据可能没写入磁盘。
触发场景(通常是被动触发):
std::thread 对象析构时,如果线程还是 joinable 状态(既没 join 也没 detach)。
线程抛出了异常,但这异常没有被 try-catch 捕获。
为什么禁止使用:除非遇到无法修复的致命错误需要立即把程序杀掉,否则绝不应该主动调用它来结束线程。
获取当前线程的ID: get_id()
作用:获取当前线程的唯一标识符(ID)。
返回值:std::thread::id 类型(通常是一个数字,但标准库封装成了对象)。
比喻:就像每个人都有身份证号。在多线程调试时,你要知道"这句话是哪个线程打印出来的",就需要看 ID。
应用场景:
日志打印:调试多线程程序时区分日志来源。
主线程判断:检查当前是否在主线程中运行。
数据关联:用 ID 作为 std::map 的 Key,为不同线程存储私有数据。
cpp#include <iostream> #include <thread> void worker() { // 获取当前(子)线程的 ID std::cout << "子线程 ID: " << std::this_thread::get_id() << std::endl; } int main() { std::thread t(worker); // 获取主线程的 ID std::cout << "主线程 ID: " << std::this_thread::get_id() << std::endl; t.join(); return 0; }
静态成员函数 (Static Functions)
- std::thread::hardware_concurrency(): 返回 CPU 核心数(逻辑核心)。如果无法获取,返回 0。常用于决定线程池的大小。
2. std::this_thread 命名空间
这里的函数只能控制当前正在运行代码的那个线程。
get_id(): 获取当前线程 ID。
yield(): 主动让出 CPU 时间片,重新进入调度队列。
sleep_for(duration): 睡眠指定时长(如 2s)。
sleep_until(time_point): 睡眠直到某个绝对时间点(如明天 8:00)。
1. get_id():我是谁?
cppstd::thread::id get_id() noexcept;作用:获取当前线程的唯一标识符(ID)。
返回值:std::thread::id 类型(通常是一个数字,但标准库封装成了对象)。
生活比喻:就像每个人都有身份证号。在多线程大合唱时,你要知道"刚才这句歌词是哪个人唱的",就需要看 ID。
应用场景 :
调试日志 :在打印 Log 时加上 ID,一眼看出日志是哪个线程打印的,排查死锁或乱序执行的神器。
逻辑判断 :比如主线程 ID 是 1001,如果检测到当前 ID 不是 1001,就不执行某些 UI 更新操作。
数据关联 :用 ID 作为 std::map 的 Key,为不同线程存储私有数据。
#include <iostream>
#include <thread>void worker() {
// 打印:我是子线程,我的工号是 xxx
std::cout << "子线程 ID: " << std::this_thread::get_id() << std::endl;
}int main() {
std::thread t(worker);
// 打印:我是主线程,我的工号是 yyy
std::cout << "主线程 ID: " << std::this_thread::get_id() << std::endl;
t.join();
return 0;
}在 C++ 标准线程库中,有两个长得一模一样的 get_id(),但它们的所属对象 和使用场景 完全不同。
一个是问 "我自己是谁?" (std::this_thread)
一个是问 "那个线程对象是谁?" (std::thread 成员函数)
2. yield():让出自己的时间片
cppvoid yield() noexcept;作用 :放弃当前的 CPU 时间片,把 CPU 让给其他线程使用。
底层机制:调用该函数的线程会告诉操作系统调度器:"我现在的任务不急,或者我在等某个锁,你可以先把 CPU 分给其他正在排队(Ready 状态)的线程用一下。"
关键点:
线程不会进入睡眠(阻塞)状态。它只是从"运行中(Running)"变回"排队中(Ready)"。
如果此时没有其他线程在排队,操作系统可能立刻又让它继续运行。
生活比喻 :"你先走, 我重新排队"(只要轮到我,我马上回来)"
应用场景 :
- 自旋锁(Spinlock) :当线程在 while 循环里等待某个条件变真时,如果一直空转(死循环)会把 CPU 跑到 100%。加上 yield() 可以降低 CPU 占用,避免"占着茅坑不拉屎",饿死其他低优先级线程。
cpp// 典型的忙等待(Busy Wait)优化 while (!data_is_ready) { // 不要死循环空跑,太费电且独占CPU std::this_thread::yield(); // 稍微让一下,等下一轮调度再来看 ready 变没变 }
3. sleep_for():我要睡一会儿(相对时间)
cpptemplate< class Rep, class Period > void sleep_for( const std::chrono::duration<Rep, Period>& rel_time );作用 :让当前线程暂停执行指定的一段时间。
参数:std::chrono::duration(时间段,例如:2秒、500毫秒)。
底层机制 :线程进入阻塞(Blocked)状态,完全释放 CPU 资源。操作系统承诺在时间没到之前,绝对不会唤醒你(除非被信号打断)。
生活比喻:设一个倒计时闹钟,"我要午休 10 分钟"。
应用场景:
模拟耗时:测试时模拟网络延迟。
降低频率:比如一个后台线程每隔 1 秒检查一次服务器状态,不需要一直跑。
cpp#include <thread> #include <chrono> using namespace std::chrono_literals; // 允许使用 2s, 100ms 这种写法 void worker() { // 线程暂停 2 秒,期间 CPU 占用率为 0 std::this_thread::sleep_for(2s); // 或者 std::this_thread::sleep_for(std::chrono::milliseconds(500)); }
4. sleep_until():我要睡到几点(绝对时间)
cpptemplate< class Clock, class Duration > void sleep_until( const std::chrono::time_point<Clock, Duration>& abs_time );作用 :让当前线程暂停执行,直到某个具体的时间点。
参数:std::chrono::time_point(时间点,例如:明天早上 8:00)。
区别:sleep_for 是"睡多久",sleep_until 是"睡到什么时候"。
生活比喻:设一个定点闹钟,"把闹钟定在 12:00:00 响"。
应用场景:
消除时间漂移(Drift):如果你写一个节拍器,要求严格每隔 1 秒响一次。
用 sleep_for(1s):因为代码执行本身要几毫秒,循环几次后,时间会慢慢变慢(1.01s, 2.02s, 3.03s...)。
用 sleep_until(next_time):每次循环都计算下一个整点时间,保证每次都在整点醒来,误差不累积。
cpp#include <thread> #include <chrono> void worker() { auto now = std::chrono::steady_clock::now(); // 计算出未来 5 秒后的那个时间点 auto wake_up_time = now + std::chrono::seconds(5); // 睡到那个时间点再醒来,不管中间发生了什么 std::this_thread::sleep_until(wake_up_time); }
总结对比
|-----------------|---------------------|--------------|---------------|--------------|
| 函数 | 状态变化 | CPU 占用 | 核心含义 | 典型场景 |
| get_id | 无 | 正常 | 我是谁? | 调试日志、数据映射 |
| yield | Running -> Ready | 低 (但不为0) | 我不急,你们先用 | 自旋锁、避免死循环空转 |
| sleep_for | Running -> Blocked | 0 (完全释放) | 我要休息 X 分钟 | 定时器、轮询间隔 |
| sleep_until | Running -> Blocked | 0 (完全释放) | 我要睡到 X 点钟 | 精确周期的任务、定时启动 |关键提示 :
不要用 yield 来代替 sleep 做延时。yield 只是重新排队,如果当时只有你一个人在排队,操作系统会立刻把你叫回来继续干活,起不到"等待"的效果,CPU 依然会很高。想要等待,请用 sleep。
二、 互斥与锁 <mutex> & <shared_mutex>
1. 互斥量 (Mutex Types)
std::mutex(互斥量)是 C++ 多线程编程中保护共享数据、避免**数据竞争(Data Race)**的最基本工具。它的核心思想是:"同一时刻,只允许一个线程访问临界区"。
加锁会导致效率降低, 一定要把控好加锁的力度
这些是底层的锁对象,不建议直接调用其 member functions,建议配合 RAII 包装器使用。
|--------------------------------|---------------------------|-----------------------------------------------------------------------------------|
| 类型 | 特性 | 成员函数 |
| std::mutex | 最基本、最高效、不可递归 | lock(), unlock(), try_lock() |
| std::recursive_mutex | 同一线程可多次 lock,需相同次数 unlock | 同上 |
| std::timed_mutex | 支持超时等待 | 同上 + try_lock_for(), try_lock_until() |
| std::recursive_timed_mutex | 可递归 + 超时 | 同上 |
| std::shared_mutex (C++17) | 读写锁 (多读单写) | lock(), unlock() (写锁)<br>lock_shared(), unlock_shared() (读锁)<br>以及对应的 try_... |
std::mutex (最基本互斥锁)
特点:独占、不可递归。最常用,性能最高。
核心签名 (Member Functions)
头文件:<mutex>
cppclass mutex { public: // 构造函数 constexpr mutex() noexcept; // 禁止拷贝和移动 mutex(const mutex&) = delete; // 1. 阻塞等待锁。如果锁被别人拿了,就一直等(死等)。 void lock(); // 2. 尝试拿锁。如果拿到了返回 true;如果锁被别人拿了,立刻返回 false,不等待。 bool try_lock(); // 3. 释放锁。 void unlock(); };实战实例
cpp#include <iostream> #include <thread> #include <mutex> std::mutex mtx; // 全局互斥量 int g_num = 0; void slow_increment(int id) { // 尝试拿锁,拿不到就去干别的 if (mtx.try_lock()) { ++g_num; std::cout << "线程 " << id << " 抢到了锁,g_num=" << g_num << "\n"; mtx.unlock(); // 记得手动解锁! } else { std::cout << "线程 " << id << " 没抢到锁,溜了\n"; } } // ⚠️ 最佳实践:实际开发中尽量不要手动调用 lock/unlock, // 而是用 std::lock_guard<std::mutex> guard(mtx);
std::recursive_mutex (递归互斥锁)
特点 :允许同一个线程多次获取同一把锁,而不会死锁。主要用于递归函数。
核心签名
头文件:<mutex>
接口与 std::mutex 完全一致,但在逻辑上允许重入。
cppvoid lock(); // 计数器 +1 bool try_lock(); // 计数器 +1 void unlock(); // 计数器 -1,当减到 0 时才真正释放给其他线程实战实例
cpp#include <iostream> #include <thread> #include <mutex> std::recursive_mutex r_mtx; void recursive_function(int n) { if (n <= 0) return; r_mtx.lock(); // 第 n 次加锁 std::cout << "Level " << n << " locked\n"; recursive_function(n - 1); // 递归调用,再次加锁 std::cout << "Level " << n << " unlocked\n"; r_mtx.unlock(); // 第 n 次解锁 } int main() { std::thread t(recursive_function, 3); t.join(); } // 如果这里用普通的 std::mutex,程序会直接卡死(死锁)。
std::timed_mutex (超时互斥锁)
特点:拿锁的时候可以设置"耐心值",等一会等不到就放弃。
核心签名
头文件:<mutex>
除了基础的 lock, unlock, try_lock 外,增加了:
cpp// 1. 尝试拿锁,最多等 duration 时间 template <class Rep, class Period> bool try_lock_for(const std::chrono::duration<Rep, Period>& rel_time); // 2. 尝试拿锁,直到某个绝对时间点 template <class Clock, class Duration> bool try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time);实战实例
cpp#include <iostream> #include <thread> #include <mutex> #include <chrono> std::timed_mutex t_mtx; void worker(int id) { using namespace std::chrono_literals; // 尝试拿锁,最多等 200 毫秒 if (t_mtx.try_lock_for(200ms)) { std::cout << "线程 " << id << " 拿到锁了!\n"; std::this_thread::sleep_for(1s); // 模拟干活 1 秒 t_mtx.unlock(); } else { std::cout << "线程 " << id << " 等了 200ms 没等到,放弃。\n"; } } int main() { std::thread t1(worker, 1); // t1 先拿锁,占 1 秒 std::this_thread::sleep_for(10ms); // 确保 t1 先运行 std::thread t2(worker, 2); // t2 只能等 200ms,肯定等不到 t1.join(); t2.join(); }
std::recursive_timed_mutex (递归超时锁)
特点:就是 recursive 和 timed 的缝合怪。既可以递归重入,又可以设置超时。
核心签名
头文件:<mutex>
集合了上述所有的功能:
lock(), unlock(), try_lock()
try_lock_for(), try_lock_until()
(实例略,逻辑同上,只是允许在递归函数里用 try_lock_for)。
std::shared_mutex (读写锁 / 共享互斥锁)
特点 :C++17 引入。区分"读者"和"写者"。
读锁 (Shared):允许多个线程同时读。
写锁 (Exclusive):只允许一个线程写,写的时候谁都不能读。
核心签名
头文件:<shared_mutex>
cppclass shared_mutex { public: // === 写锁接口 (排他锁) === void lock(); // 拿写锁,堵塞所有读和写 bool try_lock(); void unlock(); // === 读锁接口 (共享锁) === void lock_shared(); // 拿读锁。如果有写锁在,就等;如果是读锁在,直接进。 bool try_lock_shared(); void unlock_shared(); };实战实例
通常配合 std::unique_lock (写) 和 std::shared_lock (读) 使用。
cpp#include <iostream> #include <thread> #include <shared_mutex> #include <mutex> #include <vector> std::shared_mutex rw_mtx; // 读写锁 int data_store = 0; // 写者:修改数据 (排他) void writer() { // 获取写锁 (unique_lock 会调用 mtx.lock()) std::unique_lock<std::shared_mutex> lock(rw_mtx); data_store++; std::cout << "Writer 修改数据为: " << data_store << "\n"; } // 读者:读取数据 (共享) void reader(int id) { // 获取读锁 (shared_lock 会调用 mtx.lock_shared()) std::shared_lock<std::shared_mutex> lock(rw_mtx); std::cout << "Reader " << id << " 读取数据: " << data_store << "\n"; // 多个 Reader 可以同时打印这句话,不用排队 } int main() { std::thread t1(reader, 1); std::thread t2(reader, 2); std::thread t3(writer); std::thread t4(reader, 3); t1.join(); t2.join(); t3.join(); t4.join(); }总结对照表
|--------------------------|-----|-----|-------|--------------------|
| 锁类型 | 递归? | 超时? | 读写分离? | 核心用途 |
| std::mutex | ❌ | ❌ | ❌ | 90% 的场景。保护普通数据。 |
| std::recursive_mutex | ✅ | ❌ | ❌ | 递归函数、复杂的嵌套调用。 |
| std::timed_mutex | ❌ | ✅ | ❌ | 避免长时间死等,防止 GUI 卡死。 |
| std::shared_mutex | ❌ | ❌ | ✅ | 读多写少(如配置中心、缓存)。 |
2. RAII 锁包装器 (Lock Wrappers)
这些是管理 Mutex 生命周期的类,构造时加锁,析构时解锁。
std::lock_guard<Mutex>
最轻量。
构造函数:lock_guard(m) (立即锁), lock_guard(m, std::adopt_lock) (接管已锁的互斥量)。
无其他成员函数(不能手动 unlock)。
std::unique_lock<Mutex>
最灵活(配合条件变量必须用它)。
构造函数:支持 std::defer_lock (先不锁), std::try_to_lock (尝试锁), std::adopt_lock。
成员函数:lock(), unlock(), try_lock(), release() (释放所有权), owns_lock() (检查是否持有锁)。
std::scoped_lock<M...> (C++17)
- 防死锁。可以同时接收多个互斥量:scoped_lock g(m1, m2, m3);。内部使用死锁避免算法。
std::shared_lock<SharedMutex> (C++14)
- 专门用于获取 shared_mutex 的读锁(共享锁)。用法同 unique_lock。
std::lock_guard (最轻量)
核心特点 :"死板"。一旦创建必须持有锁,直到析构才释放。不能手动解锁,没有额外内存开销。
核心签名
cpptemplate <class Mutex> class lock_guard { public: // 1. 基础构造:立即调用 m.lock() explicit lock_guard(mutex_type& m); // 2. 领养构造:假设 m 已经被当前线程锁住了,这里只负责接管析构解锁权 // (std::adopt_lock 是一个标记结构体) lock_guard(mutex_type& m, std::adopt_lock_t t); // 析构:调用 m.unlock() ~lock_guard(); // 禁用拷贝和移动 lock_guard(const lock_guard&) = delete; };实战实例
cpp#include <mutex> #include <iostream> std::mutex mtx; int g_i = 0; void safe_increment() { // 【常规用法】 // 构造时立刻锁住 mtx,退出函数作用域时自动解锁 std::lock_guard<std::mutex> lock(mtx); g_i++; // lock.unlock(); // ❌ 错误!lock_guard 没有 unlock 成员函数 } void adopt_example() { mtx.lock(); // 手动上锁 (比如是在别的函数里锁的) // 【领养用法】 // 告诉 lock_guard:"我已经锁好了,你别再锁了,但你要负责帮我解锁" std::lock_guard<std::mutex> lock(mtx, std::adopt_lock); // ... 操作 ... } // 自动 unlock
std::unique_lock (最灵活)
核心特点 :"自由"。持有锁的所有权,但可以随时抛弃、延迟获取或移交。比 lock_guard 稍微慢一点点(内部有个 bool 标志记录是否持有锁)。
核心签名
cpptemplate <class Mutex> class unique_lock { public: // 1. 基础构造:立即 lock() explicit unique_lock(mutex_type& m); // 2. 延迟锁定:构造时不锁,留着以后手动调 lock() unique_lock(mutex_type& m, std::defer_lock_t t) noexcept; // 3. 尝试锁定:构造时调 try_lock(),不阻塞 unique_lock(mutex_type& m, std::try_to_lock_t t); // 4. 领养锁定:同 lock_guard unique_lock(mutex_type& m, std::adopt_lock_t t); // 成员函数 void lock(); void unlock(); bool try_lock(); bool owns_lock() const; // 检查当前是否锁着 Mutex* release(); // 放弃管理,返回原始互斥量指针(且不解锁) };实战实例
cpp#include <mutex> #include <iostream> std::mutex mtx; void flexible_worker() { // 【延迟锁定策略】 // 此时 mtx 还没有被锁住! std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // ... 做一些不需要锁的准备工作 ... // 现在需要锁了,手动锁 lock.lock(); std::cout << "Critical section" << std::endl; // 可以手动解锁,去处理别的事 lock.unlock(); // ... 做一些耗时操作 ... // 再次加锁 lock.lock(); } // 析构时:如果当前拿着锁,就 unlock;如果没拿,啥也不干
std::scoped_lock (C++17 防死锁神器)
核心特点 :"通吃"。它是 lock_guard 的升级版,支持同时锁多个互斥量。它内部使用了死锁避免算法(类似于 std::lock()),保证以安全的顺序加锁。
核心签名
cpp// 变长模板参数,支持 1 个或多个互斥量 template <class... MutexTypes> class scoped_lock { public: // 构造函数:原子的锁定所有 m... explicit scoped_lock(MutexTypes&... m); ~scoped_lock(); // 同样禁用拷贝 };实战实例
经典场景:银行转账(需要同时锁住账户 A 和账户 B,否则容易死锁)。
cpp#include <mutex> struct Account { std::mutex m; int balance; }; void transfer(Account& from, Account& to, int amount) { // 【C++17 之前】如果你先锁 from 再锁 to,而在另一个线程先锁 to 再锁 from,就会死锁。 // 【C++17 之后】scoped_lock 会自动处理加锁顺序,保证不死锁 // 无论参数顺序是 (from, to) 还是 (to, from),它都很安全 std::scoped_lock lock(from.m, to.m); from.balance -= amount; to.balance += amount; } // 两个锁同时释放
std::shared_lock (C++14 读写锁专用)
核心特点 :"只读模式"。它是 shared_mutex 的最佳拍档。它的用法和 unique_lock 几乎一模一样,区别在于它调用的是底层的 lock_shared() 而不是 lock()。
核心签名 (Class Interface)
cpptemplate <class Mutex> class shared_lock { public: // 构造时调用 m.lock_shared() (获取读锁) explicit shared_lock(mutex_type& m); // 支持 defer_lock, try_to_lock 等策略 shared_lock(mutex_type& m, std::defer_lock_t t) noexcept; // 析构调用 m.unlock_shared() ~shared_lock(); void lock(); // 实际调用 lock_shared() void unlock(); // 实际调用 unlock_shared() };实战实例
cpp#include <shared_mutex> #include <mutex> #include <iostream> std::shared_mutex rw_mtx; // 读写锁 int data = 0; // 读者线程(可以多个同时进) void reader() { // 使用 shared_lock 获取"共享锁/读锁" std::shared_lock<std::shared_mutex> lock(rw_mtx); std::cout << "Reading: " << data << std::endl; } // 自动调用 unlock_shared // 写者线程(排他,只能进一个) void writer() { // 写者必须用 unique_lock (因为它需要独占锁 lock()) std::unique_lock<std::shared_mutex> lock(rw_mtx); data++; std::cout << "Writing: " << data << std::endl; } // 自动调用 unlock总结选择指南
绝大多数情况 (只锁一个,锁整个作用域):用 std::lock_guard (如果你用 C++17,推荐直接用 std::scoped_lock 代替 lock_guard,效果一样且更通用)。
需要手动解锁、延迟加锁、或配合条件变量 :必须用 std::unique_lock。
同时锁两个及以上互斥量 :必须用 std::scoped_lock。
读取 shared_mutex :必须用 std::shared_lock。
3. 通用锁定函数
这三个函数是 C++ <mutex> 头文件中用于复杂锁管理和初始化的核心工具函数。它们都是函数模板,支持可变参数。
std::lock(m1, m2, ...);
锁定多个互斥量,使用死锁避免算法(但不负责解锁,需配合 adopt_lock 使用)。
std::try_lock(m1, m2, ...);
尝试锁定多个,按顺序锁,失败则回滚。
std::call_once(flag, func, args...);
配合 std::once_flag 使用,确保函数在多线程环境下只执行一次(单例模式神器)。
std::lock (死锁避免算法)
功能 :同时锁定两个或更多的互斥量。它使用特殊的算法(通常是"尝试-回退"策略)来保证不会发生死锁 。
注意 :它只负责"上锁",不负责"解锁"。因此,锁完之后,必须立刻将锁的所有权交给 RAII 包装器(如 lock_guard 或 unique_lock)来管理析构解锁,这就需要用到 std::adopt_lock 策略。核心签名
cpp// 位于 <mutex> template< class L1, class L2, class... L3 > void lock( L1& m1, L2& m2, L3&... m3 );
参数:接收 2 个或多个互斥量(引用)。
异常:如果抛出异常,所有已获取的锁都会被自动释放。
实战实例
经典场景:银行转账(需要同时锁住两个账户,防止 A 转 B 的同时 B 转 A 造成死锁)。
cpp#include <mutex> #include <thread> #include <iostream> struct BankAccount { int balance; std::mutex m; }; void transfer(BankAccount& from, BankAccount& to, int amount) { // 1. 使用 std::lock 同时锁住两个锁,避免死锁 // 如果这里直接 from.m.lock(); to.m.lock(); 就可能死锁 std::lock(from.m, to.m); // 2. 关键步骤:领养锁 (Adopt Lock) // 告诉 lock_guard:"锁我已经上好了,你别再上锁了,但你要负责帮我解锁" std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock); std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock); // 3. 安全操作 from.balance -= amount; to.balance += amount; }
std::try_lock (顺序尝试与回滚)
功能:尝试依次锁定传入的互斥量 m1, m2, ...。
成功:如果所有锁都抢到了,返回 -1。
失败 :只要遇到任何一个锁抢失败(被占用了),它会立刻释放之前已经抢到的所有锁(回滚) ,并返回导致失败的那个锁的索引(从 0 开始)。
核心签名
cpp// 位于 <mutex> template< class L1, class L2, class... L3 > int try_lock( L1& m1, L2& m2, L3&... m3 );
- 返回值 :int。-1 表示成功,0 表示第 1 个失败,1 表示第 2 个失败,以此类推。
实战实例
场景:我想吃顿饭,需要同时拿到"碗"和"筷子"。如果拿到了碗但没拿到筷子,我就把碗放回去,别占着资源不干活。
cpp#include <mutex> #include <iostream> std::mutex bowl_mtx; std::mutex chopstick_mtx; void eat_dinner() { // 尝试同时拿碗和筷子 int result = std::try_lock(bowl_mtx, chopstick_mtx); if (result == -1) { // === 成功拿到所有锁 === std::cout << "拿到了碗和筷子,开吃!\n"; // 同样需要领养,防止忘记解锁 std::lock_guard<std::mutex> lg1(bowl_mtx, std::adopt_lock); std::lock_guard<std::mutex> lg2(chopstick_mtx, std::adopt_lock); } else { // === 失败 === // 此时 std::try_lock 已经自动帮你回滚了解锁,你不需要手动 unlock std::cout << "获取资源失败,失败的锁索引是: " << result << "\n"; } }
std::call_once (线程安全的单例/初始化)
功能 :保证某个函数在多线程环境下只被执行一次 。通常用于懒加载(Lazy Initialization)。
机制:它依赖一个 std::once_flag 标志位。
如果有多个线程同时调用 call_once,只有一个会成功执行函数。
其他线程会阻塞等待,直到执行者成功完成。
如果执行者抛出异常,标志位不会翻转,其他线程会再次尝试执行。
核心签名
cpp// 位于 <mutex> template< class Callable, class... Args > void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
flag:必须是 std::once_flag 类型的实例(且不能拷贝)。
f:要执行的函数。
args:传递给函数的参数。
实战实例
经典场景:单例模式 或者初始化全局日志系统。
cpp#include <mutex> #include <iostream> #include <thread> #include <vector> std::once_flag init_flag; // 必须是全局或静态的标志位 std::vector<int> shared_resource; void init_resource() { // 模拟耗时的初始化操作 std::cout << "正在初始化资源 (只应打印一次)...\n"; shared_resource = {1, 2, 3, 4, 5}; } void worker(int id) { // 无论多少线程运行到这,init_resource 都只会被执行一次 std::call_once(init_flag, init_resource); // 初始化完成后,大家都可以安全使用资源了 std::cout << "线程 " << id << " 正在使用资源大小: " << shared_resource.size() << "\n"; } int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); std::thread t3(worker, 3); t1.join(); t2.join(); t3.join(); }总结对照
|--------------------|---------------------|------------|-----------------------------|
| 函数 | 核心作用 | 返回值 | 关键用法 |
| std::lock | 死锁避免。阻塞直到拿到所有锁。 | void | 必须配合 adopt_lock 使用。 |
| std::try_lock | 全拿或全不拿。非阻塞尝试。 | int (-1成功) | 失败会自动回滚解锁;成功需配合 adopt_lock。 |
| std::call_once | 一次性初始化。 | void | 必须配合 std::once_flag。 |
三、 条件变量 <condition_variable>
用于线程间的等待和通知机制。
1. std::condition_variable
必须配合 std::unique_lock<std::mutex> 使用。
等待函数 (Wait)
wait(lock): 释放锁并挂起线程。
wait(lock, predicate): 推荐。等价于 while(!pred()) wait(lock);。防止虚假唤醒。
wait_for(lock, duration, [pred]): 等待一段时间。
wait_until(lock, time_point, [pred]): 等待直到某时刻。
通知函数 (Notify)
notify_one(): 唤醒一个正在等待的线程(随机选择)。
notify_all(): 唤醒所有正在等待的线程(惊群效应)。
2. std::condition_variable_any
更通用的版本,可以配合任何符合 Lockable 协议的锁(如自旋锁、读写锁),但效率略低于前者。
这是一个关于 C++ 线程同步中最核心机制------条件变量的详细讲解。条件变量主要用于解决"线程间的等待与通知"问题(例如:生产者-消费者模型)。
下面我将分两部分,详细讲解 std::condition_variable 和 std::condition_variable_any 的签名与实例。
|------------|-----------------------------------|-------------------------------------------------------------------------------|
| 特性 | std::condition_variable | std::condition_variable_any |
| 支持的锁类型 | 仅限 std::unique_lock<std::mutex> | 任意符合 BasicLockable 要求的锁 (如 std::shared_lock, std::recursive_mutex, 自定义锁等) |
| 性能/开销 | 较高(通常由操作系统原生支持,对象体积小) | 稍低(实现更复杂,体积可能更大,某些实现可能涉及堆内存分配) |
| 适用场景 | 90% 的常规多线程开发 | 特殊场景,特别是涉及读写锁时 |
std::condition_variable
它的主要作用是让一个线程等待(阻塞),直到另一个线程通知它某个条件已经满足。它是实现多线程之间协作(尤其是"生产者-消费者"模型)的核心工具。
这是最常用、性能最好的版本,但它强制要求必须配合 std::unique_lock<std::mutex> 使用。
核心函数签名
cpp// 头文件: <condition_variable> class condition_variable { public: // === 等待函数 === // 1. 死等 (直到被 notify) void wait(std::unique_lock<std::mutex>& lock); // 2. [推荐] 条件等待 (防止虚假唤醒) // Predicate 是一个返回 bool 的函数或 Lambda // 等价于: while (!pred()) wait(lock); template<class Predicate> void wait(std::unique_lock<std::mutex>& lock, Predicate pred); // 3. 超时等待 (相对时间) // 返回 cv_status::timeout 或 cv_status::no_timeout template<class Rep, class Period> std::cv_status wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& rel_time); // 4. 超时等待 (绝对时间) template<class Clock, class Duration> std::cv_status wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock, Duration>& abs_time); // === 通知函数 === // 唤醒一个等待线程 void notify_one() noexcept; // 唤醒所有等待线程 void notify_all() noexcept; };实战实例:生产者-消费者模型
这是条件变量最经典的使用场景。
生产者:生产数据 -> 加锁 -> 放入队列 -> 解锁 -> notify_one 通知消费者。
消费者:加锁 -> wait (等待队列不为空) -> 取出数据 -> 解锁 -> 处理数据。
cpp#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> std::mutex mtx; std::condition_variable cv; std::queue<int> data_queue; bool finished = false; // 结束标志 // 消费者线程 void consumer() { while (true) { std::unique_lock<std::mutex> lk(mtx); // 【关键点】 wait(锁, 谓词) // 含义:如果队列为空且任务没结束,我就释放锁并挂起等待。 // 一旦被唤醒,先拿锁,再检查条件 (pred)。如果不满足,继续睡;满足则往下走。 cv.wait(lk, []{ return !data_queue.empty() || finished; }); // 此时已持有锁,且条件满足 if (data_queue.empty() && finished) { break; // 队列空了且任务结束,退出 } int data = data_queue.front(); data_queue.pop(); // 提早解锁,处理数据不需要占着锁 lk.unlock(); std::cout << "消费: " << data << std::endl; } } // 生产者线程 void producer() { for (int i = 0; i < 5; ++i) { { std::lock_guard<std::mutex> lk(mtx); data_queue.push(i); std::cout << "生产: " << i << std::endl; } // 离开作用域自动解锁 // 通知一个正在 wait 的消费者 // 注意:notify 不需要持有锁,放在解锁之后效率更高(避免刚唤醒消费者就发现锁还没释放) cv.notify_one(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } // 生产结束 { std::lock_guard<std::mutex> lk(mtx); finished = true; } cv.notify_all(); // 通知所有消费者该下班了 } int main() { std::thread t1(consumer); std::thread t2(producer); t1.join(); t2.join(); }
std::condition_variable_any
这个类更加灵活。它不要求 锁必须是 std::unique_lock<std::mutex>。
它可以配合任何拥有 lock() 和 unlock() 方法的对象,比如:
std::shared_lock<std::shared_mutex> (读写锁)
std::unique_lock<std::recursive_mutex> (递归锁)
甚至是自定义的自旋锁。
核心签名
cpp// 头文件: <condition_variable> class condition_variable_any { public: // 注意:这里的 Lock 是模板参数,可以是任何锁类型 template<class Lock> void wait(Lock& lock); template<class Lock, class Predicate> void wait(Lock& lock, Predicate pred); // wait_for, wait_until, notify_one, notify_all 同上 };实战实例:配合读写锁 (shared_mutex)
假设我们有一个读者-写者场景,读者等待某个状态变更。因为使用了读写锁,普通的 condition_variable 无法配合 std::shared_lock 使用,这时必须用 condition_variable_any。
cpp#include <iostream> #include <thread> #include <shared_mutex> // C++17 #include <condition_variable> std::shared_mutex rw_mtx; // 读写锁 std::condition_variable_any cv_any; // 通用条件变量 bool ready = false; void reader(int id) { // 使用共享锁 (读锁) std::shared_lock<std::shared_mutex> lock(rw_mtx); // wait 接受 shared_lock // 当 wait 挂起时,它会释放这个读锁;被唤醒时,会重新获取读锁 cv_any.wait(lock, []{ return ready; }); std::cout << "读者 " << id << " 看到 ready 变为 true" << std::endl; } void writer() { std::this_thread::sleep_for(std::chrono::seconds(1)); { // 获取写锁 std::unique_lock<std::shared_mutex> lock(rw_mtx); ready = true; std::cout << "写者将 ready 设为 true" << std::endl; } // 通知所有读者 cv_any.notify_all(); } int main() { std::thread r1(reader, 1); std::thread r2(reader, 2); std::thread w(writer); r1.join(); r2.join(); w.join(); }
总结对比
|----------|------------------------------------|-----------------------------|
| 特性 | std::condition_variable | std::condition_variable_any |
| 配合的锁 | 只能是 std::unique_lock<std::mutex> | 任何锁 (如 shared_lock, 自定义锁) |
| 性能 | 高 (针对系统底层 API 优化) | 略低 (因为通用性增加了内部开销) |
| 内存占用 | 小 | 稍大 |
| 适用场景 | 90% 的常规场景 | 特殊场景 (如读写锁、递归锁) |一句话建议 :除非你必须要用读写锁或其他特殊锁来 wait,否则永远优先使用 std::condition_variable。
四、 未来与承诺 <future>
处理异步任务返回值的机制。
<future> 头文件主要解决了一个核心痛点:如何方便地从另一个线程那里拿到返回值?
普通的 std::thread 是"发射后不管(Fire and Forget)"的。虽然你可以传参数进去,但线程跑完后,它没法直接 return 一个值给你。你通常得用全局变量+锁+条件变量来倒腾数据,非常麻烦。
<future> 库就是为了解决这个问题而生的。它引入了两个核心概念:Future(未来) 和 Promise(承诺)。
披萨店模型
想象你去餐厅点餐:
Promise(承诺 / 厨师):
你点完餐,后厨的厨师接单了。厨师承诺(Promise)一定会把披萨做出来,或者如果烤糊了会告诉你(抛出异常)。
在代码中 :std::promise 是数据的生产者(设置值的一方)。
Future(未来 / 小票):
服务员给了你一张小票(Future)。这张小票现在不能吃,但它代表了**"在未来某个时间点,你可以凭此换取一个真披萨"**。
在代码中 :std::future 是数据的消费者(获取值的一方)。
流程:
你拿着小票(future)去逛街(主线程做其他事)。
厨师(子线程)在做披萨。
当你饿了,你拿着小票去柜台(调用 future.get())。
如果披萨做好了,你立马拿走;如果没做好,你就得在柜台死等(阻塞),直到做好。
<future> 的三大优势
直接获取返回值 :
再也不用定义全局变量,再也不用手动加锁去读那个变量了。future.get() 一步到位。
捕获异常 :
这是 std::thread 做不到的。
如果子线程里抛出了异常(比如 throw std::runtime_error("出错啦")),这个异常会通过 Future "传送" 到主线程。
当你调用 future.get() 时,主线程会抛出同样的异常,你可以在主线程里 try-catch 住它。
同步机制 :
future.wait() 和 future.get() 天然就是同步屏障。你不需要额外写 condition_variable 来等待结果就绪。
1. 提供者 (Providers - 生产结果)
std::promise<T>:
手动设置值。
set_value(val): 设置结果。
set_exception(e): 设置异常。
get_future(): 获取关联的 future 对象。
std::packaged_task<T(Args...)>:
包装一个函数,使其能异步执行。
operator(): 执行函数,并将结果存入内部状态。
get_future(): 获取关联的 future 对象。
std::async (函数):
高级接口。启动一个异步任务。
参数:std::launch::async (强制新线程) 或 std::launch::deferred (惰性执行)。
返回:std::future<T>。
2. 接收者 (Receivers - 获取结果)
std::future<T>:
独占。只能 get() 一次。
get(): 阻塞直到结果准备好,取出值(之后对象失效)。
wait(): 等待结果准备好。
wait_for(), wait_until(): 超时等待。
valid(): 检查目前是否关联有效状态。
share(): 转换为 shared_future。
std::shared_future<T>:
- 共享。可以拷贝,可以多次 get()(返回 const 引用)。多个线程可以等待同一个结果。
提供者 (Providers - 生产结果)
这些类负责制造数据,并将其放入共享状态中。
1. std::promise<T>
作用:最底层的"承诺"。在线程 A 中创建一个"空槽",承诺将来在线程 B 中填入数据。
核心签名:
cpptemplate< class T > class promise; // 1. 设置结果 (值会存入共享状态,通知 future) void set_value( const T& value ); void set_value( T&& value ); // 2. 设置异常 (如果任务失败,把异常填进去,future.get() 时会抛出这个异常) void set_exception( std::exception_ptr p ); // 3. 获取关联的 future (只能调用一次) std::future<T> get_future();实例:
cpp#include <iostream> #include <future> #include <thread> void worker(std::promise<int> p) { std::this_thread::sleep_for(std::chrono::seconds(1)); // 任务完成,填入结果 p.set_value(100); } int main() { std::promise<int> p; // 1. 先拿到"提货券" std::future<int> f = p.get_future(); // 2. 把 promise 移交给子线程 (promise 不能拷贝,只能 move) std::thread t(worker, std::move(p)); std::cout << "等待结果...\n"; // 3. 阻塞直到 set_value 被调用 std::cout << "结果: " << f.get() << "\n"; t.join(); }
2. std::packaged_task<T(Args...)>
作用 :包装一个可调用的函数,自动将函数的返回值连接到 future 上。比 promise 省事,不用手动 set_value。
核心签名:
cpp// 模板参数是函数签名,例如 int(int, int) template< class R, class... ArgTypes > class packaged_task<R(ArgTypes...)>; // 1. 获取 future std::future<R> get_future(); // 2. 像函数一样调用它。执行后,返回值会自动填入 future void operator()( ArgTypes... args );实例:
cpp#include <iostream> #include <future> #include <thread> // 一个普通函数 int add(int a, int b) { return a + b; } int main() { // 1. 包装任务 std::packaged_task<int(int, int)> task(add); // 2. 获取 future std::future<int> f = task.get_future(); // 3. 放到线程里跑 (task 也是只支持 move) std::thread t(std::move(task), 10, 20); // 4. 获取结果 std::cout << "10 + 20 = " << f.get() << "\n"; t.join(); }
3. std::async (函数)
作用:最高级的封装。自动创建 promise,自动打包 task,自动(可选)创建线程。
核心签名:
cpp// 这里的返回值是 future<函数返回值类型> template< class Function, class... Args > std::future<...> async( std::launch policy, Function&& f, Args&&... args );
policy 参数:
std::launch::async: 强制开启新线程。
std::launch::deferred: 惰性执行。只有当你调用 future.get() 时,才在当前线程执行任务(不创建新线程)。
实例:
cpp#include <iostream> #include <future> int heavy_calc() { return 42; } int main() { // 一行代码搞定异步任务 std::future<int> f = std::async(std::launch::async, heavy_calc); std::cout << "主线程继续做事...\n"; // 拿结果 std::cout << "结果: " << f.get() << "\n"; }
接收者 (Receivers - 获取结果)
这些类负责持有"提货券",等待数据就绪。
1. std::future<T>
作用 :独占的提货券。一份数据只能被一个对象取走。一旦 get(),对象就空了。
核心签名:
cpptemplate< class T > class future; // 1. 取值 (阻塞直到有值)。调用后 future 变为空(invalid) T get(); // 2. 等待 (不取值) void wait() const; // 3. 超时等待 // 返回 future_status::ready (好了), timeout (超时), deferred (还没开始) std::future_status wait_for( duration ); // 4. 检查是否有效 (get 之后会变成 false) bool valid() const noexcept; // 5. 转换为共享 future std::shared_future<T> share() noexcept;实例:
cppstd::future<int> f = std::async(std::launch::async, []{ return 10; }); if (f.valid()) { // 等待最多 1 秒 if (f.wait_for(std::chrono::seconds(1)) == std::future_status::ready) { std::cout << f.get() << "\n"; // 数据被移出 } } // 再次调用 valid() 将返回 false // 再次调用 get() 会抛出异常
2. std::shared_future<T>
作用 :共享的提货券。可以把结果广播给多个线程。
核心签名:
cpptemplate< class T > class shared_future; // 1. 取值 (返回 const 引用,不会让对象失效) // 可以被多次调用 const T& get() const; // 2. 支持拷贝构造 (std::future 不支持拷贝,只支持移动) shared_future( const shared_future& other );实例 :
场景:主线程下载一个文件,三个子线程都需要用到这个文件的内容。
cpp#include <iostream> #include <future> #include <thread> #include <vector> int download_data() { return 888; } void analyze(int id, std::shared_future<int> sf) { // 多个线程都可以调用 get() std::cout << "线程 " << id << " 获取到数据: " << sf.get() << "\n"; } int main() { std::promise<int> p; // 1. 先拿到普通 future,然后转为 shared // 或者直接用 p.get_future().share() std::shared_future<int> sf = p.get_future().share(); // 2. 启动多个线程,sf 可以按值拷贝传递 std::thread t1(analyze, 1, sf); std::thread t2(analyze, 2, sf); p.set_value(download_data()); // 数据广播给所有持有 sf 的线程 t1.join(); t2.join(); }
五、 原子操作 <atomic>
这是 C++ 并发编程中最硬核、最接近底层的部分。std::atomic 利用 CPU 的特殊指令(如 x86 的 LOCK 前缀或 ARM 的 LDREX/STREX)来保证操作的原子性,避免了互斥锁(Mutex)的昂贵开销。
1. std::atomic<T>
T 通常是整数、指针或 bool。
基础操作
store(val, order): 写入值。
load(order): 读取值。
exchange(val, order): 写入新值,返回旧值 (Atomic Swap)。
算术操作 (仅针对整数/指针)
fetch_add(val), fetch_sub(val): 加/减,返回修改前的值。
fetch_and, fetch_or, fetch_xor: 位运算。
operator++, operator+= 等重载运算符。
比较与交换 (CAS - Compare And Swap)
这是无锁编程的核心。
compare_exchange_weak(expected, desired): 可能虚假失败(即便值相等也返回 false),循环中使用效率高。
compare_exchange_strong(expected, desired): 保证值相等时一定成功。
2. 内存序 (std::memory_order)
所有原子函数都可选一个 memory_order 参数,控制指令重排。
memory_order_relaxed: 松散(无同步,只保证当前原子性)。
memory_order_acquire: 获取(读屏障)。
memory_order_release: 释放(写屏障)。
memory_order_acq_rel: 获取+释放。
memory_order_seq_cst: 顺序一致性(默认,最慢但最安全)。
std::atomic<T> 基础与算术操作
T 通常是 int, long, bool 或指针类型。
1. 基础操作 (load, store, exchange)
这三个函数是最基本的读写操作。
函数签名:
cpp// 读取 T load(std::memory_order order = std::memory_order_seq_cst) const noexcept; // 写入 void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept; // 交换 (Read-Modify-Write):写入新值,并返回旧值 T exchange(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;实例:
cpp#include <atomic> #include <iostream> #include <thread> std::atomic<int> g_data(0); std::atomic<bool> g_ready(false); void producer() { int data = 42; // 写入数据 g_data.store(data, std::memory_order_release); // 写入标志位 g_ready.store(true, std::memory_order_release); } void consumer() { // 循环等待直到 ready 为 true // load 读取值 while (!g_ready.load(std::memory_order_acquire)) { std::this_thread::yield(); } // 读取数据 std::cout << "Data: " << g_data.load(std::memory_order_acquire) << "\n"; } // exchange 实例:原子地"抢占"锁 std::atomic<bool> locked(false); void try_enter_critical_section() { // 尝试把 locked 设为 true。 // 如果返回 false,说明之前没锁,我抢到了。 // 如果返回 true,说明之前已经被锁了,我没抢到。 if (!locked.exchange(true)) { std::cout << "我拿到了锁\n"; // ... 干活 ... locked.store(false); // 释放锁 } }2. 算术操作 (fetch_...)
这些操作仅适用于整型 和指针 类型的原子变量。它们是 Read-Modify-Write (RMW) 操作。
核心签名:
cpp// 加法:将 val 加到当前值上,并返回【加之前】的旧值 T fetch_add(T val, std::memory_order order = std::memory_order_seq_cst) noexcept; // 减法:返回【减之前】的旧值 T fetch_sub(T val, std::memory_order order = std::memory_order_seq_cst) noexcept; // 位运算:与、或、异或 T fetch_and(T val, std::memory_order order = std::memory_order_seq_cst) noexcept; T fetch_or(T val, std::memory_order order = std::memory_order_seq_cst) noexcept; T fetch_xor(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;运算符重载:
operator++() 等价于 fetch_add(1) + 1 (返回新值)
operator++(int) 等价于 fetch_add(1) (返回旧值)
operator+= 等价于 fetch_add 后返回新值
实例:
cppstd::atomic<int> counter(0); void worker() { // 原子加 1,多线程并发也不会算错 // fetch_add 返回的是加之前的值 (例如 0),curr 变成 1 int old_val = counter.fetch_add(1); // operator++ 是语法糖 counter++; } // 实际应用:抢号系统 std::atomic<int> ticket_dispenser(0); int take_ticket() { // 每个人拿到的号码绝对唯一,且连续 return ticket_dispenser.fetch_add(1); }3. 比较与交换 (CAS - Compare And Swap)
这是无锁编程(Lock-free Programming)的灵魂。几乎所有无锁数据结构(无锁队列、栈)都是基于 CAS 实现的。
逻辑:
"我看一眼内存里的值是不是 expected(预期值)。
如果是,我就把它改成 desired(新值),并返回 true(成功)。
如果不是(说明被别人改过了),我就把内存里最新的值填到 expected 变量里,并返回 false(失败)。"
函数签名:
cpp// 弱 CAS:允许虚假失败 (Spurious Failure) // 即使值相等,偶尔也会返回 false (硬件原因)。适合写在 while 循环里。 bool compare_exchange_weak(T& expected, T desired, std::memory_order order = std::memory_order_seq_cst) noexcept; // 强 CAS:保证值相等时一定成功 // 内部包含了循环重试逻辑,开销略大一点点。 bool compare_exchange_strong(T& expected, T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;实例 :如何实现一个线程安全的"最大值更新"?
(普通 if (val > max) max = val 在多线程下是不安全的,CAS 可以解决)
cppstd::atomic<int> max_value(0); void update_max(int new_val) { // 1. 先读取当前的最大值作为"预期值" int prev_max = max_value.load(); // 2. 循环尝试更新 // 如果 prev_max < new_val,尝试把 max_value 更新为 new_val while (prev_max < new_val && !max_value.compare_exchange_weak(prev_max, new_val)) { // 如果进入循环体,说明 CAS 失败了。 // 原因 A:内存里的值被别人改了,不等于 prev_max。 // 原因 B:虚假失败 (weak 特有)。 // 【关键点】:compare_exchange_weak 失败时, // 会自动把内存里最新的值更新到 prev_max 变量里! // 所以下一次循环比较的就是最新的值。 } }
内存序 (std::memory_order)
这是 C++ 为了极致性能暴露给程序员的"指令重排控制权"。如果不指定,默认都是 memory_order_seq_cst(最安全但最慢)。
1. memory_order_relaxed (松散)
作用 :只保证这一步操作是原子的。不保证顺序。
场景:单纯的计数器,不涉及线程间数据同步。
cppstd::atomic<int> cnt = {0}; void f() { // 随便乱序执行都行,只要最终加了1即可 cnt.fetch_add(1, std::memory_order_relaxed); }2. memory_order_acquire (获取 - 读屏障)
作用 :用于 load (读) 操作。
含义 :"在这个操作之后的读写指令,绝对不允许被重排到这个操作之前。"
场景 :我看一眼红绿灯(acquire),看到绿灯了,然后我才能踩油门(后续操作)。
3. memory_order_release (释放 - 写屏障)
作用 :用于 store (写) 操作。
含义 :"在这个操作之前的读写指令,绝对不允许被重排到这个操作之后。"
场景 :我先把货装好(前置操作),然后关上卡车门(release)。不能先关门再装货。
4. memory_order_acq_rel (获取+释放)
作用 :用于 Read-Modify-Write (RMW) 操作(如 fetch_add, exchange)。
含义:同时具有上述两者的屏障效果。
5. memory_order_seq_cst (顺序一致性 - 默认)
作用:最强约束。全局所有线程看到的原子操作顺序都是一致的。
代价:禁止了 CPU 很多优化,性能最低。
实战:Acquire-Release 模型 (最经典用法)
这是替代 mutex 进行线程同步的标准写法。
线程 A 准备数据 -> 线程 A 发布数据 -> 线程 B 获取数据 -> 线程 B 读取数据
cpp#include <atomic> #include <thread> #include <string> #include <cassert> std::string data; std::atomic<bool> ready(false); void producer() { // 1. 准备数据 (非原子) data = "Hello World"; // 2. 发布 (Release) // 保证 Step 1 的写入绝对不会跑到 Step 2 后面去 ready.store(true, std::memory_order_release); } void consumer() { // 3. 获取 (Acquire) // 保证 Step 4 的读取绝对不会跑到 Step 3 前面去 while (!ready.load(std::memory_order_acquire)) { ; // 等待 } // 4. 读取数据 (非原子) // 此时 data 必定已经写入完成 assert(data == "Hello World"); } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); }总结
小白/业务开发:直接用默认的 std::memory_order_seq_cst(也就是不传参数)。虽然慢点,但绝对不出错。
高性能开发:如果是单纯计数器,用 relaxed。
库开发者/底层同步:使用 acquire 和 release 配合来实现"发布-订阅"模式。
CAS:用 compare_exchange_weak 配合 while 循环是标准写法。
六、 C++20 现代并发设施
这是 C++20 对并发编程的一次重大升级,旨在解决 C++11 遗留的痛点(如手动 join 容易崩、缺乏轻量级同步原语等)。
1. std::jthread (Header: <thread>)
自动汇合:析构时自动调用 request_stop() 和 join()。
协作式停止:
构造时自动传入 std::stop_token 给线程函数。
request_stop(): 发出停止请求。
stop_token: 线程函数内部调用 st.stop_requested() 检查是否该退出了。
2. 信号量 (Header: <semaphore>)
std::counting_semaphore<Max>: 计数信号量。
acquire(): 计数减 1(若为 0 则阻塞)。
release(n): 计数加 n(唤醒等待者)。
try_acquire(): 尝试减 1。
std::binary_semaphore: 二值信号量(特化版,Max=1),类似互斥锁,但更轻量。
3. 门闩 (Header: <latch>)
一次性同步点,计数器减到 0 后门打开,不可重置。
std::latch(count): 构造。
count_down(n): 计数减 n。
wait(): 等待直到计数为 0。
arrive_and_wait(): 减 1 并等待。
4. 屏障 (Header: <barrier>)
可循环使用的同步点。一组线程全部到达后,执行回调,然后进入下一轮。
std::barrier(count, completion_func): 构造。
arrive_and_wait(): 到达屏障并等待其他人。
arrive_and_drop(): 我退出了,以后别等我了(计数减 1)。
std::jthread (自动汇合与协作停止)
它是 std::thread 的现代化封装。两个核心改进:
RAII:析构时自动请求停止并 join(再也不用担心忘记 join 导致 std::terminate 崩溃)。
内置停止机制:不需要自己定义 atomic<bool> 标志位来通知线程退出了。
cpp// 头文件: <thread> class jthread { public: // 构造函数:支持自动传递 std::stop_token 作为函数的第一个参数 template<class Fn, class... Args> explicit jthread(Fn&& fn, Args&&... args); // 发出停止请求 (把内部的 stop_source 设为 stop 状态) bool request_stop() noexcept; // 手动 join (通常不需要调用,析构会自动调) void join(); // 获取停止令牌 (用于传给其他函数) std::stop_token get_stop_token() const noexcept; };场景:一个后台线程每秒打印一次日志,主线程运行 3 秒后让它停下。
cpp#include <iostream> #include <thread> #include <chrono> using namespace std::chrono_literals; // 注意:这里参数接收 stop_token,是 C++20 的魔法 // jthread 会自动把它传进来 void worker(std::stop_token st) { // 循环检查是否收到了停止请求 while (!st.stop_requested()) { std::cout << "Worker is running...\n"; std::this_thread::sleep_for(1s); } std::cout << "Worker received stop request, exiting.\n"; } int main() { std::cout << "Main starts\n"; { // 创建 jthread std::jthread t(worker); // 主线程睡 3 秒 std::this_thread::sleep_for(3.5s); // 离开作用域时: // 1. t.request_stop() 被自动调用 -> st.stop_requested() 变为 true // 2. t.join() 被自动调用 -> 等待 worker 结束 } std::cout << "Main ends\n"; }
信号量 (std::semaphore)
信号量本质上是一个计数器,用来控制同时访问某个资源的线程数量。
Counting Semaphore: 停车场模型(有 N 个车位)。
Binary Semaphore: 互斥锁模型(只有 1 个车位)。
cpp// 头文件: <semaphore> // 模板参数 LeastMaxValue 是计数的最大值 template<std::ptrdiff_t LeastMaxValue = std::numeric_limits<std::ptrdiff_t>::max()> class counting_semaphore { public: // 构造函数,设置初始计数 explicit counting_semaphore(std::ptrdiff_t desired); // P操作:计数减 1。如果计数为 0,则阻塞等待 void acquire(); // V操作:计数加 update。唤醒正在等待的线程 void release(std::ptrdiff_t update = 1); // 非阻塞尝试减 1 bool try_acquire(); // 超时尝试 bool try_acquire_for(Duration...); }; // 二值信号量 (特化版) using binary_semaphore = std::counting_semaphore<1>;场景:限制并发数量。假设有一个下载任务,只允许最多 3 个线程同时下载。
cpp#include <iostream> #include <thread> #include <semaphore> #include <vector> #include <chrono> // 定义一个最多允许 3 个并发的信号量 std::counting_semaphore<3> download_sem(3); void downloader(int id) { std::cout << "Thread " << id << " waiting in queue...\n"; // P操作:申请资源。如果此时已有 3 人在下载,这里会阻塞 download_sem.acquire(); std::cout << "Thread " << id << " start downloading! [Slot Acquired]\n"; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟下载 std::cout << "Thread " << id << " finished. [Slot Released]\n"; // V操作:释放资源。让排队的人进来 download_sem.release(); } int main() { std::vector<std::thread> threads; // 启动 10 个线程,但你会发现同时只有 3 个在跑 for (int i = 0; i < 10; ++i) { threads.emplace_back(downloader, i); } for (auto& t : threads) t.join(); }
门闩 (std::latch)
一次性的倒计时器。倒计时归零后,门闩永久打开。通常用于初始化同步。
cpp// 头文件: <latch> class latch { public: // 构造函数,设置倒计时初始值 explicit latch(std::ptrdiff_t expected); // 计数减 n,如果不传 n 默认减 1 void count_down(std::ptrdiff_t n = 1); // 阻塞等待,直到计数变为 0 void wait() const; // 组合拳:先减 n,然后立刻等待直到 0 (常用) void arrive_and_wait(std::ptrdiff_t n = 1); // 检查是否已归零 bool try_wait() const noexcept; };场景:老板(主线程)等待 5 个员工(子线程)全部加载完数据,才能宣布项目开始。
cpp#include <iostream> #include <thread> #include <latch> #include <vector> // 倒计时门闩,初始值为 5 std::latch worker_done(5); void worker(int id) { // 模拟初始化工作 std::cout << "Worker " << id << " loading data...\n"; std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); std::cout << "Worker " << id << " done.\n"; // 计数减 1。注意:这里不需要阻塞,减完就没事了 worker_done.count_down(); } int main() { std::vector<std::thread> threads; for (int i = 1; i <= 5; ++i) { threads.emplace_back(worker, i); } std::cout << "Main thread waiting for workers...\n"; // 主线程阻塞在这里,直到 count_down 被调用 5 次 worker_done.wait(); std::cout << "All workers ready! Project Start!\n"; for (auto& t : threads) t.join(); }
屏障 (std::barrier)
可循环使用的同步点。所有线程到达屏障后,执行一个回调(可选),然后屏障重置,进入下一轮。
cpp// 头文件: <barrier> template<class CompletionFunction = /*...*/ > class barrier { public: // 构造函数:预期到达数量 + 到齐后的回调函数 constexpr explicit barrier(std::ptrdiff_t expected, CompletionFunction completion = CompletionFunction()); // 到达屏障并等待其他人。等所有人齐了,进入下一轮。 void arrive_and_wait(); // 到达屏障,但之后我退出了,把预期数量减 1。 void arrive_and_drop(); };场景:3 个线程并行计算,分为 Phase 1 和 Phase 2。必须所有人都做完 P1,才能一起进 P2。
cpp#include <iostream> #include <thread> #include <barrier> #include <vector> // 定义一个屏障,需要 3 个线程到达 // 第二个参数是 Lambda,当 3 个人都到了之后,由其中一个线程执行这个打印 std::barrier sync_point(3, []() noexcept { std::cout << ">>> Phase Completed! Moving to next phase... <<<\n"; }); void worker(int id) { // --- 第一阶段 --- std::cout << "Thread " << id << " doing Phase 1...\n"; std::this_thread::sleep_for(std::chrono::milliseconds(500 * id)); // 等待其他人做完 Phase 1 std::cout << "Thread " << id << " waiting at barrier 1...\n"; sync_point.arrive_and_wait(); // --- 第二阶段 (只有所有人都过了上面那行代码,才会执行这里) --- std::cout << "Thread " << id << " doing Phase 2...\n"; std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待其他人做完 Phase 2 sync_point.arrive_and_wait(); std::cout << "Thread " << id << " finished.\n"; } int main() { std::vector<std::thread> threads; for (int i = 1; i <= 3; ++i) { threads.emplace_back(worker, i); } for (auto& t : threads) t.join(); }总结区别:Latch vs Barrier
Latch (门闩) :是一次性的。像赛跑的发令枪,响了之后(归零),大家就跑了,门闩就废了。主要用于初始化。
Barrier (屏障) :是循环的。像旅游团集合,早上的景点逛完(Phase 1),大家在大巴车前集合 (Barrier),人齐了发车去下午的景点(Phase 2)。主要用于分阶段并行计算。