C++ 锁

news/2025/9/23 16:30:33/文章来源:https://www.cnblogs.com/gange111/p/19107464

在多线程编程中,当多个线程同时访问共享资源时,可能会导致数据竞争(Data Race),产生不可预期的结果。锁提供了同步机制,确保在同一时间只有一个线程可以访问临界区。

锁的本质是通过互斥机制(Mutual Exclusion)确保:

  • 同一时间只有一个线程能进入访问共享资源的代码段(临界区);
  • 线程对共享资源的修改能被其他线程立即可见(避免 CPU 缓存导致的数据不一致)。

1、标准库锁类型

C++11 及后续标准在<mutex>头文件中提供了多种锁类型,满足不同场景需求。

1.1 std::mutex:基础互斥锁

std::mutex是最基础的互斥锁,提供独占所有权 —— 同一时间仅允许一个线程锁定,其他线程尝试锁定时会阻塞等待,直到锁被释放。

核心操作

  • lock():锁定互斥锁(若已被锁定,当前线程阻塞);
  • unlock():解锁互斥锁(必须由持有锁的线程调用,否则行为未定义);
  • try_lock():尝试锁定(成功返回true,失败返回false,不阻塞)。

基础用法(手动加锁 / 解锁)

#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx; // 全局互斥锁
int shared_value = 0;// 线程函数:对共享变量累加
void increment() {for (int i = 0; i < 10000; ++i) {mtx.lock();       // 手动加锁shared_value++;   // 临界区:安全修改共享资源mtx.unlock();     // 手动解锁(必须执行,否则死锁)}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value: " << shared_value << std::endl; // 预期20000return 0;
}

手动调用lock()unlock()存在风险 —— 若临界区抛出异常,unlock()可能无法执行,导致锁永远被持有(死锁)。因此,实际开发中严禁手动管理锁的生命周期,应使用 RAII 封装。

1.2 std::lock_guard:RAII 自动管理锁(推荐)

std::lock_guardstd::mutex的 RAII(资源获取即初始化)封装类。其构造函数自动调用lock(),析构函数自动调用unlock(),确保锁在作用域结束时必然释放(即使发生异常)。

特点

  • 不可复制、不可移动(避免锁所有权被意外转移);
  • 生命周期与作用域严格绑定,简单高效。

lock_guard避免手动解锁

void safe_increment() {for (int i = 0; i < 10000; ++i) {// 构造时自动加锁,析构时(离开for循环作用域)自动解锁std::lock_guard<std::mutex> lock(mtx); shared_value++; // 临界区安全执行}
}// 主函数同上,最终输出20000(无死锁风险)

优势:即使shared_value++抛出异常(实际不会),lock_guard的析构函数仍会被调用,确保锁释放。

1.3 std::unique_lock:灵活的 RAII 锁

std::unique_locklock_guard更灵活,支持延迟锁定、手动解锁、所有权转移等操作。其内部维护一个 “是否持有锁” 的状态,因此有额外的性能开销(约几个字节的内存和状态判断)。

核心功能

  • 延迟锁定:构造时不立即加锁(需配合std::defer_lock);
  • 手动控制:可通过lock()unlock()手动加解锁;
  • 所有权转移:可通过std::move()转移锁的所有权(适合传递锁);
  • 尝试锁定:支持try_lock()和超时锁定(try_lock_for()try_lock_until())。

延迟锁定与手动控制

void flexible_operation() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟锁定(不立即加锁)// 非临界区操作(无需锁)int temp = 100; lock.lock(); // 手动加锁(进入临界区)shared_value += temp; lock.unlock(); // 提前解锁(退出临界区,让其他线程尽早访问)// 其他非临界区操作
}

超时锁定(避免永久阻塞)

#include <chrono>bool try_operation_with_timeout() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock);// 尝试锁定,最多等待100毫秒if (lock.try_lock_for(std::chrono::milliseconds(100))) {shared_value++;return true; // 锁定成功并执行操作} else {return false; // 超时未获取锁,执行备选逻辑}
}

1.4 std::recursive_mutex:递归锁(同一线程可重入)

std::mutex不允许同一线程重复锁定(会导致死锁),而std::recursive_mutex允许同一线程多次调用lock(),但需对应相同次数的unlock()才能完全释放(内部维护 “递归计数”)。

递归函数中需要重复锁定同一资源(如二叉树遍历中,递归访问节点时需锁定全局计数器)。

递归函数中的锁重入

#include <recursive_mutex>std::recursive_mutex rmtx;
int count = 0;// 递归函数:每次递归都需要锁定
void recursive_count(int depth) {if (depth <= 0) return;std::lock_guard<std::recursive_mutex> lock(rmtx); // 同一线程可多次锁定count++; recursive_count(depth - 1); // 递归调用,再次锁定
}int main() {recursive_count(5);std::cout << "Count: " << count << std::endl; // 输出5(正确累加)return 0;
}

注意:递归锁易掩盖设计缺陷(如过度依赖共享资源),非必要不使用(优先考虑拆分临界区)

1.5 std::shared_mutex(C++17):读写分离锁

std::shared_mutex(或 C++14 的std::shared_timed_mutex)支持两种锁定模式,适用于 “读多写少” 场景:

  • 共享锁(读锁):多个线程可同时获取,用于读取共享资源(不修改);
  • 独占锁(写锁):仅一个线程可获取,用于修改共享资源(此时所有读锁和写锁均阻塞)。

通过分离读写操作,提高读操作的并发性能(读线程间无需互斥)。

读写分离控制

#include <shared_mutex>
#include <vector>std::shared_mutex smtx;
std::vector<int> data = {1, 2, 3}; // 共享数据// 读操作:获取共享锁(允许多线程同时读)
int read_data(int index) {std::shared_lock<std::shared_mutex> lock(smtx); // 共享锁return data[index];
}// 写操作:获取独占锁(仅允许单线程写)
void write_data(int index, int value) {std::unique_lock<std::shared_mutex> lock(smtx); // 独占锁data[index] = value;
}int main() {// 多个读线程可并发执行read_data()std::thread t1([]{ std::cout << read_data(0) << std::endl; });std::thread t2([]{ std::cout << read_data(1) << std::endl; });// 写线程执行时,读线程需等待std::thread t3([]{ write_data(0, 100); });t1.join(); t2.join(); t3.join();return 0;
}

2、死锁

2.1 死锁的产生条件

死锁是指两个或多个线程相互等待对方释放锁,导致永久阻塞的状态,需同时满足:

  • 互斥:锁被线程独占;
  • 持有并等待:线程持有一个锁,同时等待另一个锁;
  • 不可剥夺:线程持有的锁不能被强制释放;
  • 循环等待:线程间形成等待环(如线程 1 等锁 B,线程 2 等锁 A)。

2.2 错误示范

std::mutex mtx_a, mtx_b;// 线程1:先锁A,再等B
void thread1() {std::lock_guard<std::mutex> lock_a(mtx_a);// 模拟临界区操作(给线程2抢锁时间)std::this_thread::sleep_for(std::chrono::milliseconds(10));std::lock_guard<std::mutex> lock_b(mtx_b); // 等待线程2释放B → 死锁
}// 线程2:先锁B,再等A
void thread2() {std::lock_guard<std::mutex> lock_b(mtx_b);std::this_thread::sleep_for(std::chrono::milliseconds(10));std::lock_guard<std::mutex> lock_a(mtx_a); // 等待线程1释放A → 死锁
}int main() {std::thread t1(thread1);std::thread t2(thread2);t1.join(); // 程序永久阻塞(死锁)t2.join();return 0;
}

2.3 避免死锁的核心方法

2.3.1 按固定顺序加锁

所有线程按相同的全局顺序锁定多个锁(如始终先锁mtx_a,再锁mtx_b),打破 “循环等待” 条件。

// 线程1和线程2均按"先A后B"的顺序加锁
void thread1_fixed() {std::lock_guard<std::mutex> lock_a(mtx_a);std::lock_guard<std::mutex> lock_b(mtx_b); // 不会死锁
}void thread2_fixed() {std::lock_guard<std::mutex> lock_a(mtx_a); // 先等A,再锁Bstd::lock_guard<std::mutex> lock_b(mtx_b); 
}
2.3.2 用std::lock原子锁定多个锁

std::lock原子地同时锁定多个互斥量(内部通过复杂逻辑避免死锁),适合无法固定顺序的场景。

void safe_lock_two() {// 原子锁定mtx_a和mtx_b(无顺序依赖)std::lock(mtx_a, mtx_b); // 用adopt_lock标记锁已被锁定,避免重复加锁std::lock_guard<std::mutex> lock_a(mtx_a, std::adopt_lock);std::lock_guard<std::mutex> lock_b(mtx_b, std::adopt_lock);// 临界区操作
}
2.3.3 减少锁的持有时间

临界区仅包含必要操作,锁尽早释放(如用unique_lock手动解锁),降低死锁概率。

3、锁的选择与性能考量

锁类型 核心优势 性能开销 适用场景
std::mutex+lock_guard 简单安全,无额外开销 大多数场景,临界区简单且短
std::unique_lock 支持延迟锁定、超时、所有权转移 需要灵活控制锁生命周期的场景
std::recursive_mutex 允许同一线程重入 中高 递归函数需重复锁同一资源(谨慎使用)
std::shared_mutex 读写分离,提高读并发 读多写少,如缓存、配置数据访问

C++ 锁通过互斥机制解决多线程共享资源的竞态条件问题,其核心是确保临界区原子性。实际开发中:

  1. 优先使用 RAII 封装(lock_guard/unique_lock)避免手动管理锁的风险;
  2. 根据场景选择锁类型(如读多写少用shared_mutex);
  3. 通过固定加锁顺序、std::lock等方法严格避免死锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/913164.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

哈尔滨市哪里做淘宝网站app软件开发专业公司

gkz cloud sql许多Google AppEngine开发人员一直在等待全文搜索功能&#xff0c;特别是来自网络上最大的搜索引擎Google。 我很高兴看到Google团队正在努力&#xff0c;您可以在Google I / O 2011会议上查看&#xff1a;Bo Majewski和Ged Ellis进行的全文本搜索 。 据我所知&am…

网易NDH大数据平台使用经验

网易NDH大数据平台使用经验网易NDH大数据平台是基于Hadoop、HDFS、Hive、Spark、Impala、Yarn等开源组件进行二次开发的大数据套件,具有数据集成、开发运维、规范建模、数据治理、数据服务等功能的一站式数据开发治理…

专题类网站坪山区坪山街道六联社区

文章目录 前言1.第一次尝试1.1服务被调用方更新1.2压测第一次尝试1.3 问题分析1.4 同步的不是最新列表 2.第二次尝试2.1调用方过滤下线服务2.2压测第二次尝试2.3优化 写到最后 前言 在上文的基础上&#xff0c;通过压测的结果可以看出&#xff0c;使用DiscoveryManager下线服务…

免费入驻的网站设计平台网站开发静态和动态

[react] 在React中你有遇到过安全问题吗&#xff1f;怎么解决&#xff1f; dangerouslySetInnerHTML预防xss攻击 个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

公司网页制作网站怎么设置网站

目录 传送参数页面接受参数页面最后 uniapp全局事件&#xff0c;也就是说&#xff0c;不相邻的&#xff0c;不是父子组件&#xff0c;也可以传递参数。 一个组件&#xff0c;传递项目内所有文件其中一个里面内&#xff0c;可以接受到参数。 传送参数页面 <template><…

做网站用模板专题制作 wordpress

&#x1f345; 作者主页&#xff1a;Java李杨勇 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java李杨勇公号作者✌ 简历模板、学习资料、面试题库、技术互助【关注我&#xff0c;都给你】 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f…

厦门做商城网站网站宣传册

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

网站维护员工作内容代理网页免费

av_packet_unref 该接口使用了如下调用,该接口主要作用是清理AVPacket中的所有空间数据&#xff0c;清理完毕后进行初始化操作&#xff0c;并且将 data 与 size 置为0&#xff0c;方便下次调用。 void av_packet_unref(AVPacket *pkt) {av_packet_free_side_data(pkt);av_buf…

找什么公司做网站怎么自己弄一个公众号

选择合适的扫描仪是进行档案数字化的关键步骤。以下是一些选择合适扫描仪的要点&#xff1a; 1. 扫描速度&#xff1a;选择具有合适的扫描速度的扫描仪&#xff0c;以便能够快速处理大量的文件。 2. 扫描分辨率&#xff1a;扫描分辨率决定了扫描后图像的清晰度。对于大多数文档…

飞书对程序员下手了,0 代码生成各类系统!!(附保姆级项目实战教程)

大家好,我是R哥。 之前我分享了飞书多维表格的妙用:飞书对程序员下手了,0 代码生成各类系统!!包括以下两个重点: 1、飞书多维表格无需下载飞书也能直接使用了,打开 base.feishu.cn 就可以直接使用飞书多维表格。…

Adaptix C2:跨平台渗透测试与对抗仿真框架

Adaptix C2是一个可扩展的后渗透和对抗仿真框架,专为渗透测试人员设计。采用Golang服务端和C++ QT客户端架构,支持Linux、Windows和MacOS多平台操作,提供完整的加密通信、插件化监听器和代理管理功能。项目描述 Ada…

wordpress发布插件太原网站seo

指纹由于其终身不变性、唯一性和方便性&#xff0c;几乎已成为生物特征识别的代名 词。通常我们说的指纹就是人的手指末端正面皮肤上凸凹不平的纹线&#xff0c;纹线规律地排列 形成不同的纹型。而本节所讲的指纹是指网站CMS 指纹识别、计算机操作系统及W eb 容器的指纹识别等…

ncpa.cpl 意义 这个名称的

ncpa.cpl 意义 这个名称的ncpa.cpl 其实是 Windows 控制面板小程序 (Control Panel item) 的文件名。全称含义:nc → Network Connections(网络连接)pa → Panel Applet(控制面板小程序).cpl → Control Panel ex…

国标GB28181软件EasyGBS网页直播平台在邮政快递场景的落地与应用

国标GB28181软件EasyGBS网页直播平台在邮政快递场景的落地与应用随着电子商务的迅猛发展,邮政快递行业迎来了前所未有的发展机遇,但同时也面临着诸多挑战。如何在保障货物安全、提高运输效率的同时,实现全面的监控和…

做网站的软件下载做知乎网站要多少钱

文章目录 1. 数据的关联与合并1.1 join关联1.1.1 内关联1.1.2 左关联1.1.3 右关联 1.2 Union合并 2. 缓存和checkpoint 1. 数据的关联与合并 1.1 join关联 students表数据&#xff1a; 1.1.1 内关联 内关联只返回两个 DataFrame 中在连接键上匹配的行。 # join 关联 from…

创建了网站黄石网站建设黄石

文章目录 引言复习完全背包问题——买书个人实现 状态转换机——股票买卖V个人实现参考实现 新作两数相除个人实现 新作LRU缓存实现个人实现unordered_map相关priority_queue相关 参考实现自己复现 总结 引言 今天知道拼多多挂掉了&#xff0c;难受&#xff0c;那实习就是颗粒无…

长沙教育网站开发秦皇岛建网站

哈希表是种数据结构&#xff0c;它可以提供快速的插入操作和查找操作。第一次接触哈希表时&#xff0c;它的优点多得让人难以置信。不论哈希表中有多少数据&#xff0c;插入和删除(有时包括侧除)只需要接近常量的时间即0(1)的时间级。实际上&#xff0c;这只需要几条机器指令。…

做网站一定要买主机吗wordpress书库插件

jquery 实现双击编辑并保存Jesse2013-12-11 19:47:001153最近在做一个数据修改的例子&#xff0c;一个个点开修改很麻烦&#xff0c;于是就想到ecshop后台里的 只需单击就以编辑了&#xff0c;在网上查阅资料&#xff0c;就想到双击修改&#xff0c;失去鼠标焦点后post执行HTML…

北京wap网站建设wordpress重复评论

null可赋值任何变量,将变量置为空 DBNull只用于DataRow对象,表示数据库中的空值 String.Empty是0长度字串 Convert.IsDBNull判断是否为DBNull DBNull.Value与Null的区别 Null是.net中无效的对象引用。 DBNull是一个类。DBNull.Value是它唯一的实例。它指数据库中数据为空(&l…