【C++并发编程】(三)互斥锁:std::mutex

2024-05-05 12:44
文章标签 c++ 互斥 并发 编程 std mutex

本文主要是介绍【C++并发编程】(三)互斥锁:std::mutex,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 互斥锁
    • 数据竞争
    • C++互斥锁
      • lock() 和 unlock()
      • std::lock_guard

互斥锁

数据竞争

在并发编程中,数据竞争是指多个线程同时对共享数据进行读写操作,并且至少有一个线程进行写操作,从而导致未定义的行为或结果。
下面给出一个例子:

#include <iostream>  
#include <thread>  int counter = 0; // 共享数据void increment() {  for (int i = 0; i < 100000; ++i) {  ++counter; // 修改被共享的数据}  
}  int main() {  std::thread th1(increment);  std::thread th2(increment);  th1.join();  th2.join();  // 预期输出是200000,但由于数据竞争,实际输出可能小于这个值  std::cout << "Final counter value: " << counter << std::endl;  // Final counter value: 107194return 0;  
}

两个线程t1t2可能同时对counter进行自增操作,导致counter的值增加的数量少于预期(即少于200000)。这是因为:

自增操作包含三个步骤:

  • 从内存中读取counter的当前值0。
  • 将这个值加1。
  • 将结果1写回内存中的counter。

如果两个线程同时执行这些步骤,可能会发生以下情况:

  • 线程t1读取counter的值为0。
  • 线程t2也读取counter的值为0(因为线程t1还没有将新的值写回内存)。
  • 线程t1将1写回counter。
  • 线程t2也将1写回counter(因为它之前读取的值是0)。

C++互斥锁

互斥锁(互斥量)能保护多个线程的共享资源不被同时访问。互斥锁的状态只有两种:开锁(unlocked)和闭锁(locked)。当一个任务(或线程)持有互斥锁,且该互斥锁处于闭锁状态,那么这个任务可以执行访问共享资源的代码,其他任务(或线程)则不被允许。开锁后,该线程将失去了对互斥锁的所有权,其他正在等待的线程现在有机会获取这个互斥锁,以访问共享资源。

在C++11中提供了std::mutex支持这一功能。std::mutex是一个简单的互斥锁类,它有两个主要操作:lock()unlock(),用于实现闭锁和开锁。

**lock():**当一个线程调用 lock() 函数时,它会尝试获取与该函数关联的互斥锁。如果互斥量当前没有被其他线程锁定(即它是可用的),则调用线程会成功获取锁,并继续执行其后续代码。
如果互斥量已经被其他线程锁定,则调用线程会被阻塞(即它会停止执行,直到锁变得可用)。
一旦线程获取了锁,它就可以安全地访问共享资源,而不必担心其他线程同时修改它。

unlock(): 当一个线程完成对共享资源的访问后,它应该调用 unlock() 函数来释放与该函数关联的互斥量(或其他锁)。释放锁允许其他线程获取该锁并访问共享资源。

在C++中,直接使用 lock()unlock() 函数来管理锁可能会导致一些问题,特别是当代码路径变得复杂时。为了简化锁的管理并减少出错的可能性,C++11引入了 std::lock_guardstd::unique_lock 等RAII(Resource Acquisition Is Initialization)风格的包装器,用于自动管理互斥锁的开锁和闭锁。其中,std::lock_guard 是最简单的锁包装器,它只提供了基本的锁定和解锁功能。 std::unique_lock 提供了比 std::lock_guard 更多的功能,更灵活,后续再单独介绍。

下面给出示例展示如何使用lock()unlock()以及锁包装器std::lock_guard 进行互斥锁的闭锁和开锁。

lock() 和 unlock()

#include <iostream>  
#include <thread>  
#include <mutex>  std::mutex mtx; // 创建一个互斥锁实例,用于保护共享数据
int counter = 0; // 共享数据 void increment() {  for (int i = 0; i < 100000; ++i) {  // 显式地调用了lock()和unlock()来分别获取和释放锁:mtx.lock(); // 获取锁  ++counter; // 递增操作  mtx.unlock(); // 释放锁  }  
}  int main() {  std::thread th1(increment);  std::thread th2(increment);  th1.join();  th2.join();  std::cout << "Final counter value: " << counter << std::endl;  // Final counter value: 200000return 0;  
}

std::lock_guard

#include <iostream>  
#include <thread>  
#include <mutex>  std::mutex mtx; // 互斥锁用于保护共享数据
int counter = 0; // 共享数据 void increment() {  std::unique_lock<std::mutex> lock(mtx); // 构造时自动获取锁  for (int i = 0; i < 100000; ++i) {  ++counter; // 递增操作,因为lock存在,所以此操作是安全的  }  // lock在离开作用域时自动释放锁  
}  int main() {  std::thread th1(increment);  std::thread th2(increment);  th1.join();  th2.join();  std::cout << "Final counter value: " << counter << std::endl;  // Final counter value: 200000return 0;  
}

这篇关于【C++并发编程】(三)互斥锁:std::mutex的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

Java JUC并发集合详解之线程安全容器完全攻略

《JavaJUC并发集合详解之线程安全容器完全攻略》Java通过java.util.concurrent(JUC)包提供了一整套线程安全的并发容器,它们不仅是简单的同步包装,更是基于精妙并发算法构建... 目录一、为什么需要JUC并发集合?二、核心并发集合分类与详解三、选型指南:如何选择合适的并发容器?在多

Java 结构化并发Structured Concurrency实践举例

《Java结构化并发StructuredConcurrency实践举例》Java21结构化并发通过作用域和任务句柄统一管理并发生命周期,解决线程泄漏与任务追踪问题,提升代码安全性和可观测性,其核心... 目录一、结构化并发的核心概念与设计目标二、结构化并发的核心组件(一)作用域(Scopes)(二)任务句柄

C++读写word文档(.docx)DuckX库的使用详解

《C++读写word文档(.docx)DuckX库的使用详解》DuckX是C++库,用于创建/编辑.docx文件,支持读取文档、添加段落/片段、编辑表格,解决中文乱码需更改编码方案,进阶功能含文本替换... 目录一、基本用法1. 读取文档3. 添加段落4. 添加片段3. 编辑表格二、进阶用法1. 文本替换2

C++中处理文本数据char与string的终极对比指南

《C++中处理文本数据char与string的终极对比指南》在C++编程中char和string是两种用于处理字符数据的类型,但它们在使用方式和功能上有显著的不同,:本文主要介绍C++中处理文本数... 目录1. 基本定义与本质2. 内存管理3. 操作与功能4. 性能特点5. 使用场景6. 相互转换核心区别

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

MySQL的JDBC编程详解

《MySQL的JDBC编程详解》:本文主要介绍MySQL的JDBC编程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、前置知识1. 引入依赖2. 认识 url二、JDBC 操作流程1. JDBC 的写操作2. JDBC 的读操作总结前言本文介绍了mysq

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法