当前位置: 首页 > 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…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

2025盘古石杯决赛【手机取证】

前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来&#xff0c;实在找不到&#xff0c;希望有大佬教一下我。 还有就会议时间&#xff0c;我感觉不是图片时间&#xff0c;因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战

在现代战争中&#xff0c;电磁频谱已成为继陆、海、空、天之后的 “第五维战场”&#xff0c;雷达作为电磁频谱领域的关键装备&#xff0c;其干扰与抗干扰能力的较量&#xff0c;直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器&#xff0c;凭借数字射…...

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)

UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中&#xff0c;UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化&#xf…...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)

目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关&#xff0…...

CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝

目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为&#xff1a;一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...

Oracle11g安装包

Oracle 11g安装包 适用于windows系统&#xff0c;64位 下载路径 oracle 11g 安装包...