现代C++锁介绍
文章目录
- 场景描述
- 🐞 初始实现: 非线程安全版本
- 互斥锁: `std::mutex`
- 使用`mutex`保护共享资源
- 使用`std::lock_guard`简化锁的管理
- 优化读操作: `std::shared_mutex`
- 多个锁的管理: `std::scoped_lock`
- 使用`std::scoped_lock`避免死锁
- 其他高级锁
- ⏳ 带超时的锁: `std::timed_mutex`
- 使用场景
- 使用`std::timed_mutex`实现超时锁
- 支持多次锁定的锁: `std::recursive_mutex`
- 使用场景
- 带超时和可重入的锁: `std::recursive_timed_mutex`
- 📅 总结
自从 C++11 在语言层面引入多线程模型之后, C++ 标准库提供了一套完整的工具, 用于实现线程同步, 防止多个线程同时访问共享资源时出现的数据竞争. 这些工具包括了基本的互斥锁
std::mutex 以及基于
RAII 的锁管理器, 如
std::lock_guard, 极大简化了开发者处理并发问题的复杂度.
在后续的标准更新中, C++ 又陆续引入了更多高级的同步机制:
-
C++14: 引入
std::shared_mutex, 支持多读单写的场景优化读性能. -
C++17: 新增
std::shared_lock和std::scoped_lock, 分别用于管理读锁生命周期和防止多锁死锁问题.
为了让初学者更直观地理解这些锁的应用场景, 本文以银行账户管理系统为例, 逐步介绍现代 C++ 中不同锁的特点和使用方法.
场景描述
设计一个银行账户类, 可以执行以下操作:
- 存款(Deposit)
- 取款(Withdraw)
- 查询余额(Get Balance)
另外需要保证这个类是线程安全的, 即多个线程可以同时对账户进行存款和取款操作, 但是不会出现数据竞争.
🐞 初始实现: 非线程安全版本
首先实现一个最基础的版本, 这个版本没有考虑多线程的问题, 多次运行这个程序就会发现最后的输出结果不会一直是 0, 这是因为多个线程对共享资源balance的操作存在不确定性.
#include <iostream>
#include <thread>class BankAccount {public:// 存款void Deposit(int amount) { balance_ += amount; }// 取款void Withdraw(int amount) { balance_ -= amount; }// 查询余额int GetBalance() { return balance_; }private:int balance_ = 0;
};int main() {BankAccount account;std::thread t1([&account] { account.Deposit(100); });std::thread t2([&account] { account.Withdraw(100); });t1.join();t2.join();std::cout << "Balance: " << account.GetBalance() << std::endl;return 0;
}
互斥锁: std::mutex
使用mutex保护共享资源
多个线程可能同时对账户进行存款和取款操作, 为了防止数据竞争, 需要使用 std::mutex 确保同时只有一个线程可以修改账户余额.
为此我们引入一个类成员变量std::mutex, 并在每个操作前后加锁和解锁.
比如Deposit函数可以改为:
void Deposit(int amount) {mutex.lock();balance += amount;mutex.unlock();
}
使用std::lock_guard简化锁的管理
当前例子中被锁保护的代码比较简单, 不会发生异常. 但是实际工作中往往会遇到被锁保护的代码中可能会发生异常的情况, 或者有return语句, 这样会导致锁无法被释放, 从而引发死锁.
void foo(int amount) {mutex.lock();if (amount < 0) {mutex.unlock();return; // 早期返回}bar(); // 可能会抛出异常mutex.unlock();
}
为了解决这个问题, 我们可以使用std::lock_guard来保证在函数退出时mutex一定会被解锁.
std::lock_guard是一个 RAII 风格的类, 它在构造时会锁定mutex, 在析构时会解锁mutex.
void Deposit(int amount) {std::lock_guard<std::mutex> lock(mutex_);balance_ += amount;
}
修改后的代码如下:
#include <mutex>class BankAccount {public:// 存款void Deposit(int amount) {std::lock_guard<std::mutex> lock(mutex_);balance_ += amount;}// 取款void Withdraw(int amount) {std::lock_guard<std::mutex> lock(mutex_);balance_ -= amount;}// 查询余额int GetBalance() {std::lock_guard<std::mutex> lock(mutex_);return balance_;}private:int balance_ = 0;std::mutex mutex_;
};
优化读操作: std::shared_mutex
在上面的实现中, 查询余额是只读操作, 使用互斥锁会阻塞其他线程的读操作. 为提升性能, 可引入 std::shared_mutex 实现多读单写:
std::shared_mutex是一个读写锁, 它支持两种操作:
- 读操作: 使用(
std::shared_lock), 多个线程可以同时获取读锁, 但不能获取写锁. 读锁的获取不会阻塞其他读锁的获取. - 写操作: 使用(
std::lock_guard), 只有一个线程可以获取写锁, 且此时不能有其他线程获取读锁或写锁.
改进后的代码:
void Withdraw(int amount) {// 最多只能有一个线程获得锁std::lock_guard<std::shared_mutex> lock(mutex_);balance_ -= amount;
}
读取操作可以这样写:
int GetBalance() {// 可以有多个线程同时获得锁std::shared_lock<std::shared_mutex> lock(mutex_);return balance_;
}
如下是修改后的代码:
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>class BankAccount {public:// 存款void Deposit(int amount) {std::unique_lock<std::shared_mutex> lock(mutex_);balance_ += amount;}// 取款void Withdraw(int amount) {std::unique_lock<std::shared_mutex> lock(mutex_);balance_ -= amount;}// 查询余额int GetBalance() {std::shared_lock<std::shared_mutex> lock(mutex_);return balance_;}private:int balance_ = 0;std::shared_mutex mutex_;
};
多个锁的管理: std::scoped_lock
考虑在上述BankAccount类中增加一个转账的操作Transfer, 转账操作需要同时锁定两个账户的锁. 如果直接使用std::lock来锁定两个锁, 可能会出现死锁的情况.
错误的写法如下:
void Transfer(BankAccount& to, int amount) {mutex_.lock(); // 错误写法to.mutex_.lock(); // 错误写法balance_ -= amount;to.balance_ += amount;
}
为什么会出错呢, 考虑下面的使用场景:
BankAccount a, b;std::jthread t1([&a, &b] { a.Transfer(b, 100); });
std::jthread t2([&a, &b] { b.Transfer(a, 100); });
t1线程会锁定a的锁, 然后尝试锁定b的锁t2线程会锁定b的锁, 然后尝试锁定a的锁
加锁的顺序不固定会导致出现死锁的情况.
使用std::scoped_lock避免死锁
为了避免死锁, C++17 中引入了std::scoped_lock, 它可以同时锁定多个锁, 并且避免死锁的情况.
std::scoped_lock是一个 RAII 风格的类, 它在构造时会锁定多个mutex, 按照一个固定顺序去锁定, 在析构时会解锁这些mutex.
void Transfer(BankAccount& to, int amount) {std::scoped_lock lock(mutex_, to.mutex_); // 正确写法balance_ -= amount;to.balance_ += amount;
}
其他高级锁
⏳ 带超时的锁: std::timed_mutex
使用场景
在某些情况下, 我们可能需要在一段时间内尝试获取锁, 如果超时则放弃获取锁. 这种情况下可以使用std::timed_mutex.
std::timed_mutex支持为锁定操作设置超时时间, 可以用try_lock_for()和try_lock_until()来尝试获取锁.
使用std::timed_mutex实现超时锁
if (mutex.try_lock_for(std::chrono::seconds(2))) { // 尝试获取锁, 超时时间为2秒mutex.unlock();
} else {std::cout << "Failed to acquire lock for withdrawal within timeout.\n";
}
支持多次锁定的锁: std::recursive_mutex
使用场景
理论上std::recursive_mutex可以用在下面这些场景:
- 递归函数中的锁保护
- 在同一线程中调用多个依赖相同锁的函数
- 复杂逻辑流程中需要多次加锁
但是作者认为, 在实际开发中, 应该尽量避免使用std::recursive_mutex, 因为它会增加代码的复杂性, 并且容易引入死锁的风险. 而且如果一个函数需要多次加锁, 可能意味着这个函数的设计不够合理.
#include <iostream>
#include <mutex>
#include <thread>std::recursive_mutex rmutex;void recursive_function(int count) {if (count <= 0) return;rmutex.lock();std::cout << "Thread " << std::this_thread::get_id()<< " acquired lock at count " << count << std::endl;recursive_function(count - 1);rmutex.unlock();
}int main() {std::jthread t1(recursive_function, 5);std::jthread t2(recursive_function, 5);return 0;
}
带超时和可重入的锁: std::recursive_timed_mutex
是前面二者的集合体.
📅 总结
现代 C++ 提供了丰富的并发工具, 通过不同种类的锁机制, 开发者可以轻松应对复杂的多线程场景. 本文以银行账户管理系统为例, 详细阐述了以下锁的使用场景:
std::mutex和std::lock_guard: 基础的互斥锁保护std::shared_mutex和std::shared_lock: 适用于多读单写的优化场景std::scoped_lock: 解决多锁管理中的死锁问题
希望本文能帮助你更好地理解 C++ 中的锁机制, 在实际开发中灵活选择合适的工具. 🚀
相关文章:
现代C++锁介绍
文章目录 场景描述🐞 初始实现: 非线程安全版本互斥锁: std::mutex使用mutex保护共享资源使用std::lock_guard简化锁的管理 优化读操作: std::shared_mutex多个锁的管理: std::scoped_lock使用std::scoped_lock避免死锁 其他高级锁⏳ 带超时的锁: std::timed_mutex使…...
Squid代理服务器的安装使用
1.简介 Squid代理服务器是一种高效的中间服务器,位于客户端和目标服务器之间,起到了重要的网络中介作用。以下是对Squid代理服务器的详细介绍: 一、功能特点 缓存功能: Squid可以缓存经过它的请求和响应数据。当客户端发起请求时…...
爬虫学习案例8
爬取京东评论信息 采用DrissionPage自动化工具采集,感觉比Selenium工具好,真香。 安装第三方库 pip install DrissionPage pip install pandas pip install pyecharts pip install jieba pip install wordcloud1.安装DrissionPage库 DrissionPage安装…...
深入了解 CouchDB 的 Mango 查询:操作符和限制
CouchDB 是一个基于文档的数据库管理系统,支持 HTTP 协议,拥有强大的同步机制和灵活的数据模型。Mango 查询是 CouchDB 中用于数据检索的现代化查询接口,灵感来自 MongoDB 的查询语法。本文将深入探讨 Mango 查询中的各种操作符和限制,并提供详细的例子和说明,帮助你更好地…...
基于SSM(Spring + Spring MVC + MyBatis)框架搭建一个病人跟踪信息管理系统
基于SSM(Spring Spring MVC MyBatis)框架搭建一个病人治疗跟踪信息系统是一个相对复杂的项目,涉及到多个模块和功能。以下是一个简要的指导步骤。 1. 环境准备 开发环境:确保安装了Java Development Kit (JDK),建议…...
U盘文件名变乱码:原因、恢复与预防全解析
一、U盘文件名变乱码现象描述 在日常使用U盘进行数据传输和存储时,我们有时会遇到一个令人头疼的问题:U盘中的文件名突然变成了乱码,无法正常识别或访问。这些乱码文件名可能包含各种奇怪的字符和符号,使得原本有序的文件管理变得…...
EasyGBS国标GB28181公网平台P2P远程访问故障诊断:云端服务端排查指南
随着信息技术的飞速发展,视频监控领域正经历从传统安防向智能化、网络化安防的深刻转变。EasyGBS平台,作为基于国标GB28181协议的视频流媒体平台,为用户提供了强大的视频监控直播功能。然而,在实际应用中,P2P远程访问可…...
一网多平面
“一网多平面”是一种网络架构概念,具体指的是在一张物理网络之上,逻辑划分出“1N”个平面。以下是对“一网多平面”的详细解释: 定义与构成 01一网多平面 指的是在统一的物理网络基础设施上,通过逻辑划分形成多个独立的网络平面…...
animatediff 模型网盘分享
网盘 一、123网盘,不限速 https://www.123pan.com/s/ueQ8jv-OlzPh.html 网盘 网址 animatediff 国外网址https://huggingface.co/guoyww/animatediff/tree/cd71ae134a27ec6008b968d6419952b0c0494cf2 国内镜像在 https://hf-mirror.com/guoyww/animatediff/t…...
ansible play-book玩法
使用ansible-playbook实现安装nginx_ansible 安装nginx-CSDN博客文章浏览阅读1.5k次,点赞14次,收藏19次。本文详细介绍了如何在Linux环境中准备Ansible环境,包括配置主机、下载和安装Ansible,以及使用yum模块和tar包源码安装Nginx…...
MySQL索引-索引的分类和创建
索引类型 数据类型 B树索引Hash索引FullText全文索引 物理存储 聚簇索引二级索引 字段特性 主键索引唯一索引普通索引前缀索引 字段个数 单列索引联合索引 创建索引 创建表时一同创建创建表后单独创建创建表后通过修改表结构创建 可以通过 SHOW INDEX FROM test_table;查看…...
如何给负载均衡平台做好安全防御
在现代网络架构中,负载均衡(Load Balancing)扮演着至关重要的角色。它不仅负责将流量分配到多个服务器以确保高效的服务交付,还作为第一道防线来抵御外部攻击。为了保护您的应用程序和服务免受潜在威胁,必须对负载均衡…...
HR/TA/HRBP的关系
HR(人力资源)领域包含 TA(人才获取)和 HRBP(人力资源业务伙伴)这两个重要的角色,但它们只是 HR 工作的一部分分支,一般我们说的HR指TA。 1. 人才获取(TA) 定…...
Docker环境下MySQL数据库持久化部署全攻略
概述 在当今的软件开发领域,Docker容器技术已经成为应用部署和管理的新标准。它不仅简化了应用的部署流程,还为数据管理提供了灵活的解决方案。特别是在涉及到MySQL数据库时,数据持久化是一个不可忽视的重要环节。本文将分享如何在Docker中部…...
如何查看pad的console输出,以便我们更好的进行调试,查看并了解实际可能的问题。
1、以下是baidu AI回复: 2、说明: 1)如果小伙伴们经常做android开发的话,这个不陌生,因为调试都是要开启这个开发者模式。并启用USB调试模式。 2)需要连上USB线,有的时候会忘记,然…...
react中使用ResizeObserver来观察元素的size变化
在 React 中使用 ResizeObserver 来观察元素的大小变化,可以通过创建一个自定义 Hook 来封装 ResizeObserver 的逻辑,并在组件中使用这个 Hook。以下是一个完整的示例,展示了如何在 React 中使用 ResizeObserver 来观察元素的大小变化。 自定…...
Linux快速入门-Linux文件系统管理
Linux文件系统管理 1. Linux文件系统概述1.1 文件系统概念1.2 用户权限差异1.3 文件命名规范 2. Linux文件系统分类及特点2.1 ext2(第二扩展文件系统)2.2 ext3(第三扩展文件系统)2.3 ext4(第四扩展文件系统)…...
漏洞检测工具:Swagger UI敏感信息泄露
Swagger UI敏感信息泄露 漏洞定义 Swagger UI是一个交互式的、可视化的RESTful API文档工具,它允许开发人员快速浏览、测试API接口。Swagger UI通过读取由Swagger(也称为OpenAPI)规范定义的API描述文件(如swagger.json或swagger…...
VSCode如何修改默认扩展路径和用户文件夹目录到D盘
在使用Visual Studio Code(VSCode)时,随着安装的扩展和用户数据的增多,C盘的空间可能会逐渐紧张。为了优化存储管理,将VSCode的默认扩展路径和用户文件夹目录迁移到D盘是一个有效的解决方案。以下是详细的操作步骤&…...
【超详细实操内容】django的身份验证系统之限制用户访问的三种方式
目录 1、使用request.user.is_authenticated属性 2、装饰器login_required 3、LoginRequiredMixin类 通常情况下,网站都会对用户限制访问,例如,未登录的用户不可访问用户中心页面。Django框架中使用request.user.isauthenticated属性、装饰器loginrequired和LoginRequire…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...
【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
多模态图像修复系统:基于深度学习的图片修复实现
多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...
Unity中的transform.up
2025年6月8日,周日下午 在Unity中,transform.up是Transform组件的一个属性,表示游戏对象在世界空间中的“上”方向(Y轴正方向),且会随对象旋转动态变化。以下是关键点解析: 基本定义 transfor…...
02.运算符
目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&:逻辑与 ||:逻辑或 !:逻辑非 短路求值 位运算符 按位与&: 按位或 | 按位取反~ …...
