C++并发编程之共享数据(二)

3.1 条件竞争

恶性条件竞争通常发生于完成对多于一个的数据块的修改。例如对一个双向链表的结点的修改。该节点有两个指针。
避免条件竞争的两种方式:
方式一:确保只有进行修改的线程才能看到不变量被破坏时的中间状态。从其他访问线程的角度来
看,修改不是已经完成了,就是还没开始。
方式二:对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分
割的变化,也就是保证每个不变量保持稳定的状态,这就是所谓的无锁编程。

3.2 使用互斥量

不建议直接使用std::mutex,要保证在所有的函数出口调用unlock();C++标准库为互斥量提供了一个RAII语法的模板类 std::lock_guard ,在构造时就能提供已锁的互斥量,并在析构的时候进行解锁。

#include <list>
#include <mutex>
#include <algorithm>std::list<int> some_list; // 1
std::mutex some_mutex; // 2void add_to_list(int new_value)
{std::lock_guard<std::mutex> guard(some_mutex); // 3some_list.push_back(new_value);
}bool list_contains(int value_to_find)
{std::lock_guard<std::mutex> guard(some_mutex); // 4std::scoped_lock guard(some_mutex);return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}

3.2.1 互斥锁解决条件竞争

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

#include <list>
#include <mutex>
#include <algorithm>
#include <thread>
#include <numeric>
#include <vector>
#include <functional>class some_data
{int a;std::string b;
public:void do_something() {a += 1;}
};//这个类包装有一个数据,对这个数据进行保护
class data_wrapper
{
private:some_data data;//这个互斥锁是保护data数据的std::mutex m;public:template<typename Function>void process_data(Function func){std::lock_guard<std::mutex> l(m);func(data); // 1 传递“保护”数据给用户函数 当保护数据传递了出去,该保护数据失去了保护。}
};some_data* unprotected;void malicious_function(some_data& protected_data)
{unprotected = &protected_data;
}data_wrapper x;void foo()
{x.process_data(malicious_function); // 2 传递一个恶意函数unprotected->do_something(); // 3 在无保护的情况下访问保护数据
}

现在有一个问题是:即使使用了互斥量或其他机制保护了共享数据,就不必再为条件竞争所担忧吗
答案是否定的。考虑双链表的例子,为了能让线程安全地删除一个节点,需要确保防止对这三个节点(待删除的节点及其前后相邻的节点)的并发访问。如果只对指向每个节点的指针进行访问保护,那就和没有使用互斥量一样,条件竞争仍会发生。看这个例子:

template<typename T, typename Container = std::deque<T>>
class stack
{
public:explicit stack(const Container&);bool empty() const;size_t size() const;T top();void push(T const&);void push(T&&);void pop();
};

即使stack的私有数据都在互斥锁的保护之下且没有将私有数据传递出去,虽然empty()和size()可能在返回时是正确的,但其它的结果是不可靠的;当它们返回后,其他线程就可以自由地访问栈,并且可能push()多个新元素到栈中,也可能pop()一些已在栈中的元素。这样的话,之前从empty()和size()得到的结果就有问题了。

stack<int> s;
if (! s.empty()){ // 1
int const value = s.top(); // 2
s.pop(); // 3
do_something(value);
}

在1和2执行中间,可能有来自另一个线程的pop()调用删除了最后一个元素,对一个空栈操作会导致未定义行为。
导致该问题的是接口设计问题
std::stack 的设计人员将这个操作分为两部分:先获取顶部元素(top()),然后从栈中移
除(pop())的原因是:当将这两个操作合成一个的时候,如果我先将元素从栈中移除(这是一个节点,将指针指向修改一下),然后再复制该元素,当拷贝元素失败的时候,要弹出的数据消失了,但它的确从栈中移除了。
要解决上述提到的条件竞争有四种方式:
**方式一:**传入一个引用。

std::vector<int> result;//栈中的一个元素
some_stack.pop(result);

该方法的缺点是:需要临时构造一个节点的实例。构造函数可能需要参数,代码在这个阶段可能提供不了这个参数。还需要支持赋值操作。
方式二: 无异常抛出的拷贝构造函数或移动构造函数
将两个pop和top两个操作合二为一的时候可能出现拷贝元素失败的问题,如果该元素的拷贝构造函数和移动构造函数是无异常抛出就可以。
方式三: 返回元素的指针
这样就可以避免拷贝。
方式四: 1+2或者1+3
下面使用方式1和方式3写一个线程安全的堆栈类定义。

#include <list>
#include <mutex>
#include <algorithm>
#include <thread>
#include <numeric>
#include <vector>
#include <functional>
#include <deque>
#include <exception>
#include <memory> /*
* class empty_stack : std::exception
{
public:const char* what() const throw() {return "Empty stack exception: Cannot perform operation on empty stack";}
};
* 在C++11之前,throw()被用来指定函数不会抛出任何异常,如果函数抛出了异常,则会导致未定义行为。
在C++11中,应该使用noexcept关键字来代替throw()。在这种情况下,const char* what() const throw()声明了what()函数不会抛出异常。
这表示代码在执行what()函数时不会引发异常。这在异常处理中非常重要,因为如果在处理异常时又引发了新的异常,会导致程序终止。
*/
class empty_stack : std::exception
{
public:const char* what() const noexcept {return "Empty stack exception: Cannot perform operation on empty stack";}
};template<typename T>
class threadsafe_stack
{
private:std::stack<T> data;mutable std::mutex m;
public:threadsafe_stack(): data(std::stack<T>()) {}threadsafe_stack(const threadsafe_stack& other){std::lock_guard<std::mutex> lock(other.m);data = other.data; // 1 在构造函数体中的执行拷贝}threadsafe_stack& operator=(const threadsafe_stack&) = delete;void push(T new_value){std::lock_guard<std::mutex> lock(m);data.push(new_value);}//这两个pop操作的设计可以避免获取栈顶元素不对的问题。/** stack<int> s;if (! s.empty()){ // 1int const value = s.top(); // 2s.pop(); // 3do_something(value);}如果线程1执行完2后,线程3执行了2和3就会导致两个线程获取到了同一个值,但是pop了两次;使用下面的接口不会出现获取栈顶元素不对的问题。*/std::shared_ptr<T> pop(){std::lock_guard<std::mutex> lock(m);if (data.empty()) throw empty_stack(); // 在调用pop前,检查栈是否为空std::shared_ptr<T> const 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()) throw empty_stack();value = data.top();data.pop();}bool empty() const{std::lock_guard<std::mutex> lock(m);return data.empty();}
};int main()
{while (true);return 0;
}

线程不安全的stack对于pop接口的设计中,如果有下面的情况存在会出现问题。

stack<int> s;if (! s.empty()){ // 1int const value = s.top(); // 2s.pop(); // 3do_something(value);}

如果线程1执行完位置2后,线程2执行了位置2和位置3就会导致两个线程获取到了同一个值,但是pop了两次;使用线程安全的stack的接口不会出现获取栈顶元素不对的问题。

对于这句代码的理解:if (data.empty()) throw empty_stack();
旧的stack会导致未定义行为,而这个将会有异常抛出,用户可以自行决定该如何做。

3.2.2 死锁

存在两个锁或者两个以上的锁的时候,可能会出现死锁。死锁是两个线程互相等待,从而什么耶做不了。
避免死锁的一般建议,就是让两个互斥量总以相同的顺序上锁。

std::mutex是不可重入的互斥量,不能在同一线程中连续两次锁定。加几次锁必须解几次锁,不能多也不能少

举一个经典的死锁例子:

#include <iostream>
#include <thread>
#include <mutex>std::mutex m1;
std::mutex m2;void threadA()
{std::unique_lock<std::mutex> lock1(m1);std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 稍作延时,使线程B中的锁得到执行std::unique_lock<std::mutex> lock2(m2); // 线程A尝试获取锁m2
}void threadB()
{std::unique_lock<std::mutex> lock2(m2);std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 稍作延时,使线程A中的锁得到执行std::unique_lock<std::mutex> lock1(m1); // 线程B尝试获取锁m1
}int main()
{std::thread t1(threadA);std::thread t2(threadB);t1.join();t2.join();return 0;
}

3.2.2.1 使用std::lock同时加锁

#include <list>
#include <mutex>
#include <algorithm>
#include <thread>
#include <numeric>
#include <vector>
#include <functional>
#include <deque>
#include <exception>
#include <memory> // 这里的std::lock()需要包含<mutex>头文件
class some_big_object {};void swap(some_big_object& lhs, some_big_object& rhs) {}class X
{
private:some_big_object some_detail;std::mutex m;
public:X(some_big_object const& sd) :some_detail(sd) {}friend void swap(X& lhs, X& rhs){if (&lhs == &rhs) return;std::lock(lhs.m, rhs.m); // 1/*std::adopt_lock是一个标志,指示std::lock_guard对象构造函数不需要再次加锁互斥量。它假设互斥量已在当前线程上被提前锁定,std::lock_guard对象将负责互斥量的解锁*/std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);// 2std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);// 3swap(lhs.some_detail, rhs.some_detail);}
};

'if (&lhs == &rhs) return;'这句代码是必须的。如果没有这句代码,可能导致同一个线程获取两次一个互斥量。

std::lock 要么将两个锁都锁住,要不一个都不锁。
C++17提供了 std::scoped_lock<> 一种新的RAII类型模板类型,与std::lock_guard<> 的功能等价。 std::scoped_lock能接受不定数量的互斥锁类型作为模板参数。

swap函数重写如下:

void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::scoped_lock guard(lhs.m,rhs.m); // 1
swap(lhs.some_detail,rhs.some_detail);
}

3.2.2.2 避免死锁的建议

建议一:避免嵌套锁
一个线程已获得一个锁时,再别去获取第二个。当你需要获取多个锁,使用一
个 std::lock 来做这件事,避免产生死锁。
建议二:避免在持有锁时调用用户提供的代码
建议三:使用固定顺序获取锁
建议四:使用锁的层次结构

3.2.2.3 adopt_lock和defer_lock两个参数

class X
{
private:some_big_object some_detail;std::mutex m;public:X(some_big_object const& sd) :some_detail(sd) {}friend void swap(X& lhs, X& rhs){if (&lhs == &rhs)return;std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);// 1std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);// 1 std::defer_lock 留下未上锁的互斥量std::lock(lock_a, lock_b); // 2 互斥量在这里上锁swap(lhs.some_detail, rhs.some_detail);}friend void swap(X& lhs, X& rhs){if (&lhs == &rhs)return;std::lock(lhs.m, rhs.m); // 1std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);// 2std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);// 3swap(lhs.some_detail, rhs.some_detail);}
};

从这两个版本的swap函数中体会一下两个参数的区别;

3.3 仅在初始化过程中保护共享数据

3.3.1 分析几个代码

//用互斥实现线程安全的延迟初始化
std::shared_ptr<some_resource> resource_ptr;
void foo() {std::unique_lock<std::mutex> lk(resource_mutex);if(!resource_ptr){resource_ptr.reset(new some_resource);}lk.unlock();resource_ptr->do_something();
}

虽然安全,但是每次调用foo,线程都会等待获取锁,性能太低。

双重检查带来的问题:

void undefined_behaviour_with_double_checked_locking() {if (!resource_ptr) {//1std::lock_guard<std::mutex> lk(resource_mutex);if (!resource_ptr) {//2resource_ptr.reset(new some_resource);//3}}resource_ptr->do_something();//4
}

这段代码可能导致的未定义行为,分析如下:
指令重排问题:

  • 第一步: 为对象分配一片内存
  • 第二步: 构造一个对象,存入已分配的内存区
  • 第三步: 将指针指向这片内存区

但是有时候第二步和第三步会乱序,这是编译器优化的结果。
考虑如下场景: 当解锁发生了,3只做了第三步的操作,第二步操作还未进行,此时这个地址是一个野指针。当另一个线程进行do_something时就会出现未定义行为。

使用std::call_oncestd::once_flag解决这个问题。
代码如下:

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;void init_resource() {resource_ptr.reset(new some_resource);
}void foo() {std::call_once(resource_flag, init_resource);resource_ptr->do_something();
}

3.3.2 关于"C++11标准规定初始化只会在某一个线程上单独发生,在初始化完成之前,其他线程不会越过静态数据的声明而继续运行"的理解

C++11标准确实规定了在静态数据成员初始化之前,其他线程不能越过其声明继续运行。这个规定适用于静态数据成员和局部静态变量。

具体来说,C++11规定了在多线程环境下,静态变量(包括静态数据成员和局部静态变量)的初始化是线程安全的。每个线程只会执行一次静态变量的初始化,而其他线程在初始化完成之前会被阻塞。

例如,对于如下代码:

void foo() {static int x = 1;// ...
}

在多线程环境下,每个线程第一次调用foo()时,静态变量x的初始化会被分配给一个线程,其作用域仅限于那个线程。其他线程在初始化完成之前无法访问x,而只有在初始化完成后才能访问。

需要注意的是,对于普通的局部变量,C++标准没有规定其初始化的线程安全性。但是,大部分编译器会根据编译器的设定和优化策略来管理局部变量的初始化过程。在多线程环境下,如果确保局部变量的初始化线程安全性是必要的话,可以使用std::call_once或其他线程同步机制来保证。

3.3.3 基于std::call_once和std::once_flag实现一个单例模式

class Singleton {
private:static Singleton* instance;static std::once_flag initFlag;Singleton() {}public:static Singleton* getInstance() {std::call_once(initFlag, []() {instance = new Singleton();});return instance;}
};

3.4 读写锁

对于这样一种场景, 数据更新很少,但也是存在的,即一般都是读取,但有时也会写;
共享锁即读锁: std::shared_lock <std::shared_mutex>;
排他锁即写锁: 对应std::lock_guard<std::shared_mutex>和std::unique_lock<std::shared_mutex>

class dns_cache {std::map<std::string, dns_entry> entries;//mutablemutable std::shared_mutex entry_mutex;public:dns_entry find_entry(std::string const& domain) const {std::shared_lock<std::shared_mutex> lk(entry_mutex);std::map<std::string, dns_entry>::const_iterator const it =entries.find(domain);return (it == entries.end()) ? dns_entry() : it->second;}void update_or_add_entry(std::string const& domain, dns_entry const& dns_details) {std::lock_guard<std::shared_mutex> lk(entry_mutex);entries[domain] = dns_details;}
};

3.5 递归加锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/4313.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git 常用命令

Git操作 克隆远程仓库到本地 # Git初始化&#xff08;仅一次使用的适合需要设置&#xff09; git config --global user.name "用户名" git config --global user.email "邮箱账号"# 朴实无华的拉取 git clone <url>分支 # 查看分支 git branch # …

Spring Boot 配置属性设置优先级

文章首发地址 Spring Boot设计了非常特殊的加载指定属性文件&#xff08;PropertySource&#xff09;的顺序&#xff0c;以允许对属性值进行合理的覆盖。属性值会以下面的优先级进行设置。 home目录下的Devtools全局设置属性&#xff08;~/.spring-boot-devtools.properties&a…

【每日随笔】摩托车安全驾驶 ② ( 头盔选择 | 新手上路技巧 | 摩托车驾驶速度 | 保命法则 | 骑车心理态度 )

文章目录 一、摩托车安全驾驶0、头盔选择1、新手上路技巧2、摩托车驾驶速度3、保命法则4、骑车心理态度 重要的保命法则 : ① 不变道 : 前方 有车 , 就停车 , 等他走你再走 , 千万不要随意变道 , 摩托车变道很危险 , 一个不好就进骨科 , 而且还是你自己全责 ; 只有在 左转 ( …

Puppeteer 使用教程-实战篇(爬取图片、视频、音频,页面数据)

目录 前言 一、 获取实体店铺信息 二、 获取全国各省市县地图json数据 三、 cookies 四、 获取网络图片、视频资源 五、 自动化测试 总结 前言 续上篇&#xff0c;我们简单讲述一下puppeteer常见的应用场景&#xff0c;包括静态页面数据获取&#xff0c;网络请求获取截取…

第 5 章 Spark Shuffle 解析

第 5 章 Spark Shuffle 解析 5.1 Shuffle 的核心要点1. 数据分区&#xff1a;2.数据传输&#xff1a;3. 数据排序&#xff1a;4.数据聚合&#xff1a;5. 数据重分发&#xff1a;6.数据持久化&#xff1a;5.1.1 ShuffleMapStage 与 ResultStage 5.2 HashShuffle 解析5.2.1 未优化…

day7-三数之和

三数之和 力扣题目链接(opens new window) 给你一个包含 n 个整数的数组 nums&#xff0c;判断 nums 中是否存在三个元素 a&#xff0c;b&#xff0c;c &#xff0c;使得 a b c 0 &#xff1f;请你找出所有满足条件且不重复的三元组。 注意&#xff1a; 答案中不可以包含…

Appium python 框架

目录 前言 流程 结构 具体说说 run.py 思路 其他模块 前言 Appium是一个开源的移动应用自动化测试框架&#xff0c;它允许开发人员使用多种编程语言&#xff08;包括Python&#xff09;来编写自动化测试脚本。Appium框架提供了一套API和工具&#xff0c;可以与移动设备进…

基于单片机语音识别智能家居系统的设计与实现

功能介绍 以STM32单片机作为主控系统&#xff1b;液晶显示当前环境温湿度&#xff0c;用电器开关状态通过语音模块识别设定的语音&#xff1b;DHT11进行环境温湿度采集&#xff1b;通过语音播报模块报当前温湿度&#xff0c;智能回复通过语音识别可以打开灯&#xff0c;窗帘&am…

C语言-排序,初识指针

目录 【1】冒泡排序&#xff08;从小到大&#xff09; 【2】选择排序 【3】二维数组 【4】指针 【5】指针修饰 【6】大小端 【7】初见二级指针 练习&#xff1a; 【1】冒泡排序&#xff08;从小到大&#xff09; #include <stdio.h> //数组哪里的\0?自己和字符串…

Flink 在新能源场站运维的应用

摘要&#xff1a;本文整理自中南电力设计院工程师、注册测绘师姚远&#xff0c;在 Flink Forward Asia 2022 行业案例专场的分享。本篇内容主要分为四个部分&#xff1a; 建设背景 技术架构 应用落地 后续及其他 点击查看原文视频 & 演讲PPT 一、建设背景 建设背景主要…

Yalmip入门教程(3)-约束条件的定义

博客中所有内容均来源于自己学习过程中积累的经验以及对yalmip官方文档的翻译&#xff1a;https://yalmip.github.io/tutorials/ 之前的博客简单介绍了约束条件的定义方法&#xff0c;接下来将对其进行详细介绍。 首先简单复习一下&#xff1a; 1.定义约束条件可以使用矩阵拼接…

GRE和MGRE

目录 GRE GRE环境的搭建 MGRE MGRE的配置 MGRE环境下的RIP网络 MGRE实验 VPN 说到GRE&#xff0c;我们先来说个大家熟悉一点的&#xff0c;那就是VPN技术。 背景需求 企业、组织、商家等对专用网有强大的需求。 高性能、高速度和高安全性是专用网明显的优势。 物理专…

Notepad++ 配置python虚拟环境(Anaconda)

Notepad配置python运行环境步骤&#xff1a; 打开Notepad ->”运行”菜单->”运行”按钮在弹出的窗口内输入以下命令&#xff1a; 我的conda中存在虚拟环境 (1) base (2) pytorch_gpu 添加base环境至Notepad中 cmd /k chdir /d $(CURRENT_DIRECTORY) & call cond…

TX Barcode .NET for WPF Crack

TX Barcode .NET for WPF Crack 用于WPF软件的TX Barcode.NET包括一天完成的功能以及用于WPF的软件的2D条形码控制。 用于WPF的TX Barcode.NET的功能和属性&#xff1a; 它具有以下特性和属性&#xff0c;如&#xff1a; 常见的文字处理功能&#xff1a;它可以为用户和开发人员…

Spark和Hive概念

Spark介绍&#xff1a; Spark是一个开源的分布式数据处理引擎&#xff0c;最初由加州大学伯克利分校的AMPLab开发。它被设计用来处理大规模数据集&#xff0c;提供快速、通用、易用的数据处理框架。Spark能够在内存中快速处理数据&#xff0c;支持多种数据源&#xff0c;包括Ha…

FastEdit ⚡:在10秒内编辑大型语言模型

概述&#xff1a; 这个仓库旨在通过一个单一的命令&#xff0c;有效地将新鲜且定制化的知识注入到大型语言模型中&#xff0c;以辅助开发人员的工作。 支持的模型&#xff1a;○ GPT-J (6B)○ LLaMA (7B/13B)○ BLOOM (7.1B)○ Falcon (7B)○ Baichuan (7B/13B)○ InternLM (7…

Java语法和C#语法有哪些异同?

Java和C#是两种流行的面向对象编程语言&#xff0c;它们有许多相似之处&#xff0c;因为它们都受到C和面向对象编程的影响。但它们也有一些语法上的异同&#xff0c;让我们来看看它们的一些主要异同点&#xff1a; 相同点&#xff1a; 1、面向对象编程&#xff1a;Java和C#都…

stable diffusion windows本地搭建的坑

刚刚2小时前&#xff0c;我搭好了&#xff0c;欣喜若狂&#xff0c;开放端口&#xff0c;同事也尝试了。我的配置 16G内存&#xff0c;AMD卡&#xff0c;有gpu但是没有用。这里不说具体步骤&#xff0c;只说坑点。 首先就是安装gfpgan、clip、openclip问题&#xff0c;我参考…

MySQL的MVCC是否解决幻读

MySQL的MVCC是否解决幻读 MySQL事务隔离级别 ✓ 读未提交&#xff08;Read Uncommitted&#xff09;&#xff1a;最低的隔离级别&#xff0c;会读取到其他事务还未提交的内容&#xff0c;存在脏读。 ✓ 读已提交&#xff08;Read Committed&#xff09;&#xff1a;读取到的内容…

Linux/ubuntu 如何使用 SCP 和 SFTP 安全传输文件

本文章向大家介绍Linux如何使用 SCP 和 SFTP 安全传输文件&#xff0c;主要内容包括使用 SCP 复制文件、使用 SFTP 复制文件、总结、基本概念、基础应用、原理机制和需要注意的事项等&#xff0c;并结合实例形式分析了其使用技巧&#xff0c;希望通过本文能帮助到大家理解应用这…