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

深入解析CryptoJS:AES加密与解密在前端安全传输中的实战应用

1. 为什么前端需要加密传输&#xff1f; 想象一下这样的场景&#xff1a;用户在登录页面输入账号密码&#xff0c;点击提交按钮后&#xff0c;这些敏感信息会以明文形式在网络中传输。如果被中间人截获&#xff0c;后果不堪设想。这就是为什么我们需要在前端对敏感数据进行加密…...

让Kindle电子书封面重获新生:开源工具Fix-Kindle-Ebook-Cover使用指南

让Kindle电子书封面重获新生&#xff1a;开源工具Fix-Kindle-Ebook-Cover使用指南 【免费下载链接】Fix-Kindle-Ebook-Cover A tool to fix damaged cover of Kindle ebook. 项目地址: https://gitcode.com/gh_mirrors/fi/Fix-Kindle-Ebook-Cover 深夜的阅读时光本该是惬…...

别再死记硬背了!ZBrush 2024最常用快捷键清单,附送一张桌面壁纸随时查

ZBrush 2024高效工作流&#xff1a;快捷键深度解析与实战应用指南 在数字雕塑的世界里&#xff0c;ZBrush早已成为行业标杆。但很多创作者在初次接触这款软件时&#xff0c;往往会被其复杂的快捷键系统所困扰。实际上&#xff0c;掌握快捷键并非简单的记忆游戏&#xff0c;而是…...

2026年最新盘点:全球TOP5高尔夫模拟系统公司,谁将引领行业新标准?

随着科技与体育的深度融合&#xff0c;室内高尔夫模拟系统已成为高尔夫爱好者、专业球员乃至商业场馆不可或缺的装备。它不仅打破了传统高尔夫运动对天气、场地和时间的严苛限制&#xff0c;更通过精准的数据分析&#xff0c;为技术提升提供了科学依据。面对市场上琳琅满目的品…...

SH_MLCD_J:Sharp HR-TFT内存液晶驱动库详解

1. 项目概述SH_MLCD_J 是一款专为驱动 Sharp 公司 HR-TFT 系列单色内存液晶显示屏&#xff08;Monochrome Memory LCD&#xff09;设计的嵌入式底层图形库。该库被广泛应用于秋月电子等国内元器件分销商所售的 SHARP 原厂模组&#xff0c;典型型号包括 LS013B7DH03、LS027B7DH0…...

AdminBSB表单组件实战:从基础到高级的完整解决方案

AdminBSB表单组件实战&#xff1a;从基础到高级的完整解决方案 【免费下载链接】AdminBSBMaterialDesign AdminBSB - Free admin panel that is based on Bootstrap 3.x with Material Design 项目地址: https://gitcode.com/gh_mirrors/ad/AdminBSBMaterialDesign Admi…...

基于单片机的井盖监测系统

摘 要 当前我国设计的井盖监测主要通过在井盖上放置标识等放置被盗&#xff0c;然后监测到被盗后&#xff0c;通过摄像头对其进行跟踪&#xff0c;导致当前还是存在很多井盖被盗&#xff0c;因此此次设计一款主要针对井盖防盗系统&#xff0c;监测到井盖移动时发送信息到管理人…...

Linux下CST8XX触摸屏驱动调试实战:从I2C波形异常到内核崩溃的完整解决记录

Linux下CST8XX触摸屏驱动调试实战&#xff1a;从I2C波形异常到内核崩溃的完整解决记录 在嵌入式Linux开发中&#xff0c;触摸屏驱动的调试往往是最具挑战性的环节之一。本文将详细记录CST8XX系列电容触摸屏在Linux平台上的完整调试过程&#xff0c;涵盖从硬件信号异常到内核崩溃…...

DLT Viewer全景指南:汽车电子日志分析的核心功能与实战应用

DLT Viewer全景指南&#xff1a;汽车电子日志分析的核心功能与实战应用 【免费下载链接】dlt-viewer Diagnostic Log and Trace viewing program 项目地址: https://gitcode.com/gh_mirrors/dl/dlt-viewer 在汽车电子开发的复杂环境中&#xff0c;诊断日志与追踪数据如同…...

剪映API技术解析:如何通过代码驱动实现视频剪辑自动化与效率革命

剪映API技术解析&#xff1a;如何通过代码驱动实现视频剪辑自动化与效率革命 【免费下载链接】JianYingApi Third Party JianYing Api. 第三方剪映Api 项目地址: https://gitcode.com/gh_mirrors/ji/JianYingApi 在视频内容创作进入工业化生产的今天&#xff0c;传统手动…...