当前位置: 首页 > news >正文

std::unique_lock 与 std::lock_guard

std::unique_lock 与 std::lock_guard 全面讲解

std::lock_guardstd::unique_lock都是 C++ 标准库提供的RAII 风格互斥锁管理工具,核心目的是避免手动调用lock()/unlock()导致的锁泄漏(如异常时未解锁),但二者在灵活性、性能、适用场景上有显著差异。

一、核心共性:RAII 管理锁

无论lock_guard还是unique_lock,都遵循 RAII 原则:

  • 构造时:关联互斥锁(可选择立即加锁/延迟加锁);
  • 析构时:自动解锁(无需手动调用unlock());
  • 异常安全:即使临界区抛出异常,析构函数仍会执行,保证锁一定会释放,避免死锁。
二、核心差异对比(表格更清晰)
特性std::lock_guardstd::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_locklock_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_variablewait()方法仅接受 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;});// 临界区...}
五、选型原则:什么时候用哪个?
  1. 优先用 std::lock_guard

    • 场景:简单的临界区,无需提前解锁、延迟加锁;
    • 理由:性能最优,代码更简洁,符合“极简主义”。
  2. 必须用 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的核心逻辑:

    1. 多个线程调用call_once时,先原子检查once_flag的状态;
    2. 若状态为“未执行”,则其中一个线程会获取内部锁,执行目标函数,并将状态置为“已执行”;
    3. 其他线程会阻塞等待,直到目标函数执行完成(或抛出异常);
    4. 若目标函数抛出异常,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_once

3. 进阶用法:处理函数抛出异常的场景

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:执行后无解锁开销,性能更优。

四、典型应用场景

  1. 线程安全单例模式:替代双重检查锁定(DCLP),避免内存序问题;
  2. 全局/静态资源初始化:如配置文件加载、数据库连接初始化、日志系统初始化;
  3. 一次性初始化逻辑:如注册回调函数、加载动态库、初始化硬件资源;
  4. 异常安全的一次性执行:函数可能抛异常时,无需手动重置标志位。

总结(核心要点回顾)

  1. 核心功能std::call_once结合std::once_flag保证多线程下函数仅执行一次,异常时可重试;
  2. 核心优势:比手动加锁更安全(避免DCLP坑)、更高效(执行后无锁开销)、异常友好;
  3. 关键规则
    • once_flag不可拷贝/移动,生命周期需覆盖所有调用线程;
    • 函数抛异常时,once_flag重置,允许后续线程重新调用;
    • 优先用于“一次性初始化”场景,而非通用互斥。
http://www.cnnetsun.cn/news/1388470.html

相关文章:

  • 别再只怪网络了!排查Moonlight/SteamLink串流失败的另一个关键:Windows会话状态
  • Windows任务栏分组管理终极指南:Taskbar Groups让桌面井井有条
  • Qwen2.5-VL-7B-Instruct与MySQL集成:构建智能问答知识库系统
  • Nanbeige 4.1-3B部署教程:OpenTelemetry集成实现像素终端全链路追踪
  • RexUniNLU实战:用零样本框架快速解析社交媒体热点话题
  • 通义千问3-4B优化升级:如何让本地知识库响应更快、更准确
  • 从配置文件,去理解OpenClaw的消息路由(第13讲,干货收藏)
  • 支持实时备份功能的云盘并不少见:2026年功能原理与产品深度盘点
  • [逆向] x64dbg消息断点实战:从游戏交互到API追踪
  • 从流量到留存的数字化飞跃:企业微信私域运营自动化全链路方案
  • 2026年VPS托管服务更新:功能升级与市场竞争新态势
  • Qt之QFile高效文件读写实践指南
  • SOONet模型Matlab联合仿真:视频分析与算法验证工作流
  • 新概念英语第一册055_The Sawyer family
  • 省下10小时读文献时间!百考通AI自动生成结构完整、引用规范的综述
  • AAAI 2026 | 解锁LLM真实想法!EAGLE从多层隐藏状态出发,让置信度评估告别“表面功夫”
  • OFA-VE与PS软件集成:创意设计中的视觉分析
  • CTC语音唤醒模型在Java企业级应用中的实践案例
  • Mac上Docker Desktop配置全攻略:从零开始搭建多容器开发环境(含常见错误修复)
  • Verilog 组合逻辑中不完整条件语句的锁存器陷阱与规避实战
  • 计算机毕业设计springboot旺苍县图书管理平台 基于SpringBoot的旺苍县智慧图书馆信息管理系统 SpringBoot框架下的旺苍县公共图书服务数字化平台
  • TMS320F280049C 实战解析:CLA 在电机控制中的高效应用
  • 【目标跟踪算法】Strong SORT与Deep SORT对比:优化点解析与性能提升实战
  • Three.js实战:基于Gemini3与MediaPipe打造手势驱动的3D粒子艺术画廊
  • SecGPT-14B可部署方案:离线环境运行,满足等保三级数据不出域要求
  • Qwen-Image镜像真实案例分享:RTX4090D上Qwen-VL准确识别复杂菜单图并翻译
  • Janus-Pro-7B部署案例:企业级多模态AI服务快速落地7860端口
  • 使用VSCode开发mPLUG应用:环境配置与调试技巧
  • RTX30系列显卡专属配置:MMRotate v0.3.4环境搭建与模型训练全攻略
  • 别再乱用装饰器了!NestJS项目中最值得收藏的5个装饰器模式