在线程间共享数据---互斥量

2024-04-13 02:12

本文主要是介绍在线程间共享数据---互斥量,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在线程间共享数据—互斥量

std::mutex


std::mutex 是C++标准库中 <mutex> 头文件中定义的一个类,它是实现多线程同步的基础工具,主要用于保护共享资源,防止多个线程同时访问同一资源造成的数据竞争(race condition)。

std::mutex 表示一个互斥量(Mutex),它有两个主要的操作:

  1. 锁定(Locking):通过调用 lock() 函数来获取互斥锁。如果互斥量此时未被任何线程锁定,则当前线程获得该锁,否则线程将阻塞(即挂起),直到互斥量可用。

    std::mutex m;
    m.lock();
    // 在此处访问或修改受保护的资源
    m.unlock();
    
  2. 解锁(Unlocking):通过调用 unlock() 函数释放互斥锁,使得其他等待该锁的线程有机会获得锁并继续执行。

    m.unlock();
    

另外,std::mutex 还提供了诸如 try_lock()(尝试非阻塞地获取锁)和 lock_guardunique_lock 等辅助工具,帮助更安全、便捷地管理锁的生命周期。

std::lock_guard


std::lock_guard 是C++标准库 <mutex> 中的一个类,用于管理互斥锁(如 std::mutex)的生命周期,遵循RAII原则。它的主要目的是确保互斥锁在进入特定作用域时自动上锁,并在离开该作用域时自动解锁,从而有效地避免手动管理锁可能导致的问题,如忘记解锁、异常安全问题等。

使用 std::lock_guard 的典型方式如下:

#include <mutex>std::mutex mtx;void safe_operation() {std::lock_guard<std::mutex> guard(mtx); // 当guard创建时自动锁定mtx// 在此作用域内,互斥锁处于锁定状态,保证了对共享资源的独占访问// 对共享资源的访问和修改代码...
} // 当guard超出作用域被销毁时,会自动解锁mtx

在上述代码中,当 safe_operation 函数开始执行时,std::lock_guard 构造器会立即锁定 mtx 互斥量。然后,在 safe_operation 函数执行完毕(无论正常结束还是抛出异常)时,std::lock_guard 的析构函数会被自动调用,从而解锁互斥量。这样就确保了互斥锁始终会在适当的时候得到释放,提高了程序的健壮性和安全性。

std::unique_lock


std::unique_lock 是一个类,用于管理互斥量(如 std::mutexstd::recursive_mutex 等)的锁定和解锁操作。它遵循RAII(Resource Acquisition Is Initialization)原则,确保互斥量在适当的时间被锁定和解锁,即便在处理过程中遇到异常也能保证资源的安全性。

std::unique_lock 的特点和常见用法如下:

  1. 构造函数
    • 默认构造函数创建一个不关联任何互斥量的 std::unique_lock 对象。
    • 接收一个互斥量作为参数的构造函数会立即尝试锁定互斥量。
    • 通过传递一个额外的布尔标志 defer_locktry_to_lock 到构造函数,可以选择延迟锁定或尝试非阻塞锁定互斥量。
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx); // 直接锁定互斥量
std::unique_lock<std::mutex> deferred_lock(mtx, std::defer_lock); // 延迟锁定互斥量
std::unique_lock<std::mutex> try_lock(mtx, std::try_to_lock); // 尝试非阻塞锁定互斥量
  1. 方法

    • lock():锁定互斥量,如果互斥量已被锁定,则当前线程将阻塞,直到互斥量可用。
    • unlock():解锁互斥量。
    • try_lock():尝试非阻塞地锁定互斥量,若成功则返回 true,否则返回 false
    • owns_lock():检查 std::unique_lock 是否持有互斥锁。
  2. RAII 特性

    • std::unique_lock 对象离开作用域时,如果仍持有互斥锁,则会自动释放(解锁)互斥量,从而避免了手动解锁带来的风险。
  3. 迁移所有权

    • std::unique_lock 允许你转移对互斥量的锁定所有权,这意味着你可以将一个 std::unique_lock 对象的所有权转移到另一个对象,而原对象将不再拥有互斥锁。

举例:

std::mutex mtx;
{std::unique_lock<std::mutex> lock(mtx);// 在此作用域内,互斥量被锁定// 对共享资源的访问和修改...
} // lock离开作用域时,互斥量自动解锁// 或者使用try_lock和unlock
std::unique_lock<std::mutex> ul(mtx, std::try_to_lock);
if (ul.owns_lock()) {// 已经成功锁定互斥量,执行操作...
} else {// 未能锁定互斥量,执行其他操作...
}
ul.unlock(); // 手动解锁

总的来说,std::unique_lock 提供了一种安全、便捷的方式来管理互斥量的锁定和解锁,尤其适合用于那些需要保证资源安全访问的多线程环境。

避免死锁–std::lock


std::lock 是一个函数,用于同时锁定多个互斥量(如 std::mutexstd::recursive_mutex 等),并且保证所有互斥量都是以一种无死锁的方式进行锁定的。这对于需要同时保护多个资源的场景非常有用。
std::lock 函数在成功返回后,它尝试锁定的所有互斥量都将保持锁定状态。

使用 std::lock 的一般形式如下:

#include <mutex>
#include <vector>std::mutex mtx1, mtx2, mtx3; // 假设有多个需要同时锁定的互斥量void safe_multiple_access() {std::vector<std::unique_lock<std::mutex>> locks;locks.reserve(3); // 预先分配足够的空间// 使用std::lock同时锁定多个互斥量std::lock(mtx1, mtx2, mtx3);// 将锁定的互斥量放入unique_lock容器中以便后续解锁locks.emplace_back(mtx1, std::adopt_lock);locks.emplace_back(mtx2, std::adopt_lock);locks.emplace_back(mtx3, std::adopt_lock);// 在此作用域内,所有互斥量都已锁定,可以安全地访问相关资源// ...} // 退出作用域后,unique_lock对象会自动解锁互斥量

在上述代码中,std::lock 函数接受一个或多个互斥量作为参数,并尽可能按照最优化的顺序尝试锁定它们,如果无法同时锁定所有互斥量,函数将会一直阻塞直到可以成功锁定为止。这种方法可以避免死锁的发生,因为所有互斥量都会以一种一致的顺序尝试锁定。

需要注意的是,std::lock 并不会直接返回锁住的互斥量,因此通常配合 std::unique_lock 或者其他的 RAII机制来确保在适当的时候解锁互斥量。在上面的例子中,我们使用了 std::unique_lock 并设置了 std::adopt_lock 标志来接手已经由 std::lock 锁定的互斥量。当这些 std::unique_lock 对象离开作用域时,它们会自动解锁对应的互斥量。

单次初始化–std::once_flag

std::once_flag 是C++标准库 <mutex> 中的一个类,用于实现单次初始化(也称为一次初始化或惰性初始化)的线程安全机制。在多线程环境下,有时我们希望某些初始化操作只执行一次,不论有多少线程试图执行它。std::once_flagstd::call_once 函数配合使用,可以确保某个特定的初始化代码块在整个程序生命周期中仅被执行一次。

基本使用方式如下:

#include <mutex>std::once_flag init_flag; // 创建一个once_flag实例// 初始化函数
void initialize() {// 这里是初始化逻辑,只会被执行一次// ...
}// 在多线程环境中安全调用initialize函数
void thread_safe_init() {std::call_once(init_flag, initialize); // 如果尚未初始化,则执行initialize函数
}int main() {std::thread t1(thread_safe_init);std::thread t2(thread_safe_init);t1.join();t2.join();// 即使这里有更多线程调用 thread_safe_init,initialize 函数也只会执行一次return 0;
}

在这个例子中,std::once_flag 实例 init_flagstd::call_once 函数搭配,确保了 initialize 函数在整个程序运行期间只执行一次,不管有多少线程调用了 thread_safe_init 函数。这样就能在多线程环境下安全地进行单次初始化操作。

读写锁


  • std::shared_mutex

std::shared_mutex 是 C++14 引入的标准库中的一个类,用于实现读写锁(Read-Write Lock)的功能。读写锁允许在多线程环境中更高效地管理对共享资源的访问:

  • 当多个线程希望读取共享资源时,它们可以同时获取读取锁(shared lock),也就是多个读线程可以并发访问资源。
  • 当一个线程希望修改(写入)共享资源时,它必须获取写入锁(exclusive lock)。在写入锁被持有的情况下,任何其他试图获取读取锁或写入锁的线程都会被阻塞,直到写入锁被释放。

使用 std::shared_mutex 可以确保在任何时刻,最多只有一个线程持有写入锁,但可以有任意数量的线程持有读取锁。这样既能保障数据的一致性,又能充分利用多核处理器的优势,在读取密集型操作中提升并发性能。

  • std::shared_lock

std::shared_lock 是C++标准库 <shared_mutex> 中的一个类,用于管理读写锁(std::shared_mutex 或兼容的类型)的共享锁部分。共享锁允许多个线程同时读取(共享访问)受保护的资源,但不允许任何线程在同一时间写入(独占访问)资源。

使用 std::shared_lock 的好处在于它可以确保在多线程环境下,多个读取线程可以同时访问共享资源,而不需要互相等待。当线程不再需要读取资源时,std::shared_lock 会在析构时自动释放共享锁。

#include <shared_mutex>
#include <iostream>std::shared_mutex mtx;
int shared_data = 0;void reader() {std::shared_lock<std::shared_mutex> read_lock(mtx);// 在此作用域内,可以安全地读取 shared_datastd::cout << "Reader: " << shared_data << '\n';
}void writer() {std::unique_lock<std::shared_mutex> write_lock(mtx);// 在此作用域内,可以安全地修改 shared_data++shared_data;std::cout << "Writer: Updated to " << shared_data << '\n';
}int main() {std::thread reader_thread1(reader);std::thread reader_thread2(reader);// 确保读者线程已经开始读取std::this_thread::sleep_for(std::chrono::milliseconds(100));std::thread writer_thread(writer);reader_thread1.join();reader_thread2.join();writer_thread.join();return 0;
}

这篇关于在线程间共享数据---互斥量的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/898871

相关文章

详解如何使用Python构建从数据到文档的自动化工作流

《详解如何使用Python构建从数据到文档的自动化工作流》这篇文章将通过真实工作场景拆解,为大家展示如何用Python构建自动化工作流,让工具代替人力完成这些数字苦力活,感兴趣的小伙伴可以跟随小编一起... 目录一、Excel处理:从数据搬运工到智能分析师二、PDF处理:文档工厂的智能生产线三、邮件自动化:

Python数据分析与可视化的全面指南(从数据清洗到图表呈现)

《Python数据分析与可视化的全面指南(从数据清洗到图表呈现)》Python是数据分析与可视化领域中最受欢迎的编程语言之一,凭借其丰富的库和工具,Python能够帮助我们快速处理、分析数据并生成高质... 目录一、数据采集与初步探索二、数据清洗的七种武器1. 缺失值处理策略2. 异常值检测与修正3. 数据

pandas实现数据concat拼接的示例代码

《pandas实现数据concat拼接的示例代码》pandas.concat用于合并DataFrame或Series,本文主要介绍了pandas实现数据concat拼接的示例代码,具有一定的参考价值,... 目录语法示例:使用pandas.concat合并数据默认的concat:参数axis=0,join=

C#代码实现解析WTGPS和BD数据

《C#代码实现解析WTGPS和BD数据》在现代的导航与定位应用中,准确解析GPS和北斗(BD)等卫星定位数据至关重要,本文将使用C#语言实现解析WTGPS和BD数据,需要的可以了解下... 目录一、代码结构概览1. 核心解析方法2. 位置信息解析3. 经纬度转换方法4. 日期和时间戳解析5. 辅助方法二、L

使用Python和Matplotlib实现可视化字体轮廓(从路径数据到矢量图形)

《使用Python和Matplotlib实现可视化字体轮廓(从路径数据到矢量图形)》字体设计和矢量图形处理是编程中一个有趣且实用的领域,通过Python的matplotlib库,我们可以轻松将字体轮廓... 目录背景知识字体轮廓的表示实现步骤1. 安装依赖库2. 准备数据3. 解析路径指令4. 绘制图形关键

解决mysql插入数据锁等待超时报错:Lock wait timeout exceeded;try restarting transaction

《解决mysql插入数据锁等待超时报错:Lockwaittimeoutexceeded;tryrestartingtransaction》:本文主要介绍解决mysql插入数据锁等待超时报... 目录报错信息解决办法1、数据库中执行如下sql2、再到 INNODB_TRX 事务表中查看总结报错信息Lock

使用C#删除Excel表格中的重复行数据的代码详解

《使用C#删除Excel表格中的重复行数据的代码详解》重复行是指在Excel表格中完全相同的多行数据,删除这些重复行至关重要,因为它们不仅会干扰数据分析,还可能导致错误的决策和结论,所以本文给大家介绍... 目录简介使用工具C# 删除Excel工作表中的重复行语法工作原理实现代码C# 删除指定Excel单元

Linux lvm实例之如何创建一个专用于MySQL数据存储的LVM卷组

《Linuxlvm实例之如何创建一个专用于MySQL数据存储的LVM卷组》:本文主要介绍使用Linux创建一个专用于MySQL数据存储的LVM卷组的实例,具有很好的参考价值,希望对大家有所帮助,... 目录在Centos 7上创建卷China编程组并配置mysql数据目录1. 检查现有磁盘2. 创建物理卷3. 创

Nacos日志与Raft的数据清理指南

《Nacos日志与Raft的数据清理指南》随着运行时间的增长,Nacos的日志文件(logs/)和Raft持久化数据(data/protocol/raft/)可能会占用大量磁盘空间,影响系统稳定性,本... 目录引言1. Nacos 日志文件(logs/ 目录)清理1.1 日志文件的作用1.2 是否可以删除

使用Python获取JS加载的数据的多种实现方法

《使用Python获取JS加载的数据的多种实现方法》在当今的互联网时代,网页数据的动态加载已经成为一种常见的技术手段,许多现代网站通过JavaScript(JS)动态加载内容,这使得传统的静态网页爬取... 目录引言一、动态 网页与js加载数据的原理二、python爬取JS加载数据的方法(一)分析网络请求1