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

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

相关文章

MySQL快速复制一张表的四种核心方法(包括表结构和数据)

《MySQL快速复制一张表的四种核心方法(包括表结构和数据)》本文详细介绍了四种复制MySQL表(结构+数据)的方法,并对每种方法进行了对比分析,适用于不同场景和数据量的复制需求,特别是针对超大表(1... 目录一、mysql 复制表(结构+数据)的 4 种核心方法(面试结构化回答)方法 1:CREATE

详解C++ 存储二进制数据容器的几种方法

《详解C++存储二进制数据容器的几种方法》本文主要介绍了详解C++存储二进制数据容器,包括std::vector、std::array、std::string、std::bitset和std::ve... 目录1.std::vector<uint8_t>(最常用)特点:适用场景:示例:2.std::arra

MySQL中的DELETE删除数据及注意事项

《MySQL中的DELETE删除数据及注意事项》MySQL的DELETE语句是数据库操作中不可或缺的一部分,通过合理使用索引、批量删除、避免全表删除、使用TRUNCATE、使用ORDERBY和LIMI... 目录1. 基本语法单表删除2. 高级用法使用子查询删除删除多表3. 性能优化策略使用索引批量删除避免

MySQL 数据库进阶之SQL 数据操作与子查询操作大全

《MySQL数据库进阶之SQL数据操作与子查询操作大全》本文详细介绍了SQL中的子查询、数据添加(INSERT)、数据修改(UPDATE)和数据删除(DELETE、TRUNCATE、DROP)操作... 目录一、子查询:嵌套在查询中的查询1.1 子查询的基本语法1.2 子查询的实战示例二、数据添加:INSE

Linux服务器数据盘移除并重新挂载的全过程

《Linux服务器数据盘移除并重新挂载的全过程》:本文主要介绍在Linux服务器上移除并重新挂载数据盘的整个过程,分为三大步:卸载文件系统、分离磁盘和重新挂载,每一步都有详细的步骤和注意事项,确保... 目录引言第一步:卸载文件系统第二步:分离磁盘第三步:重新挂载引言在 linux 服务器上移除并重新挂p

使用MyBatis TypeHandler实现数据加密与解密的具体方案

《使用MyBatisTypeHandler实现数据加密与解密的具体方案》在我们日常的开发工作中,经常会遇到一些敏感数据需要存储,比如用户的手机号、身份证号、银行卡号等,为了保障数据安全,我们通常会对... 目录1. 核心概念:什么是 TypeHandler?2. 实战场景3. 代码实现步骤步骤 1:定义 E

使用C#导出Excel数据并保存多种格式的完整示例

《使用C#导出Excel数据并保存多种格式的完整示例》在现代企业信息化管理中,Excel已经成为最常用的数据存储和分析工具,从员工信息表、销售数据报表到财务分析表,几乎所有部门都离不开Excel,本文... 目录引言1. 安装 Spire.XLS2. 创建工作簿和填充数据3. 保存为不同格式4. 效果展示5

Python多任务爬虫实现爬取图片和GDP数据

《Python多任务爬虫实现爬取图片和GDP数据》本文主要介绍了基于FastAPI开发Web站点的方法,包括搭建Web服务器、处理图片资源、实现多任务爬虫和数据可视化,同时,还简要介绍了Python爬... 目录一. 基于FastAPI之Web站点开发1. 基于FastAPI搭建Web服务器2. Web服务

MySQL 批量插入的原理和实战方法(快速提升大数据导入效率)

《MySQL批量插入的原理和实战方法(快速提升大数据导入效率)》在日常开发中,我们经常需要将大量数据批量插入到MySQL数据库中,本文将介绍批量插入的原理、实现方法,并结合Python和PyMySQ... 目录一、批量插入的优势二、mysql 表的创建示例三、python 实现批量插入1. 安装 PyMyS

关于MySQL将表中数据删除后多久空间会被释放出来

《关于MySQL将表中数据删除后多久空间会被释放出来》MySQL删除数据后,空间不会立即释放给操作系统,而是会被标记为“可重用”,以供未来插入新数据时使用,只有满足特定条件时,空间才可能真正返还给操作... 目录一、mysql数据删除与空间管理1.1 理解MySQL数据删除原理1.3 执行SQL1.3 使用