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

现代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_lockstd::scoped_lock, 分别用于管理读锁生命周期和防止多锁死锁问题.

为了让初学者更直观地理解这些锁的应用场景, 本文以银行账户管理系统为例, 逐步介绍现代 C++ 中不同锁的特点和使用方法.


场景描述

设计一个银行账户类, 可以执行以下操作:

  1. 存款(Deposit)
  2. 取款(Withdraw)
  3. 查询余额(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可以用在下面这些场景:

  1. 递归函数中的锁保护
  2. 在同一线程中调用多个依赖相同锁的函数
  3. 复杂逻辑流程中需要多次加锁

但是作者认为, 在实际开发中, 应该尽量避免使用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::mutexstd::lock_guard: 基础的互斥锁保护
  • std::shared_mutexstd::shared_lock: 适用于多读单写的优化场景
  • std::scoped_lock: 解决多锁管理中的死锁问题

希望本文能帮助你更好地理解 C++ 中的锁机制, 在实际开发中灵活选择合适的工具. 🚀

相关文章:

现代C++锁介绍

文章目录 场景描述&#x1f41e; 初始实现: 非线程安全版本互斥锁: std::mutex使用mutex保护共享资源使用std::lock_guard简化锁的管理 优化读操作: std::shared_mutex多个锁的管理: std::scoped_lock使用std::scoped_lock避免死锁 其他高级锁⏳ 带超时的锁: std::timed_mutex使…...

Squid代理服务器的安装使用

1.简介 Squid代理服务器是一种高效的中间服务器&#xff0c;位于客户端和目标服务器之间&#xff0c;起到了重要的网络中介作用。以下是对Squid代理服务器的详细介绍&#xff1a; 一、功能特点 缓存功能&#xff1a; Squid可以缓存经过它的请求和响应数据。当客户端发起请求时…...

爬虫学习案例8

爬取京东评论信息 采用DrissionPage自动化工具采集&#xff0c;感觉比Selenium工具好&#xff0c;真香。 安装第三方库 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&#xff08;Spring Spring MVC MyBatis&#xff09;框架搭建一个病人治疗跟踪信息系统是一个相对复杂的项目&#xff0c;涉及到多个模块和功能。以下是一个简要的指导步骤。 1. 环境准备 开发环境&#xff1a;确保安装了Java Development Kit (JDK)&#xff0c;建议…...

U盘文件名变乱码:原因、恢复与预防全解析

一、U盘文件名变乱码现象描述 在日常使用U盘进行数据传输和存储时&#xff0c;我们有时会遇到一个令人头疼的问题&#xff1a;U盘中的文件名突然变成了乱码&#xff0c;无法正常识别或访问。这些乱码文件名可能包含各种奇怪的字符和符号&#xff0c;使得原本有序的文件管理变得…...

EasyGBS国标GB28181公网平台P2P远程访问故障诊断:云端服务端排查指南

随着信息技术的飞速发展&#xff0c;视频监控领域正经历从传统安防向智能化、网络化安防的深刻转变。EasyGBS平台&#xff0c;作为基于国标GB28181协议的视频流媒体平台&#xff0c;为用户提供了强大的视频监控直播功能。然而&#xff0c;在实际应用中&#xff0c;P2P远程访问可…...

一网多平面

“一网多平面”是一种网络架构概念&#xff0c;具体指的是在一张物理网络之上&#xff0c;逻辑划分出“1N”个平面。以下是对“一网多平面”的详细解释&#xff1a; 定义与构成 01一网多平面 指的是在统一的物理网络基础设施上&#xff0c;通过逻辑划分形成多个独立的网络平面…...

animatediff 模型网盘分享

网盘 一、123网盘&#xff0c;不限速 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次&#xff0c;点赞14次&#xff0c;收藏19次。本文详细介绍了如何在Linux环境中准备Ansible环境&#xff0c;包括配置主机、下载和安装Ansible&#xff0c;以及使用yum模块和tar包源码安装Nginx…...

MySQL索引-索引的分类和创建

索引类型 数据类型 B树索引Hash索引FullText全文索引 物理存储 聚簇索引二级索引 字段特性 主键索引唯一索引普通索引前缀索引 字段个数 单列索引联合索引 创建索引 创建表时一同创建创建表后单独创建创建表后通过修改表结构创建 可以通过 SHOW INDEX FROM test_table;查看…...

如何给负载均衡平台做好安全防御

在现代网络架构中&#xff0c;负载均衡&#xff08;Load Balancing&#xff09;扮演着至关重要的角色。它不仅负责将流量分配到多个服务器以确保高效的服务交付&#xff0c;还作为第一道防线来抵御外部攻击。为了保护您的应用程序和服务免受潜在威胁&#xff0c;必须对负载均衡…...

HR/TA/HRBP的关系

HR&#xff08;人力资源&#xff09;领域包含 TA&#xff08;人才获取&#xff09;和 HRBP&#xff08;人力资源业务伙伴&#xff09;这两个重要的角色&#xff0c;但它们只是 HR 工作的一部分分支&#xff0c;一般我们说的HR指TA。 1. 人才获取&#xff08;TA&#xff09; 定…...

Docker环境下MySQL数据库持久化部署全攻略

概述 在当今的软件开发领域&#xff0c;Docker容器技术已经成为应用部署和管理的新标准。它不仅简化了应用的部署流程&#xff0c;还为数据管理提供了灵活的解决方案。特别是在涉及到MySQL数据库时&#xff0c;数据持久化是一个不可忽视的重要环节。本文将分享如何在Docker中部…...

如何查看pad的console输出,以便我们更好的进行调试,查看并了解实际可能的问题。

1、以下是baidu AI回复&#xff1a; 2、说明&#xff1a; 1&#xff09;如果小伙伴们经常做android开发的话&#xff0c;这个不陌生&#xff0c;因为调试都是要开启这个开发者模式。并启用USB调试模式。 2&#xff09;需要连上USB线&#xff0c;有的时候会忘记&#xff0c;然…...

react中使用ResizeObserver来观察元素的size变化

在 React 中使用 ResizeObserver 来观察元素的大小变化&#xff0c;可以通过创建一个自定义 Hook 来封装 ResizeObserver 的逻辑&#xff0c;并在组件中使用这个 Hook。以下是一个完整的示例&#xff0c;展示了如何在 React 中使用 ResizeObserver 来观察元素的大小变化。 自定…...

Linux快速入门-Linux文件系统管理

Linux文件系统管理 1. Linux文件系统概述1.1 文件系统概念1.2 用户权限差异1.3 文件命名规范 2. Linux文件系统分类及特点2.1 ext2&#xff08;第二扩展文件系统&#xff09;2.2 ext3&#xff08;第三扩展文件系统&#xff09;2.3 ext4&#xff08;第四扩展文件系统&#xff09…...

漏洞检测工具:Swagger UI敏感信息泄露

Swagger UI敏感信息泄露 漏洞定义 Swagger UI是一个交互式的、可视化的RESTful API文档工具&#xff0c;它允许开发人员快速浏览、测试API接口。Swagger UI通过读取由Swagger&#xff08;也称为OpenAPI&#xff09;规范定义的API描述文件&#xff08;如swagger.json或swagger…...

VSCode如何修改默认扩展路径和用户文件夹目录到D盘

在使用Visual Studio Code&#xff08;VSCode&#xff09;时&#xff0c;随着安装的扩展和用户数据的增多&#xff0c;C盘的空间可能会逐渐紧张。为了优化存储管理&#xff0c;将VSCode的默认扩展路径和用户文件夹目录迁移到D盘是一个有效的解决方案。以下是详细的操作步骤&…...

【超详细实操内容】django的身份验证系统之限制用户访问的三种方式

目录 1、使用request.user.is_authenticated属性 2、装饰器login_required 3、LoginRequiredMixin类 通常情况下,网站都会对用户限制访问,例如,未登录的用户不可访问用户中心页面。Django框架中使用request.user.isauthenticated属性、装饰器loginrequired和LoginRequire…...

【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15

缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下&#xff1a; struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

无法与IP建立连接,未能下载VSCode服务器

如题&#xff0c;在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈&#xff0c;发现是VSCode版本自动更新惹的祸&#xff01;&#xff01;&#xff01; 在VSCode的帮助->关于这里发现前几天VSCode自动更新了&#xff0c;我的版本号变成了1.100.3 才导致了远程连接出…...

vscode(仍待补充)

写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh&#xff1f; debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...

【解密LSTM、GRU如何解决传统RNN梯度消失问题】

解密LSTM与GRU&#xff1a;如何让RNN变得更聪明&#xff1f; 在深度学习的世界里&#xff0c;循环神经网络&#xff08;RNN&#xff09;以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而&#xff0c;传统RNN存在的一个严重问题——梯度消失&#…...

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

04-初识css

一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

c#开发AI模型对话

AI模型 前面已经介绍了一般AI模型本地部署&#xff0c;直接调用现成的模型数据。这里主要讲述讲接口集成到我们自己的程序中使用方式。 微软提供了ML.NET来开发和使用AI模型&#xff0c;但是目前国内可能使用不多&#xff0c;至少实践例子很少看见。开发训练模型就不介绍了&am…...