std::unique_lock 与 std::lock_guard
std::unique_lock 与 std::lock_guard 全面讲解
std::lock_guard和std::unique_lock都是 C++ 标准库提供的RAII 风格互斥锁管理工具,核心目的是避免手动调用lock()/unlock()导致的锁泄漏(如异常时未解锁),但二者在灵活性、性能、适用场景上有显著差异。
一、核心共性:RAII 管理锁
无论lock_guard还是unique_lock,都遵循 RAII 原则:
- 构造时:关联互斥锁(可选择立即加锁/延迟加锁);
- 析构时:自动解锁(无需手动调用
unlock()); - 异常安全:即使临界区抛出异常,析构函数仍会执行,保证锁一定会释放,避免死锁。
二、核心差异对比(表格更清晰)
| 特性 | std::lock_guard | std::unique_lock |
|---|---|---|
| 灵活性 | 极低(极简封装) | 极高(全功能锁管理) |
| 锁的生命周期 | 与对象生命周期绑定(构造加锁,析构解锁) | 可手动控制(提前解锁、重新加锁) |
| 构造参数 | 仅支持std::adopt_lock(接管已锁的锁) | 支持std::defer_lock(延迟加锁)、std::adopt_lock(接管已锁)、std::try_to_lock(尝试加锁) |
| 手动解锁/加锁 | 不支持(无unlock()/lock()方法) | 支持(unlock()/lock()/try_lock()) |
| 所有权转移 | 不支持(不可拷贝、不可移动) | 支持(可移动,不可拷贝) |
| 性能 | 轻量级(无额外状态,几乎零开销) | 略重(维护锁的状态标记,如“是否持有锁”) |
| 适用场景 | 简单的“加锁→执行→解锁”场景 | 复杂场景(延迟加锁、条件变量、超时加锁) |
三、std::lock_guard:极简的“一次性锁”
std::lock_guard是轻量级、不可扩展的锁管理器,设计目标是“最简单的锁管理”。
1. 基本用法(核心场景)
#include<mutex>#include<thread>#include<iostream>std::mutex m;intcount=0;voidincrement(){// 构造时立即加锁,析构时自动解锁std::lock_guard<std::mutex>lock(m);count++;// 临界区std::cout<<count<<std::endl;}intmain(){std::threadt1(increment);std::threadt2(increment);t1.join();t2.join();return0;}2. 关键特性解析
- 构造即加锁:默认构造时会调用
mutex::lock(),无需手动操作; - 仅支持 std::adopt_lock:若锁已被手动锁定,可通过
adopt_lock让lock_guard接管(避免重复加锁):m.lock();// 手动加锁std::lock_guard<std::mutex>lock(m,std::adopt_lock);// 接管已锁的锁 - 不可手动控制:没有
unlock()方法,锁必须等到lock_guard析构才释放; - 不可移动/拷贝:保证锁的所有权唯一,避免误操作。
3. 适用场景
- 临界区逻辑简单,无需提前解锁;
- 追求极致性能(无额外开销);
- 一次性加锁/解锁的场景(如简单的变量读写、函数内的临界区)。
四、std::unique_lock:灵活的“全能锁”
std::unique_lock是功能完整、可灵活控制的锁管理器,支持lock_guard的所有功能,还扩展了更多高级特性。
1. 核心用法1:延迟加锁(std::defer_lock)
最常用的高级特性,适合“多锁原子锁定”场景(如之前的 swap 示例):
// 延迟加锁:构造时不锁定,后续手动/批量锁定std::unique_lock<std::mutex>lock_a(m1,std::defer_lock);std::unique_lock<std::mutex>lock_b(m2,std::defer_lock);std::lock(lock_a,lock_b);// 原子性锁定多个锁,避免死锁// 临界区...2. 核心用法2:手动解锁(提升并发效率)
可提前解锁,让其他线程更早访问资源:
std::unique_lock<std::mutex>lock(m);// 操作1:必须加锁的核心逻辑count++;lock.unlock();// 提前解锁,释放资源// 操作2:无需加锁的耗时逻辑(如IO、计算)std::this_thread::sleep_for(std::chrono::seconds(1));lock.lock();// 重新加锁(如需继续操作临界区)count++;// 析构时再次解锁3. 核心用法3:尝试加锁(std::try_to_lock)
非阻塞式加锁,避免线程阻塞:
std::unique_lock<std::mutex>lock(m,std::try_to_lock);if(lock.owns_lock()){// 检查是否成功加锁// 拿到锁,执行临界区count++;}else{// 未拿到锁,执行备用逻辑std::cout<<"未拿到锁,跳过操作"<<std::endl;}4. 核心用法4:配合条件变量(唯一适配)
std::condition_variable的wait()方法仅接受 std::unique_lock(因为需要在等待时临时解锁,唤醒时重新加锁):
#include<condition_variable>std::condition_variable cv;std::mutex m;boolready=false;voidwait_for_ready(){std::unique_lock<std::mutex>lock(m);// 等待时自动解锁,唤醒时重新加锁cv.wait(lock,[]{returnready;});// 临界区...}五、选型原则:什么时候用哪个?
优先用 std::lock_guard:
- 场景:简单的临界区,无需提前解锁、延迟加锁;
- 理由:性能最优,代码更简洁,符合“极简主义”。
必须用 std::unique_lock:
- 场景1:需要延迟加锁(如多锁原子锁定);
- 场景2:需要手动解锁/重新加锁(如临界区拆分);
- 场景3:配合条件变量(
std::condition_variable); - 场景4:需要尝试加锁(非阻塞加锁)或超时加锁;
- 场景5:需要转移锁的所有权(如函数返回锁)。
std::call_once + std::once_flag 全解析(原理+用法+场景+坑点)
std::call_once(C++11 引入)结合std::once_flag是 C++ 标准库中实现多线程环境下函数仅执行一次的核心工具,比手动加锁(如std::mutex+ 标志位)更安全、高效,且能避免“双重检查锁定”的经典坑点。
一、核心概念与底层原理
1. 基本定义
- std::once_flag:一个不可拷贝、不可移动的标记类型,用于标记“某个函数是否已经执行过”,其内部存储了函数执行状态(未执行/已执行);
- std::call_once:模板函数,接收一个
std::once_flag引用和一个可调用对象(函数/lambda/函数对象),保证无论多少线程调用,该可调用对象仅被执行一次。
2. 底层原理
std::once_flag内部维护了一个原子状态变量(如std::atomic<bool>),标记函数是否已执行;std::call_once的核心逻辑:- 多个线程调用
call_once时,先原子检查once_flag的状态; - 若状态为“未执行”,则其中一个线程会获取内部锁,执行目标函数,并将状态置为“已执行”;
- 其他线程会阻塞等待,直到目标函数执行完成(或抛出异常);
- 若目标函数抛出异常,
once_flag状态会重置为“未执行”,允许后续线程重新调用(这是与手动加锁最大的区别之一)。
- 多个线程调用
相比手动实现(
mutex + bool flag),call_once的优势:特性 std::call_once + once_flag 手动 mutex + bool 标志位 异常处理 函数抛异常时重置状态,允许重试 需手动处理异常,否则标志位错误 性能 执行后无锁开销(原子检查) 每次调用都需加锁/解锁 安全性 避免双重检查锁定的坑 易因内存序问题导致竞态 可移植性 跨平台兼容 需手动适配不同编译器内存模型
二、核心用法(从基础到进阶)
1. 基础用法:单例模式(最典型场景)
call_once是实现线程安全单例的最优方式(替代双重检查锁定):
#include<iostream>#include<thread>#include<mutex>#include<once_flag>classSingleton{private:// 1. 定义once_flag(静态成员)staticstd::once_flag init_flag;// 2. 单例对象指针staticSingleton*instance;// 私有构造函数(禁止外部实例化)Singleton(){std::cout<<"Singleton 构造函数执行(仅执行一次)"<<std::endl;}// 3. 初始化函数(要保证仅执行一次的逻辑)staticvoidinit_instance(){instance=newSingleton();}public:// 4. 获取单例对象(多线程安全)staticSingleton*get_instance(){// 核心:无论多少线程调用,init_instance仅执行一次std::call_once(init_flag,init_instance);returninstance;}~Singleton(){std::cout<<"Singleton 析构"<<std::endl;}voidprint(){std::cout<<"Singleton 实例地址:"<<this<<std::endl;}};// 静态成员初始化std::once_flag Singleton::init_flag;Singleton*Singleton::instance=nullptr;// 线程函数:调用单例voidthread_func(intid){std::cout<<"线程"<<id<<" 获取单例"<<std::endl;Singleton*s=Singleton::get_instance();s->print();}intmain(){// 创建5个线程,同时调用get_instancestd::threadt1(thread_func,1);std::threadt2(thread_func,2);std::threadt3(thread_func,3);std::threadt4(thread_func,4);std::threadt5(thread_func,5);t1.join();t2.join();t3.join();t4.join();t5.join();return0;}输出结果(核心特征):
线程1 获取单例 线程2 获取单例 Singleton 构造函数执行(仅执行一次) Singleton 实例地址:0x7f8b9a4052a0 线程3 获取单例 Singleton 实例地址:0x7f8b9a4052a0 线程4 获取单例 Singleton 实例地址:0x7f8b9a4052a0 线程5 获取单例 Singleton 实例地址:0x7f8b9a4052a0- 无论多少线程调用,构造函数仅执行一次;
- 所有线程获取的是同一个实例地址。
2. 基础用法:普通函数仅执行一次
#include<iostream>#include<thread>#include<once_flag>std::once_flag flag;voiddo_something(){std::cout<<"do_something 执行(仅一次)"<<std::endl;}voidthread_task(intid){std::cout<<"线程"<<id<<" 调用call_once"<<std::endl;std::call_once(flag,do_something);}intmain(){std::threadt1(thread_task,1);std::threadt2(thread_task,2);std::threadt3(thread_task,3);t1.join();t2.join();t3.join();return0;}输出:
线程1 调用call_once 线程2 调用call_once do_something 执行(仅一次) 线程3 调用call_once3. 进阶用法:处理函数抛出异常的场景
若call_once调用的函数抛出异常,once_flag会重置,允许后续线程重新调用:
#include<iostream>#include<thread>#include<once_flag>#include<stdexcept>std::once_flag flag;intcall_count=0;// 记录函数调用次数voidrisky_function(){call_count++;std::cout<<"risky_function 第"<<call_count<<"次调用"<<std::endl;// 第一次调用抛异常,第二次调用正常执行if(call_count==1){throwstd::runtime_error("第一次调用失败");}}voidthread_task(intid){try{std::cout<<"线程"<<id<<" 尝试调用risky_function"<<std::endl;std::call_once(flag,risky_function);std::cout<<"线程"<<id<<" 调用成功"<<std::endl;}catch(conststd::exception&e){std::cout<<"线程"<<id<<" 捕获异常:"<<e.what()<<std::endl;}}intmain(){// 第一个线程调用,函数抛异常std::threadt1(thread_task,1);t1.join();// 第二个线程调用,函数正常执行std::threadt2(thread_task,2);t2.join();// 第三个线程调用,函数已执行过,不再调用std::threadt3(thread_task,3);t3.join();return0;}输出:
线程1 尝试调用risky_function risky_function 第1次调用 线程1 捕获异常:第一次调用失败 线程2 尝试调用risky_function risky_function 第2次调用 线程2 调用成功 线程3 尝试调用risky_function 线程3 调用成功- 关键点:第一次调用抛异常后,
once_flag未标记为“已执行”,第二个线程可重新调用; - 第二次调用成功后,
once_flag标记为“已执行”,第三个线程不再调用。
4. 进阶用法:传递参数给可调用对象
std::call_once支持向目标函数传递参数(完美转发):
#include<iostream>#include<thread>#include<once_flag>#include<string>std::once_flag flag;voidprint_info(conststd::string&msg,intnum){std::cout<<"msg: "<<msg<<", num: "<<num<<std::endl;}voidthread_task(){// 传递参数给print_infostd::call_once(flag,print_info,"仅执行一次的信息",100);}intmain(){std::threadt1(thread_task);std::threadt2(thread_task);t1.join();t2.join();return0;}输出:
msg: 仅执行一次的信息, num: 100三、关键注意事项(避坑指南)
1. std::once_flag 的特性
- 不可拷贝/不可移动:
once_flag没有拷贝构造函数和移动构造函数,只能通过引用传递给call_once;// 错误:拷贝once_flag(编译失败)std::once_flag flag1;std::once_flag flag2=flag1;// 正确:传递引用std::call_once(flag1,do_something); - 生命周期:
once_flag的生命周期必须覆盖所有调用call_once的线程,否则会导致未定义行为(如局部once_flag被销毁后,线程仍调用call_once);// 错误示例:局部once_flag,线程可能在flag销毁后调用call_oncevoidbad_func(){std::once_flag flag;std::threadt([&](){std::call_once(flag,do_something);});t.detach();// 线程分离,可能在flag销毁后执行}
2. 避免重复定义 once_flag
若once_flag是局部静态变量,需注意:C++11 后局部静态变量的初始化是线程安全的,但结合call_once时仍需保证once_flag唯一:
// 不推荐:局部静态once_flag(虽可行,但不如全局/类静态清晰)voidfunc(){staticstd::once_flag flag;std::call_once(flag,do_something);}3. 与 std::mutex 的选择
- 若仅需“函数执行一次”:优先用
call_once + once_flag,更简洁、安全; - 若需“保护一段临界区,允许多次执行但互斥”:用
std::mutex; - 若需“初始化后只读,且仅初始化一次”:
call_once是最优解。
4. 性能考量
call_once在函数执行前:需要原子检查 + 可能的锁竞争(一次);- 函数执行后:仅需原子检查(无锁开销),性能接近直接读取原子变量;
- 相比手动
mutex + bool:执行后无解锁开销,性能更优。
四、典型应用场景
- 线程安全单例模式:替代双重检查锁定(DCLP),避免内存序问题;
- 全局/静态资源初始化:如配置文件加载、数据库连接初始化、日志系统初始化;
- 一次性初始化逻辑:如注册回调函数、加载动态库、初始化硬件资源;
- 异常安全的一次性执行:函数可能抛异常时,无需手动重置标志位。
总结(核心要点回顾)
- 核心功能:
std::call_once结合std::once_flag保证多线程下函数仅执行一次,异常时可重试; - 核心优势:比手动加锁更安全(避免DCLP坑)、更高效(执行后无锁开销)、异常友好;
- 关键规则:
once_flag不可拷贝/移动,生命周期需覆盖所有调用线程;- 函数抛异常时,
once_flag重置,允许后续线程重新调用; - 优先用于“一次性初始化”场景,而非通用互斥。
