C++并发:锁
一、前言
C++中的锁和同步原语的多样化选择使得程序员可以根据具体的线程和数据保护需求来选择最合适的工具。这些工具的正确使用可以大大提高程序的稳定性和性能,本文讨论了部分锁。
二、std::lock
在C++中,std::lock
是一个用于一次性锁定两个或多个互斥量(mutexes)的函数,而且还保证不会发生死锁。这是通过采用一种称为“死锁避免算法”的技术来实现的,该技术能够保证多个互斥量按照一定的顺序加锁。
使用场景
当需要同时锁定多个互斥量,而且希望避免因为锁定顺序不一致而引起死锁时,使用std::lock
是非常合适的。它通常与 std::unique_lock
或 std::lock_guard
配合使用,以提供灵活的锁定管理或自动锁定和解锁功能。
基本用法
以下是std::lock
的一个基本示例,展示如何使用它来安全地锁定两个互斥量:
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx1, mtx2;void process_data() {// 使用std::lock来同时锁定两个互斥量std::lock(mtx1, mtx2);// 确保两个互斥量都已锁定,使用std::lock_guard进行管理,不指定std::adopt_lock参数std::lock_guard<std::mutex> lk1(mtx1, std::adopt_lock);std::lock_guard<std::mutex> lk2(mtx2, std::adopt_lock);// 执行一些操作std::cout << "Processing shared data." << std::endl;
}int main() {std::thread t1(process_data);std::thread t2(process_data);t1.join();t2.join();return 0;
}
说明
-
std::lock:这个函数尝试锁定所有提供的互斥量,不返回直到所有的互斥量都成功锁定。它使用一个特殊的锁定算法来避免死锁。
-
std::lock_guard:此范例中用
std::lock_guard
来自动管理互斥量的锁定状态。由于互斥量已经被std::lock
锁定,所以我们使用std::adopt_lock
标记,告诉std::lock_guard
对象互斥量已经被锁定,并且在std::lock_guard
的生命周期结束时释放它们。 -
std::adopt_lock:这是一个构造参数,告诉
std::lock_guard
或std::unique_lock
对象该互斥量已经被当前线程锁定了,对象不应该尝试再次锁定互斥量,而是在析构时解锁它。
通过使用 std::adopt_lock
参数,正确地指示了 std::lock_guard
对象(在这个例子中是 lk1
和 lk2
),互斥量已经被当前线程锁定。这样,std::lock_guard
不会在构造时尝试锁定互斥量,而是会在其析构函数中释放它们。
这意味着,当 lk1
和 lk2
的作用域结束时(例如,当 process_data
函数执行完毕时),lk1
会自动释放 mtx1
,lk2
会自动释放 mtx2
。这是 std::lock_guard
的典型用法,通过在构造时获取锁并在析构时释放锁,它提供了一种方便的资源管理方式,这种方式常被称为 RAII(Resource Acquisition Is Initialization)。
三、std::lock_guard
上面的实例中已经用到了 std::lock_guard
,主要是想利用它的 RAII 特性。下面详细介绍 std::lock_guard
。
std::lock_guard
是 C++ 中一个非常有用的同步原语,用于在作用域内自动管理互斥量的锁定和解锁。它是一个模板类,提供了一种方便的方式来实现作用域内的锁定保护,确保在任何退出路径(包括异常退出)上都能释放锁,从而帮助避免死锁。
基本用法
std::lock_guard
的基本用法很简单:在需要保护的代码块前创建一个 std::lock_guard
对象,将互斥量作为参数传递给它。std::lock_guard
会在构造时自动锁定互斥量,在其析构函数中自动解锁互斥量。
示例代码
这里是一个使用 std::lock_guard
的简单示例:
#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx; // 全局互斥量void print_data(const std::string& data) {std::lock_guard<std::mutex> guard(mtx); // 创建时自动锁定mtx// 以下代码在互斥锁保护下执行std::cout << data << std::endl;// guard 在离开作用域时自动解锁mtx
}int main() {std::thread t1(print_data, "Hello from Thread 1");std::thread t2(print_data, "Hello from Thread 2");t1.join();t2.join();return 0;
}
说明
-
自动锁定与解锁:在
print_data
函数中,std::lock_guard
的实例guard
在创建时自动对mtx
进行锁定,并在函数结束时(guard
的生命周期结束时)自动对mtx
进行解锁。这确保了即使在发生异常的情况下也能释放锁,从而防止死锁。 -
作用域控制:
std::lock_guard
的作用范围限制于它被定义的代码块内。一旦代码块执行完毕,std::lock_guard
会被销毁,互斥量会被自动释放。 -
不支持手动控制:与
std::unique_lock
不同,std::lock_guard
不提供锁的手动控制(如调用lock()
和unlock()
)。它仅在构造时自动加锁,在析构时自动解锁。
通过使用 std::lock_guard
,你可以确保即使面对多个返回路径和异常,互斥锁的管理也是安全的,从而简化多线程代码的编写。这使得 std::lock_guard
成为处理互斥量时的首选工具之一,尤其是在简单的锁定场景中。
四、std::unique_lock
std::unique_lock
是 C++ 标准库中的一个灵活的同步工具,用于管理互斥量(mutex)。与 std::lock_guard
相比,std::unique_lock
提供了更多的控制能力,包括延迟锁定、尝试锁定、条件变量支持和手动锁定与解锁的能力。这使得 std::unique_lock
在需要复杂锁定逻辑的情况下非常有用。
基本用法
std::unique_lock
的基本用法包括自动管理互斥量的锁定和解锁,但它也支持手动操作和条件变量。
示例代码
下面是一些展示 std::unique_lock
使用方式的示例:
基本的自动锁定与解锁
#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx; // 全局互斥量void print_data(const std::string& data) {std::unique_lock<std::mutex> lock(mtx); // 在构造时自动锁定mtxstd::cout << data << std::endl;// lock 在离开作用域时自动解锁mtx
}int main() {std::thread t1(print_data, "Thread 1");std::thread t2(print_data, "Thread 2");t1.join();t2.join();return 0;
}
延迟锁定
std::unique_lock
允许延迟锁定,即创建锁对象时不立即锁定互斥量。
void delayed_lock_example() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 创建时不锁定// 进行一些不需要互斥量保护的操作lock.lock(); // 现在需要锁定std::cout << "Locked and safe" << std::endl;// lock 在离开作用域时自动解锁mtx
}
手动控制锁定与解锁
std::unique_lock
提供了 lock()
和 unlock()
方法,允许在其生命周期内多次锁定和解锁。
void manual_lock_control() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock);// 决定什么时候锁定lock.lock();std::cout << "Processing data" << std::endl;lock.unlock();// 可以再次锁定lock.lock();std::cout << "Processing more data" << std::endl;// lock 在离开作用域时自动解锁mtx
}
与条件变量结合使用
std::unique_lock
通常与条件变量一起使用,因为它支持在等待期间解锁和重新锁定。
std::condition_variable cv;
bool data_ready = false;void data_preparation_thread() {{std::unique_lock<std::mutex> lock(mtx);// 准备数据data_ready = true;}cv.notify_one(); // 通知等待线程
}void data_processing_thread() {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return data_ready; }); // 等待数据准备好// 处理数据std::cout << "Data processed" << std::endl;
}
转移互斥归属权到函数调用者
转移有一种用途:准许函数锁定互斥,然后把互斥的归属权转移给函数调用者,好让它在同一个锁的保护下执行其他操作。下面的代码片段就此做了示范:get_lock()
函数先锁定互斥,接着对数据做前期准备,再将归属权返回给调用者:
std::unique_lock<std::mutex> get_lock()
{extern std::mutex some_mutex;std::unique_lock<std::mutex> lk(some_mutex);prepare_data();return lk; ⇽--- ①
}
void process_data()
{std::unique_lock<std::mutex> lk(get_lock()); ⇽--- ②do_something();
}
①处通过移动构造创建返回值,该值为右值。然后右值在②处移动构造 lk
。我们关注的是,这里的 std::unique_lock
的移动语义特性。这使得 std::unique_lock
对象可以在函数或其他作用域之间传递互斥体的所有权,而不是仅仅通过复制来共享所有权。这一点尤其重要,因为 std::unique_lock
管理的互斥体锁定状态需要保持一致性和独占性,复制操作会破坏这一点。
std::unique_lock
类十分灵活,允许它的实例在被销毁前解锁。其成员函数 unlock()
负责解锁操作,这与互斥一致。
五、std::scoped_lock(C++17)
前面的实例中,有些复杂,我们可以使用更简单的 std::scoped_lock
。因为它自动处理了多个互斥量的锁定和解锁,而不需要显式指定 std::adopt_lock
。C++17提供了新的RAII类模板std::scoped_lock<>
。它封装了多互斥体的锁定功能,确保无死锁,且使用方便。
std::scoped_lock
自动锁定其构造函数中传递的所有互斥体,并在作用域结束时释放它们,因此非常适合用于替代 std::lock
加 std::lock_guard
的组合使用。
示例
以下是一个使用 std::scoped_lock
的例子,处理两个互斥量:
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx1, mtx2;void process_shared_data() {// 使用std::scoped_lock同时锁定两个互斥量std::scoped_lock lock(mtx1, mtx2); //<------①// 执行一些操作std::cout << "Processing shared data safely." << std::endl;
}int main() {std::thread t1(process_shared_data);std::thread t2(process_shared_data);t1.join();t2.join();return 0;
}
说明
在这个例子中:
- std::scoped_lock: 构造时自动锁定传递给它的所有互斥量(在这里是
mtx1
和mtx2
)。这样的锁定是原子的,这意味着它使用死锁避免算法来避免在尝试锁定多个互斥量时可能发生的死锁问题。 - 自动解锁:当
std::scoped_lock
的实例lock
的作用域结束时,它自动以安全的顺序释放所有互斥体。这在函数process_shared_data
结束时发生。 - 简洁性和安全性:与
std::lock
和std::lock_guard
结合使用相比,std::scoped_lock
更简洁且不易出错,因为不需要使用std::adopt_lock
或担心锁定的顺序。
C++17具有隐式类模板参数推导(implicit class template parameter deduction)机制,依据传入构造函数的参数对象自动匹配,选择正确的互斥型别。①处的语句等价于下面完整写明的版本:
std::scoped_lock<std::mutex,std::mutex> lock(mtx1, mtx2);
六、防范死锁的补充准则
防范死锁的准则最终可归纳成一个思想:只要另一线程有可能正在等待当前线程,那么当前线程千万不能反过来等待它。
- 第一条准则最简单:假如已经持有锁,就不要试图获取第二个锁。
- 一旦持锁,就须避免调用由用户提供的程序接口。
- 依从固定顺序获取锁。
- 按照层接加锁。
按照层级加锁
这一块儿比较重要,需要展开讨论。思路是,我们把应用程序分层,并且明确每个互斥位于哪个层级。若某线程已对低层级互斥加锁,则不准它再对高层级互斥加锁。以下伪代码示范了两个线程如何运用层级互斥:
hierarchical_mutex high_level_mutex(10000); ⇽--- ①
hierarchical_mutex low_level_mutex(5000); ⇽--- ②
hierarchical_mutex other_mutex(6000); ⇽--- ③
int do_low_level_stuff();
int low_level_func()
{std::lock_guard<hierarchical_mutex> lk(low_level_mutex); ⇽--- ④return do_low_level_stuff();
}
void high_level_stuff(int some_param);
void high_level_func()
{std::lock_guard<hierarchical_mutex> lk(high_level_mutex); ⇽--- ⑥high_level_stuff(low_level_func()); ⇽--- ⑤
}
void thread_a() ⇽--- ⑦
{high_level_func();
}void do_other_stuff();
void other_stuff()
{high_level_func(); ⇽--- ⑩do_other_stuff();
}
void thread_b() ⇽--- ⑧
{std::lock_guard<hierarchical_mutex> lk(other_mutex); ⇽--- ⑨other_stuff();
}
显然,⑧处的代码不符合规范,因为目前持有的锁是 other_mutex
,其标号是 6000,而底层调用的代码 other_stuff()
中却持有了一个 high_level_mutex
,其标号为 10000。这没有遵守底层调用持有底层锁,hierarchical_mutex
会抛出异常。
七、参考
《C++并发编程实战》(第二版)。
相关文章:
C++并发:锁
一、前言 C中的锁和同步原语的多样化选择使得程序员可以根据具体的线程和数据保护需求来选择最合适的工具。这些工具的正确使用可以大大提高程序的稳定性和性能,本文讨论了部分锁。 二、std::lock 在C中,std::lock 是一个用于一次性锁定两个或多个互斥…...
Git | git log 和 git status 的区别
如是我闻: git log和git status是Git中的两个非常有用的命令,它们用于不同的目的,并提供不同类型的信息。 git log git log命令用于显示一个或多个分支的提交历史记录。这个命令会列出提交历史,包括每次提交的SHA-1哈希值、提交…...

Django 4.x 智能分页get_elided_page_range
Django智能分页 分页效果 第1页的效果 第10页的效果 带输入框的效果 主要函数 # 参数解释 # number: 当前页码,默认:1 # on_each_side:当前页码前后显示几页,默认:3 # on_ends:首尾固定显示几页&#…...

java-spring 09 下.populateBean (方法成员变量的注入@Autowird,@Resource)
1.在populateBean 方法中的一部分:用于Autowird,Resource注入 // 后处理器已经初始化boolean hasInstAwareBpps hasInstantiationAwareBeanPostProcessors();// 需要依赖检查boolean needsDepCheck (mbd.getDependencyCheck() ! AbstractBeanDefinitio…...

赛氪网携手众机构助力第七届京津冀生态修复实践论坛圆满落幕
近日,由北京生态修复学会联合工业固废网、中国老科协国土资源分会共同主办,赛氪网作为支持单位的第七届京津冀生态修复实践论坛在北京温德姆酒店圆满落幕。本次论坛汇聚了众多行业专家、学者以及企业代表,共同探讨生态修复领域的新技术、新方…...

Naive RAG 、Advanced RAG 和 Modular RAG 简介
简介: RAG(Retrieval-Augmented Generation)系统是一种结合了检索(Retrieval)和生成(Generation)的机制,用于提高大型语言模型(LLMs)在特定任务上的表现。随…...

Python高级编程-DJango2
Python高级编程-DJango2 没有清醒的头脑,再快的脚步也会走歪;没有谨慎的步伐,再平的道路也会跌倒。 目录 Python高级编程-DJango2 1.显示基本网页 2.输入框的形式: 1)文本输入框 2)单选框 3ÿ…...
bash脚本 报错:/bin/bash^M:解释器错误: 没有那个文件或目录
bash脚本 报错:/bin/bash^M:解释器错误: 没有那个文件或目录 出现这个问题是因为该脚本文件在windows下编辑过 在windows下,每一行的结尾是\n\r,而在linux下文件的结尾是\n,那么你在windows下编辑过的文件在linux下打…...

win10专业版远程桌面连接不上,win10专业版远程桌面连接不上常见原因与解决方法
Win10专业版远程桌面连接功能是一项非常实用的工具,它允许用户远程访问和操作另一台计算机。然而,有时在尝试进行远程桌面连接时,可能会遇到连接不上的情况。本文将分析导致这一问题的常见原因,并提供相应的解决方法。 一、常见原…...
前端 日期 new Date 少0 转换成 yyyy-MM-dd js vue
在console控制台直接输出new Date(),是这样: Fri May 10 2024 23:36:06 GMT0800 (中国标准时间) 输出new Date().toLocaleString(),是这样: 2024/5/10 23:36:06 输出new Date().toISOString(),是这样: …...

Linux中的磁盘分析工具ncdu
2024年5月14日,周二上午 概述 ncdu 是一个基于文本的用户界面磁盘使用情况分析工具。它可以在终端中快速扫描目录,并统计该目录下的文件和文件夹的磁盘使用情况,以交互友好的方式呈现给用户。 安装 在 Debian/Ubuntu 系统下,可…...

Angular入门
Angular版本:Angular 版本演进史概述-天翼云开发者社区 - 天翼云 安装nodejs:Node.js安装与配置环境 v20.13.1(LTS)-CSDN博客 Angular CLI是啥 Angular CLI 是一个命令行接口(Angular Command Line Interface),是开发 Angular 应用的最快、最…...

Java进阶11 IO流、功能流
Java进阶11 IO流-功能流 一、字符缓冲流 字符缓冲流在源代码中内置了字符数组,可以提高读写效率 1、构造方法 方法说明BufferedReader(new FileReader(文件路径))对传入的字符输入流进行包装BufferedWriter(new FileWriter(文件路径))对传入的字符输出流进行包装…...

windows 安装 Conda
1 Conda简介 Conda 是一个开源的软件包管理系统和环境管理系统,用于安装多个版本的软件包及其依赖关系,并在它们之间轻松切换。Conda 是为 Python 程序创建的,适用于 Linux,OS X 和Windows,也可以打包和分发其他软件。一般用conda来维护多个python版本。 2 安装…...

IPsec VPN简介
什么是IPsec? IPsec(Internet Protocol Security)是为IP网络提供安全性的协议和服务的集合,它是VPN(Virtual Private Network,虚拟专用网)中常用的一种技术。其实就是一种协议簇(类…...

探索 Canva 的功能以及如何有效使用 Canva
『创意瞬间变现!Canva AI Drawing 让你的文字描绘成艺术』 在数字设计和创意领域,Canva 是创新和用户友好性的灯塔。这个平台不仅简化了图形设计,还引入了 AI Drawing 等强大工具,使其成为专业人士和初学者的首选解决方案。让我们…...
python中匿名函数简单样例
目录 一、匿名函数(也称为 lambda 函数): 二、简单样例: 2.1 filter() 函数: 2.2 map() 函数: 2.3 sorted() 函数: 一、匿名函数(也称为 lambda 函数): 简洁性:匿名函数通常比命…...

【SpringBoot】 什么是springboot(二)?springboot操作mybatisPlus、swagger、thymeleaf模板
文章目录 SpringBoot第三章1、整合mybatsPlus1-234-67-10问题 2、整合pageHelper分页3、MP代码生成器1、编写yml文件2、导入依赖3、创建mp代码生成器4、生成代码5、编写配置类扫描mapper类6、编写控制器类 4、swagger1、什么是swagger2、作用3、发展历程4、一个简单的swagger项…...

【JavaWeb】前后端分离SpringBoot项目快速排错指南
1 发起业务请求 打开浏览器开发者工具,同时显示网络(Internet)和控制台(console) 接着,清空控制台和网络的内容,如下图 然后,点击你的业务按钮,发起请求。 首先看控制台…...
Go语言高级特性
目录 1. 并发编程 1.1 Goroutine轻量级线程 1.2 Channel通信机制 1.3 WaitGroup等待组 1.4 Mutex互斥锁 2. 垃圾回收机制 2.1 内存管理介绍 2.2 垃圾回收原理 2.3 性能调优策略 2.4 常见问题及解决方案 3. 接口与反射 3.1 接口定义与实现 3.2 空接口与类型断言 3…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
React 第五十五节 Router 中 useAsyncError的使用详解
前言 useAsyncError 是 React Router v6.4 引入的一个钩子,用于处理异步操作(如数据加载)中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误:捕获在 loader 或 action 中发生的异步错误替…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...

NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...

算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...