现代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…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...
JS手写代码篇----使用Promise封装AJAX请求
15、使用Promise封装AJAX请求 promise就有reject和resolve了,就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
【安全篇】金刚不坏之身:整合 Spring Security + JWT 实现无状态认证与授权
摘要 本文是《Spring Boot 实战派》系列的第四篇。我们将直面所有 Web 应用都无法回避的核心问题:安全。文章将详细阐述认证(Authentication) 与授权(Authorization的核心概念,对比传统 Session-Cookie 与现代 JWT(JS…...
ThreadLocal 源码
ThreadLocal 源码 此类提供线程局部变量。这些变量不同于它们的普通对应物,因为每个访问一个线程局部变量的线程(通过其 get 或 set 方法)都有自己独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些类希望将…...
在Zenodo下载文件 用到googlecolab googledrive
方法:Figshare/Zenodo上的数据/文件下载不下来?尝试利用Google Colab :https://zhuanlan.zhihu.com/p/1898503078782674027 参考: 通过Colab&谷歌云下载Figshare数据,超级实用!!࿰…...
