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

C++并发:锁

一、前言

C++中的锁和同步原语的多样化选择使得程序员可以根据具体的线程和数据保护需求来选择最合适的工具。这些工具的正确使用可以大大提高程序的稳定性和性能,本文讨论了部分锁。

二、std::lock

在C++中,std::lock 是一个用于一次性锁定两个或多个互斥量(mutexes)的函数,而且还保证不会发生死锁。这是通过采用一种称为“死锁避免算法”的技术来实现的,该技术能够保证多个互斥量按照一定的顺序加锁

使用场景

当需要同时锁定多个互斥量,而且希望避免因为锁定顺序不一致而引起死锁时,使用std::lock 是非常合适的。它通常与 std::unique_lockstd::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;
}

说明

  1. std::lock:这个函数尝试锁定所有提供的互斥量,不返回直到所有的互斥量都成功锁定。它使用一个特殊的锁定算法来避免死锁。

  2. std::lock_guard:此范例中用 std::lock_guard 来自动管理互斥量的锁定状态。由于互斥量已经被 std::lock 锁定,所以我们使用 std::adopt_lock 标记,告诉 std::lock_guard 对象互斥量已经被锁定,并且在 std::lock_guard 的生命周期结束时释放它们。

  3. std::adopt_lock:这是一个构造参数,告诉 std::lock_guardstd::unique_lock 对象该互斥量已经被当前线程锁定了,对象不应该尝试再次锁定互斥量,而是在析构时解锁它

通过使用 std::adopt_lock 参数,正确地指示了 std::lock_guard 对象(在这个例子中是 lk1lk2),互斥量已经被当前线程锁定。这样,std::lock_guard 不会在构造时尝试锁定互斥量,而是会在其析构函数中释放它们。

这意味着,当 lk1lk2 的作用域结束时(例如,当 process_data 函数执行完毕时),lk1 会自动释放 mtx1lk2 会自动释放 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;
}

说明

  1. 自动锁定与解锁:在 print_data 函数中,std::lock_guard 的实例 guard 在创建时自动对 mtx 进行锁定,并在函数结束时(guard 的生命周期结束时)自动对 mtx 进行解锁。这确保了即使在发生异常的情况下也能释放锁,从而防止死锁。

  2. 作用域控制std::lock_guard 的作用范围限制于它被定义的代码块内。一旦代码块执行完毕,std::lock_guard 会被销毁,互斥量会被自动释放。

  3. 不支持手动控制:与 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::lockstd::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;
}

说明

在这个例子中:

  1. std::scoped_lock: 构造时自动锁定传递给它的所有互斥量(在这里是 mtx1mtx2)。这样的锁定是原子的,这意味着它使用死锁避免算法来避免在尝试锁定多个互斥量时可能发生的死锁问题。
  2. 自动解锁:当 std::scoped_lock 的实例 lock 的作用域结束时,它自动以安全的顺序释放所有互斥体。这在函数 process_shared_data 结束时发生。
  3. 简洁性和安全性:与 std::lockstd::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中的锁和同步原语的多样化选择使得程序员可以根据具体的线程和数据保护需求来选择最合适的工具。这些工具的正确使用可以大大提高程序的稳定性和性能&#xff0c;本文讨论了部分锁。 二、std::lock 在C中&#xff0c;std::lock 是一个用于一次性锁定两个或多个互斥…...

Git | git log 和 git status 的区别

如是我闻&#xff1a; git log和git status是Git中的两个非常有用的命令&#xff0c;它们用于不同的目的&#xff0c;并提供不同类型的信息。 git log git log命令用于显示一个或多个分支的提交历史记录。这个命令会列出提交历史&#xff0c;包括每次提交的SHA-1哈希值、提交…...

Django 4.x 智能分页get_elided_page_range

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

java-spring 09 下.populateBean (方法成员变量的注入@Autowird,@Resource)

1.在populateBean 方法中的一部分&#xff1a;用于Autowird&#xff0c;Resource注入 // 后处理器已经初始化boolean hasInstAwareBpps hasInstantiationAwareBeanPostProcessors();// 需要依赖检查boolean needsDepCheck (mbd.getDependencyCheck() ! AbstractBeanDefinitio…...

赛氪网携手众机构助力第七届京津冀生态修复实践论坛圆满落幕

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

Naive RAG 、Advanced RAG 和 Modular RAG 简介

简介&#xff1a; RAG&#xff08;Retrieval-Augmented Generation&#xff09;系统是一种结合了检索&#xff08;Retrieval&#xff09;和生成&#xff08;Generation&#xff09;的机制&#xff0c;用于提高大型语言模型&#xff08;LLMs&#xff09;在特定任务上的表现。随…...

Python高级编程-DJango2

Python高级编程-DJango2 没有清醒的头脑&#xff0c;再快的脚步也会走歪&#xff1b;没有谨慎的步伐&#xff0c;再平的道路也会跌倒。 目录 Python高级编程-DJango2 1.显示基本网页 2.输入框的形式&#xff1a; 1&#xff09;文本输入框 2&#xff09;单选框 3&#xff…...

bash脚本 报错:/bin/bash^M:解释器错误: 没有那个文件或目录

bash脚本 报错&#xff1a;/bin/bash^M&#xff1a;解释器错误: 没有那个文件或目录 出现这个问题是因为该脚本文件在windows下编辑过 在windows下&#xff0c;每一行的结尾是\n\r&#xff0c;而在linux下文件的结尾是\n&#xff0c;那么你在windows下编辑过的文件在linux下打…...

win10专业版远程桌面连接不上,win10专业版远程桌面连接不上常见原因与解决方法

Win10专业版远程桌面连接功能是一项非常实用的工具&#xff0c;它允许用户远程访问和操作另一台计算机。然而&#xff0c;有时在尝试进行远程桌面连接时&#xff0c;可能会遇到连接不上的情况。本文将分析导致这一问题的常见原因&#xff0c;并提供相应的解决方法。 一、常见原…...

前端 日期 new Date 少0 转换成 yyyy-MM-dd js vue

在console控制台直接输出new Date()&#xff0c;是这样&#xff1a; Fri May 10 2024 23:36:06 GMT0800 (中国标准时间) 输出new Date().toLocaleString()&#xff0c;是这样&#xff1a; 2024/5/10 23:36:06 输出new Date().toISOString()&#xff0c;是这样&#xff1a; …...

Linux中的磁盘分析工具ncdu

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

Angular入门

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

Java进阶11 IO流、功能流

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

windows 安装 Conda

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

IPsec VPN简介

什么是IPsec&#xff1f; IPsec&#xff08;Internet Protocol Security&#xff09;是为IP网络提供安全性的协议和服务的集合&#xff0c;它是VPN&#xff08;Virtual Private Network&#xff0c;虚拟专用网&#xff09;中常用的一种技术。其实就是一种协议簇&#xff08;类…...

探索 Canva 的功能以及如何有效使用 Canva

『创意瞬间变现&#xff01;Canva AI Drawing 让你的文字描绘成艺术』 在数字设计和创意领域&#xff0c;Canva 是创新和用户友好性的灯塔。这个平台不仅简化了图形设计&#xff0c;还引入了 AI Drawing 等强大工具&#xff0c;使其成为专业人士和初学者的首选解决方案。让我们…...

python中匿名函数简单样例

目录 一、匿名函数&#xff08;也称为 lambda 函数&#xff09;&#xff1a; 二、简单样例&#xff1a; 2.1 filter() 函数: 2.2 map() 函数: 2.3 sorted() 函数: 一、匿名函数&#xff08;也称为 lambda 函数&#xff09;&#xff1a; 简洁性&#xff1a;匿名函数通常比命…...

【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 发起业务请求 打开浏览器开发者工具&#xff0c;同时显示网络&#xff08;Internet&#xff09;和控制台&#xff08;console&#xff09; 接着&#xff0c;清空控制台和网络的内容&#xff0c;如下图 然后&#xff0c;点击你的业务按钮&#xff0c;发起请求。 首先看控制台…...

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…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

超短脉冲激光自聚焦效应

前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应&#xff0c;这是一种非线性光学现象&#xff0c;主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场&#xff0c;对材料产生非线性响应&#xff0c;可能…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统

医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上&#xff0c;开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识&#xff0c;在 vs 2017 平台上&#xff0c;进行 ASP.NET 应用程序和简易网站的开发&#xff1b;初步熟悉开发一…...

【入坑系列】TiDB 强制索引在不同库下不生效问题

文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建

制造业采购供应链管理是企业运营的核心环节&#xff0c;供应链协同管理在供应链上下游企业之间建立紧密的合作关系&#xff0c;通过信息共享、资源整合、业务协同等方式&#xff0c;实现供应链的全面管理和优化&#xff0c;提高供应链的效率和透明度&#xff0c;降低供应链的成…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

第25节 Node.js 断言测试

Node.js的assert模块主要用于编写程序的单元测试时使用&#xff0c;通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试&#xff0c;通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明

AI 领域的快速发展正在催生一个新时代&#xff0c;智能代理&#xff08;agents&#xff09;不再是孤立的个体&#xff0c;而是能够像一个数字团队一样协作。然而&#xff0c;当前 AI 生态系统的碎片化阻碍了这一愿景的实现&#xff0c;导致了“AI 巴别塔问题”——不同代理之间…...

Python爬虫(一):爬虫伪装

一、网站防爬机制概述 在当今互联网环境中&#xff0c;具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类&#xff1a; 身份验证机制&#xff1a;直接将未经授权的爬虫阻挡在外反爬技术体系&#xff1a;通过各种技术手段增加爬虫获取数据的难度…...