C++并发编程2——为共享数据加锁(二)

C++并发编程 线程间的数据共享 1. 为什么要并发?分离关注点、提升性能 2. 何时不用并发?收益比不上成本。 启动线程存在固有开销 线程需要独立的堆栈空间 运行线程时,操作系统每次上下文切换都需要耗费时间 3. C++11支持的并发:全新的线程感知内存模型,以及管理线程、保护共享数据、线程同步和原子操作等 4. 线程:启动线程 、等待线程结束join() 或自主运行detached() (当线程退出时, 相关资源的能够正确回收) 5. 线程间共享数据 条件竞争(race condition):不变量遭到破坏(比如双向... 阅读详情

让复用变得容易,拒绝重复。

上一节说到,std::mutex并不能完全解决保护数据的问题。存在好几种情况,即使我们已经使用了互斥量,数据还是被破坏了。

  • 将被保护数据暴露到互斥量作用域之外
  • 被保护数据的访问接口本身就存在竞态条件(条件竞争)

不要暴露你的数据

来看下面例子:

struct protected_data
{
    char data[100];
}
class MutexTest
{
public:
    template<typename Function>
    void process(Function func)
    {
        std::lock_guard<std::mutex> guard<m_dataMutex>;        
        func(m_data); 
    }
private:
    std::mutex            m_dataMutex;
    struct protected_data m_data;    
}
struct protected_data *pData; 
void inject(Data &data)
{
    pData = &data;
}
// 即使process没有显式传出,但是还是被inject传出
// process执行完后,pData能在无锁的情况下访问数据
void Test()
{
    process(inject);
    for(int i = 0; i < 100; ++i)
    {
        pData.data[i] = i;
    }
}
std::thread mutexTestThread1(Test);
std::thread mutexTestThread2(Test);

我想不到比较好的例子来说明这个问题,上面的例子是基于C++并发编程上面改编的例子,其也能说明问题:在上锁后执行了用户定义的函数,将被保护数据传递到互斥锁作用域之外。

这个场景,mutexTestThread1解锁,mutexTestThread2上锁后,mutexTestThread2仍然无法独占被保护数据。pData总是获取到了被保护的数据,并在mutexTestThread2访问数据时修改该数据。

这种代码看起来很正常,也很不容易被发现,但是背后的错误逻辑是致命的,数据常常被莫名修改,奔溃也有可能随之而来。

切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中去。

谨慎的设计你的数据接口

来看下面例子:

std::deque<int> intDeque(1, 10);
std::stack<int> intStack(intDeque);
void Process()
{
    if(!intStack.empty())
    {
        const int value = intStack.top();
        intStack.pop();
    }
}
std::thread t1(Process);
std::thread t2(Process);

即使top()操作和pop()操作都已经上锁,也无法解决条件竞争的问题。

假设栈的实现中对数据的访问已加锁,在单线程情况下,上面程序可以无误执行,但是在多线程情况下,就有可能出现异常。调用空stack的top()是未定义行为。在多线程情况下,intStack.empty()操作获取的结果是不可靠的。

上述例子中intStack栈只有一个元素,如果线程t1和t2的执行顺序如下,就会出现未定义行为:

// example 1
t1: intStack.empty() // one element in intStack
t1: intStack.top()   // one element in intStack
t2: intStack.empty() // one element in intStack 
t1: intStack.pop()   // no element in intStack
t2: intStack.top()   // undefined behavior, intStack is empty()

即使不出现未定义行为,也有可能出现非预期行为——处理同一份数据多次:

// example 2
t1: intStack.empty() // one element in intStack
t2: intStack.empty() // one element in intStack 
t1: intStack.top()   // handle this data
t2: intStack.top()   // handle this data again

要解决上述问题,就需要接口设计上有较大的改动,最好的操作是重新设计接口

  • 1、重新设计接口实现:top()接口内提供异常机制,当栈大小为零时,抛出异常
  • 2、重新设计接口功能:将pop()和top()操作合并

第1种方案并不能解决example 2,所以推荐重新设计接口功能。一个线程安全的栈类定义如下:

template<typename T>
class Stack
{
private:
    std::stack<T>      m_data;
    mutable std::mutex m_mutex;
public:
    Stack(): m_data(std::stack<int>()){}
    Stack(const Stack& other)
    {
        std::lock_guard<std::mutex> lock(other.m);
        data = other.data;
    }
    Stack& operator=(const Stack&) = delete;
    void push(T new_value)
    {
        std::lock_guard<std::mutex> lock(m);
        data.push(new_value);
    }
    std::shared_ptr<T> pop()
    {
        std::lock_guard<std::mutex> lock(m);
        if(data.empty()) nullptr;
        const std::shared_ptr<T> res(std::make_shared<T>(data.top()));
        data.pop();
        return res; 
    }
    void pop(T& value)
    {
        std::lock_guard<std::mutex> lock(m);
        if(data.empty()) return nullptr;
        value=data.top();
        data.pop();
    }
    bool empty() const
    {
        std::lock_guard<std::mutex> lock(m);
        return data.empty();
    }
};

栈操作为什么需要先top()后pop(),而不直接pop()时返回数据?这是为了防止pop()时的拷贝操作失败,导致数据丢失。

如果不重新设计接口,在使用的时候加锁也能解决这个问题:

std::mutex stackMutex;
void Process()
{
    std::lock_gurad<std::mutex> guard(statckMutex);
    if(!intStack.empty())
    {
        const int value = intStack.top();
        intStack.pop();
    }
}

上述两种可能导致加锁失效的竞态条件场景,需要我们在组织代码或设计接口时精雕细琢,在很多场景下,提供线程安全的代码是很有必要的。

下一节,我们会死锁问题,即使没有加锁,也是有可能出现死锁,必须要按照一定的规范来涉及代码,才能有效的避免死锁问题。


关注微信公众号
C++并发编程--第三章--线程间共享数据 1. 线程间共享数据存在的问题 所有线程间共享数据的问题,都是修改数据导致的(竞争条件)。如果所有的共享数据都是只读的,就没问题,因为一个线程所读取的数据不受另一个线程是否正在读取相同的数据而影响。(引用https://blog.youkuaiyun.com/xy_cpp/article/details/80646443)。 2. 利用锁(mutex)保护共享数据 2.1 使用C++11中的std::mutex 在C++11中,你可以通过std::mutex的构造函数创建一个std::mutex实例,通过调用l 阅读详情

相关推荐

Python爬虫实战:避开巨潮资讯网反爬,稳定获取上市公司年报PDF下载地址

本文详细介绍了如何使用Python和Selenium突破巨潮资讯网的动态加载限制,稳定获取上市公司年报PDF下载地址。通过模拟真实用户操作、智能等待策略和反反爬虫技巧,帮助开发者高效爬取财经数据,适用于股票代码查询和年报分析等场景。

weixin_29065659的博客 1667

C++临界区mutex学习记录

头文件 #incldue <mutex> 主要用途 在多线程环境下,保证共享数据访问的正确性。 好的定义方式 mutable std::mutex m_mutex; 本质上mutex是一个值,mutable关键字使得mutex在常函数中也能改变值。 类的设计 如果设计一个类,而这个类是线程共享的资源,那么我们可以在类中加一个锁。如下: class Test { public: Test(){} ~Test(){} void lockMutex() { m_mutex.lock(

ccy0806的博客 949

从Landsat 8到哨兵2号:手把手教你用ENVI处理不同卫星数据的NDVI(附常见错误排查)

本文详细解析了从Landsat 8到哨兵2号的多源遥感数据NDVI计算全流程,包括不同卫星的波段设置差异、ENVI处理步骤及常见错误排查。通过实战指南和代码示例,帮助用户高效处理NDVI数据,特别针对哨兵2号数据的特殊处理提供了专业建议,适用于植被监测和遥感分析。

weixin_27741699的博客 330

C++ std::Thread多线程和mutex锁通俗易懂

多线程有多种方式。std::Thread、boost::Thread、pthread、Windows库等。本文只关注std::Thread,可以跨平台运行。

llllldm的博客 2396

DEMATEL-ISM模型的Python实现——方法介绍以及代码复现

本文参考文献与网络资料,对DEMATEL-ISM方法做了介绍何步骤描述,并给出Python代码进行了计算和绘图。

cros1的博客 2万+

FAST_LIO_ROS2 可以用gazebo仿真吗?

摘要:FAST_LIO_ROS2可与Gazebo仿真环境兼容,通过订阅Gazebo模拟的激光雷达和IMU数据实现SLAM建图。已有开源项目(如pb_rm_simulation)验证了该方案,支持导航模式切换与真实机器人移植。关键步骤包括环境配置、传感器话题匹配及参数调试,需注意版本兼容性与数据噪声处理。相关教程提供了从环境搭建到三维建图的完整流程。

专注 激光SLAM, 目标检测, 欢迎关注, 分享交流! 1138

【C++】11新特性:std::thread、std::mutex和两种RAII方式的锁封装

c++11引入了`std::thread`来创建线程,支持对线程`join`或者`detach`。`std::mutex`是一种线程同步的手段,用于保存多线程同时操作的共享数据。相比于以往的`pthread_mutex_t`方便了许多。c++11主要有`std::lock_guard`和`std::unique_lock`两种锁封装的方式,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。

qq_41950508的博客 1762

[QT笔记]QTableView与QTableWidget表头设置实战对比

本文深入对比了QT开发中QTableView与QTableWidget在表头设置上的核心差异与实践方法。QTableWidget提供开箱即用的便捷API,适合快速开发;而QTableView遵循模型-视图架构,表头数据由模型管理,虽设置稍复杂但灵活性与扩展性更强。文章通过实战代码示例,详细解析了两种组件的适用场景、性能考量及常见避坑指南,帮助开发者根据项目需求做出高效选择。

weixin_29018335的博客 322

C++并发编程实战:基于锁的并发数据结构

设计基于锁的并发数据结构的奥义就是,要确保先锁定合适的互斥,再访问数据,并尽可能缩短持锁时间。即使仅凭一个互斥来保护整个数据结构,其难度也不容忽视。我们在第3章已经分析过,需要保证不得访问在互斥锁保护范围以外的数据,且成员函数接口上不得存在固有的条件竞争。若针对数据结构中的各部分分别采用独立互斥,这两个问题就会互相混杂而恶化。另外,假使并发数据结构上的操作需要锁住多个互斥,则可能会引发死锁。所以,相比只用一个互斥的数据结构,如果我们考虑采用多个互斥,就需要更加谨慎。 我们将遵从上一节的指引,在这一节设计几

人邮异步社区 1067

JS逆向-Protobuf逆向解析

目录一、什么是Protobuf二、Protobuf环境配置三、Protobuf实例序列化与反序列化四、逆向解析 Protobuf案例1、python序列化2、python反序列化 一、什么是Protobuf 1、protocol buffers介绍:是一种语言无关、平台无关、可扩展的序列化结构数据的方法。严格说不算是加密,只能是叫序列化结构数据,让可读变为疑似的乱码,那反序列化即让疑似的乱码变为可读 2、protobuf使用流程的前提是有一个.proto文件,对逆向而言就是还原.proto文件,选择

十一姐的博客 1万+

YOLO26改进 - 注意力机制 | ParNet并行子网络:多分支协同优化特征表达,增强模型判别能

本文介绍了ParNet注意力机制及其在YOLO26中的应用。ParNet注意力通过并行子网络结构,将网络层组织成多个子网络并行处理输入特征,降低了传统注意力机制在处理长序列时的计算复杂度。该机制采用VGG风格的块和特征融合策略,具有低深度高性能、参数效率高、可扩展性强和并行化能力好等创新点。我们将ParNet注意力引入YOLO26,在检测头部分应用该机制。通过实验训练改进后的模型,有望提升YOLO26在目标检测任务中的性能。

魔改工程师的博客 443

C++11使用互斥量保护共享数据

C++中通过实例化std::mutex创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去调用unlock(),也包括异常的情况。C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个...

Keep Moving~ 2728

学Simulink--基于新能源与储能系统的场景实例:压缩空气储能(CAES)系统热力学建模

本文介绍了使用Simulink对压缩空气储能(CAES)系统进行热力学建模的详细流程。重点讲解了系统各模块的设计方法,包括压缩机、储气装置、膨胀机和热交换器等核心组件的参数设置与计算模型。文章还提供了控制策略设计和仿真运行的具体步骤,通过建立MATLAB函数计算关键参数变化,并分析了系统性能评估指标。该建模方法可用于验证CAES系统的设计有效性,并为优化储能效率提供技术参考。全文采用模块化设计思路,为新能源与储能系统的仿真研究提供了实用案例。

xiaoheshang_123的博客 298

C++并发编程 -2.线程间共享数据

本章就以在C++中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时,将共享数据的优势发挥到最大。

qq_45604814的博客 2003

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

C++并发编程实战 第三章 在线程间共享数据

private:public:// 1 在构造函数体中的执行拷贝// 在调用pop前,检查栈是否为空// 在修改堆栈前,分配出返回值data.pop();return res;data.pop();

qq_52758467的博客 530

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

C++并发编程实战 第三章 线程间共享数据

add_to_list() 和 list_contains() 函数使用 std::lock_guard 来确保对 some_list 的访问是互斥的,即一个线程在修改列表时,其他线程不能读取或修改该列表。除了上述特性外,std::unique_lock 还支持锁所有权的转移,这意味着你可以将一个 std::unique_lock 持有的锁转移给另一个 std::unique_lock 实例。因为两个线程可能尝试在相同的一对实例之间交换数据,但选择了相反的锁定顺序,从而导致死锁。

_ 839

《C++并发编程实战》精读总结:第三章 在线程间共享数据

多线程数据共享的问题多是由于对数据的改动引起的,要对共享的数据进行保护,比较直接的方法就是实用互斥和加锁。下面看一个简单的例子,使用到了互斥mutex,和对互斥加锁lock_guard<>。这里,假设一个线程调用add向stack1里添加元素时,如果另外一个线程调用top_value想要看stack顶部的元素是无法做到的,因为add已经锁住了互斥mutex1。注意,互斥标记的是代码。锁住互斥就是锁住标记代码涉及到的共享数据。

见牛羊的博客 299

C++ 加锁的原则

规则1 多线程、进程并行访问共享资源时,一定要加锁保护 说明:共享资源包括全局变量,静态变量,共享内存,文件等。 建议封装像智能指针一样的对象对锁进行管理,比如我们就封装了一个auto_lock,在构造时申请 锁,析构中释放锁,保证不会忘记“解锁”。如果锁的作用范围有限,则可以这样: do { auto_lock lock(&lock); //… }while(0); 规则2 锁的职责单一 说明:每个锁只锁一个唯一共享资源;这样,才能保证锁应用的单一,也能更好的确保加锁的范围尽 量小。

GoodLinGL的博客 2908

C++并发编程2——为保护数据加锁(一)

在应届生面试的时候,很多面试官都会问——“多线程如何共享资源”。在操作系统层面上可以给出若干关键词答案,但是在语言层面,这个问题考虑的就没有那么简单了。

小熊糖否——木子兮的自留地 6318

16.C++11线程—在线程间共享数据(笔记)

在并发编程中,操作由两个或多个线程负责,它们争先让线程执行各自的操作,而结果取决于它们执行的相对次序,所有这种情况都是条件竞争。有时候,这直观、易懂,因为诸多互斥的用途各异,也会出现棘手的状况,例如,运用多个互斥分别保护多个独立的实例,这些实例属于同一个类。可是,如果选用了固定的次序(两个对象通过参数传入,我们总是先给第一个实例的互斥加锁,再轮到第二个实例的互斥),前面的建议就适得其反:针对两个相同的实例,若两个线程都通过该函数在它们之间互换数据,只是两次调用的参数顺序相反,会导致它们陷入死锁!

C++方向学习笔记 940

《C++并发编程实战》之第3章 线程共享

c++ 并发编程 数据共享

yxqq378287007的博客 1770

C++并发编程之二 在线程间共享数据

C++并发之探索编程二实战

Awesomewan的博客 1966

C++并发编程-3.互斥与死锁

本文介绍如何使用互斥量保证共享数据的安全,并讲述死锁的相关处理方案。

yang_pi_pi的博客 1111

C++并发编程学习(三)——线程共享数据和互斥

比如一个栈对象,我们要保证其在多线程访问的时候是安全的,可以在判断栈是否为空,判断操作内部我们可以加锁,但是判断结束后返回值就不在加锁了,就会存在线程安全问题。C++线程库保证了,一旦有线程锁住了某个互斥,若其他线程试图再给它加锁,则须等待,直至最初成功加锁的线程把该互斥解锁。在作用域结束时自动调用其析构函数解锁,这么做的一个好处是简化了一些特殊情况从函数中返回的写法,比如异常或者条件不满足时,函数内部直接return,锁也会自动解开。如果多个线程共享数据,只要一个线程开始改动数据,就会带来很多隐患。

AutumnorLiu的博客 608
上一篇: C++并发编程2——为保护数据加锁(一)
下一篇: C++并发编程2——为共享数据加锁(三)
JerkWisdom
博客等级 码龄14年 70粉丝 76原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值