C++11线程间共享数据

news2024/9/21 20:47:06

C++11线程间共享数据

使用全局变量等不考虑安全的方式以及原子变量这里就不进行说明了。

在多线程中的全局变量,就好比现实生活中的公共资源一样,比如你有一个同时只能允许一个人做饭的厨房,那么在你占用期间,你的室友就必须等待。多线程的程序中,如果某个资源同时只允许一个线程占用,这时就出现了竞争条件。

竞争条件

假设你想看电影,影院有很多座位,当然有很多买票的人,假如你和另外一个人同时相中同一场次同一个座位,那这时你俩便存在竞争条件,因为同一场次同一个座位只能卖给一个人。

如果你写多线程程序,那么竞争条件将很容易出现。

避免出现竞争条件

解决竞争条件的方法有很多种,最简单的方法就是在物理上解决,保证每个线程都独享有一份数据相互之间不干扰。当然我们可以使用C++自带的原子变量或者自己实现线程安全数据类型来保证,这种方法准确的来说是伪避免。另外我们可以使用事务的方式确保变量修改唯一,我们可以将多个线程对一个变量的更改记录到log中最终由一个线程汇总提交结果,我们称这种方式为软件事务存储器software transactional memory(STM)

多线程中,保护共享数据最常用的方式还是使用互斥锁

互斥锁

线程间共享数据,首先要了解互斥锁(std::mutex)实现的数据共享,C++11提供了两种基本的锁类型

  • std::lock_guard,RAIL风格的互斥锁
  • std::unique_lock,RAIL风格的互斥锁,但是控制粒度更细,提供了更加友好的加锁解锁方式

RAII - cppreference.com

空类的作用

在mutex的定义中还提供了三个空数据结构用来控制加锁的方式,很多学习go的明白空结构体的妙用,其实空数据在C++中也有很多地方在巧妙的运用

三个空数据结构定义如下:

/// 使用该类型作为构造函数参数时,不对互斥锁加锁.
struct defer_lock_t { explicit defer_lock_t() = default; };
/// 尝试使用非阻塞的方式对互斥锁加锁 
struct try_to_lock_t { explicit try_to_lock_t() = default; };
/// 调用之前确保当前线程已经对互斥锁加锁了,调用之后接管互斥锁 
struct adopt_lock_t { explicit adopt_lock_t() = default; };
// 定义对应的const 变量
/// Tag used to prevent a scoped lock from acquiring ownership of a mutex.
_GLIBCXX17_INLINE constexpr defer_lock_t   defer_lock { };
/// Tag used to prevent a scoped lock from blocking if a mutex is locked.
_GLIBCXX17_INLINE constexpr try_to_lock_t  try_to_lock { };
/// Tag used to make a scoped lock take ownership of a locked mutex.
_GLIBCXX17_INLINE constexpr adopt_lock_t   adopt_lock { };

我们可以看下lock_guard的构造函数

// 只有一个入参
explicit lock_guard(mutex_type& __m) : _M_device(__m)
{ _M_device.lock(); }
// 入参有两个参数,其中第二个参数没有实际使用,只是起到按照参数来定位构造函数的作用
lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m)
{ } // calling thread owns mutex

当我们实际加锁时

// 只传一个参数
std::lock_guard<std::mutex> lock(mux);
// 调用的是
explicit lock_guard(mutex_type& __m) : _M_device(__m)
{ _M_device.lock(); }

// 当传入第二个参数时
std::lock_guard<std::mutex> lock(mux, std::adopt_lock);
// 调用的是 这样参数adopt_lock就起到了不加锁的目的
lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m)
{ } // calling thread owns mutex

传入的第二个实参并没有真正使用,只是起到定位函数的作用, 传入不同类型的参数根据函数重载规则就可以调用不同的函数,实现对不同构造函数的调用,使用空类能够将参数传递带来的影响降低到最小。

互斥锁的使用示例

使用全局锁保护全局list的安全

std::list<int> some_list;
std::mutex some_mutex;
// 线程1中调用
void add_to_list(int new_value)
{
    some_mutex.lock();
    some_list.push_back(new_value);
    some_mutex.unlock();
}
// 线程2中调用
bool list_contains(int value_to_find)
{
    some_mutex.lock();
    auto ret =  std::find(some_list.begin(),some_list.end(),value_to_find)
           != some_list.end();
    some_mutex.unlock();
    return ret;
}

封装多线程安全类,来保证线程间数据安全

class some_data {
    int a{};
    std::string b;
public:
    void do_something();
};

void some_data::do_something() {

}

class data_wrapper {
private:
    some_data data;
    std::mutex m;
public:
    template<typename Function>
    void process_data(Function func) {
        std::lock_guard<std::mutex> l(m);
        func(data);
    }
};

some_data *unprotected;

void malicious_function(some_data &protected_data) {
    unprotected = &protected_data;
}

data_wrapper x;

void foo() {
    x.process_data(malicious_function);
    unprotected->do_something();
}

std::lock

C++中求出变参函数变参个数,同样非模板函数也适用

// sizeof...(变参类型)  可求出变参函数变参个数
template<typename L1, typename... L3>
int Data(L1& _l1, L3&... _l3)
{
    return  (sizeof...(L3));
}

std::lock会尝试对给定的互斥锁进行上锁,没有顺序要求,其中任意一个互斥锁上锁失败都会导致lock阻塞

结合std::lock_guard使用

std::mutex mux1, mux2;
// mux1, mux2,进行加锁,任意一个失败都会导致阻塞
std::lock(mux1, mux2);
// 这里的guard不对互斥锁加锁,只是为了确保函数结束时会自动对互斥锁去锁
std::lock_guard<std::mutex> guard1(mux1, std::adopt_lock);
std::lock_guard<std::mutex> guard2(mux2, std::adopt_lock);

结合std::unique_lock使用

class X {
private:
    int data;
    std::mutex mux;
public:
    friend void swap(X& lhs, X& rhs) {
        if (&lhs == &rhs)
            return;
        // 延迟加锁
        std::unique_lock<std::mutex> lock_a(lhs.mux, std::defer_lock);
        std::unique_lock<std::mutex> lock_b(rhs.mux, std::defer_lock);
        // 在这里调用unique_lock对两个互斥锁进行加锁
        std::lock(lock_a, lock_b);
    }
};

在结合unique_lock使用时,会更加灵活,因为unique_lock本身实现了lock, unlock,并且支持defer_lock等。

std::lock_guard

std::lock_guard通常用来管理某个锁对象,方便线程对互斥变量的加解锁。在std::lock_guard的生命周期内会保持对某个锁对象的加锁,在std::lock_guard生命周期结束时会对管理的锁对象进行释放。需要注意的是,std::lock_guard并不负责mutex锁对象的生命周期,只是简化了mutex对象加锁和解锁的步骤。

std::unique_lock

std::lock_guard的优点是足够简单,缺点也是简单,使用std::lock_guard程序员不能对锁实现足够灵活的控制。因此std::unique_lock便应运而生,std::unique_lock除了实现std::lock_guard的功能,还增加了对互斥锁更加灵活的控制,程序员可以根据需要在任何地方实现对互斥锁加解锁的人为干预,std::unique_lock具体的实现如下:

// 默认构造函数,unique_lock定义时不对任何锁对象进行管理
unique_lock() noexcept: _M_device(0), _M_owns(false)
// 创建一个unique_lock对象,并对传入的mutex对象加锁
explicit unique_lock(mutex_type& __m)
: _M_device(std::__addressof(__m)), _M_owns(false)
{
    lock();
    _M_owns = true;
}
// 创建一个unique_lock对象,创建对象时不对mutex对象加锁,并且当前线程没有对mutex对象加锁,
// 在需要的地方需要手动加锁
unique_lock(mutex_type& __m, defer_lock_t) noexcept
: _M_device(std::__addressof(__m)), _M_owns(false)
{ }
// 创建一个unique_lock对象,并尝试对mutex对象进行加锁
unique_lock(mutex_type& __m, try_to_lock_t)
: _M_device(std::__addressof(__m)), _M_owns(_M_device->try_lock())
{ }
// 创建一个unique_lock对象,创建对象时不对mutex对象加锁,并且保证mutex是一个已经被当前线程持有锁的对象
unique_lock(mutex_type& __m, adopt_lock_t) noexcept
: _M_device(std::__addressof(__m)), _M_owns(true)
{
// XXX calling thread owns mutex
}
// 创建一个unique_lock对象,并持有锁到指定的时间点
template<typename _Clock, typename _Duration>
unique_lock(mutex_type& __m, const chrono::time_point<_Clock, _Duration>& __atime)
: _M_device(std::__addressof(__m)),
_M_owns(_M_device->try_lock_until(__atime))
{ }
// 创建unique_lock对象,并持有锁一段时间
 template<typename _Rep, typename _Period>
unique_lock(mutex_type& __m,
   const chrono::duration<_Rep, _Period>& __rtime)
: _M_device(std::__addressof(__m)),
_M_owns(_M_device->try_lock_for(__rtime))
{ }
// 不允许unique_lock对象赋值和使用一个unique_lock构造另外一个unique_lock对象
unique_lock(const unique_lock&) = delete;
unique_lock& operator=(const unique_lock&) = delete;
// 允许unique_lock对象右值传递
unique_lock(unique_lock&& __u) noexcept
: _M_device(__u._M_device), _M_owns(__u._M_owns)
// 允许unique_lock右值赋值
unique_lock& operator=(unique_lock&& __u) noexcept
// 支持手动加锁
void lock()
// 支持手动尝试加锁
bool try_lock()
// 支持手动尝试加锁到指定的时间点
template<typename _Clock, typename _Duration>
bool try_lock_until(const chrono::time_point<_Clock, _Duration>& __atime)
// 在指定的时间段内尝试加锁,加锁成功返回或者超出时间段返回
template<typename _Rep, typename _Period>
bool try_lock_for(const chrono::duration<_Rep, _Period>& __rtime)
// 支持在需要的地方进行解锁
void unlock()
// 将传入的unique_lock与当前对象进行互换
void swap(unique_lock& __u) noexcept
// 释放对mutex对象的控制,注意这里并不是释放unique_lock对象
mutex_type* release() noexcept
// 用来返回是否对某个锁对象已经加锁
bool owns_lock() const noexcept
// 用来返回是否对某个锁对象已经加锁
explicit operator bool() const noexcept
// 获取unique_lock控制的互斥锁对象
mutex_type* mutex() const noexcept

和lock_guard不同的是,std::unique_lock允许锁之间的传递,比如在两个函数中都需要一把锁中间不间断的一直保护到两个函数结束,那这个时候就可以借用std::unique_lock支持复制传递的特性来实现

std::mutex mux;
std::unique_lock<std::mutex> get_lock() {
    std::unique_lock<std::mutex> lock(mux);
    // do something
    return lock;
}

void process_data() {
    std::unique_lock<std::mutex> lock(get_lock());
    //do_something();
}

避免死锁的出现

避免持有锁的同时获取另外一把锁

死锁并不是只出现在互斥锁上,当你调用thread.join时也可能出现死锁。比如你在A线程中join B线程在B线程中join A线程,这时A需要等到B结束自己才能结束,B需要等到A结束自己才能结束,就会出现经典的AB锁。想要避免这种死锁也简单,就是不要在一个需要被主线程结束的线程中join其他线程。

如果是锁导致的AB锁,那么就不要获取一个锁之后再获取另外一个,如果你能确保在一个线程中同时只获取一个互斥锁,那么就不可能出现死锁的情况。但是有些情况下我们必须在获取一个锁的同时获取另外一个锁,这个时候就可以借用std::lock来管理互斥锁防止出现死锁的情况,常用方式见std::lock小节。

避免持有锁的同时调用用户提供的代码

用户代码中可能会做任何事情,如果你加锁之后调用用户代码,并且用户再在其代码中调用你其他加锁之后的函数接口,那么就有可能导致死锁的出现。

加锁之后调用用户的代码,那么用户代码中也有可能加锁,并且用户在调用你的代码时有可能也是先加锁再调用的,那么这个时候就有可能出现AB锁的情况。

按照统一的顺序加锁

如果你执行某个步骤时必须进行多次加锁,那么请在任何地方都按照固定顺序进行加锁,如在线程1中先加A锁再加B锁,那么在线程2中也要先加A锁再加B锁。

互斥锁的其他用法

  1. 使用互斥锁实现线程间共享数据
  2. 为了避免死锁可以考虑std::lock()或者boost::shared_mutex
  3. 要尽量保护更少的数据

std::lock用于保证使用多个锁而没有死锁的风险

需要注意的是, 当使

用 std::lock 去锁lhs.m或rhs.m时, 可能会抛出异常; 这种情况下, 异常会传播到 std::lock 之外。 当 std::lock 成功的获取一个互斥量上的锁, 并且当其尝试从另一个互斥

量上再获取锁时, 就会有异常抛出, 第一个锁也会随着异常的产生而自动释放, 所

以 std::lock 要么将两个锁都锁住, 要不一个都不锁

// 这里的std::lock()需要包含<mutex>头文件

class X
{
private:
    int some_detail;
    std::mutex m;
public:
    explicit X(int const& sd):some_detail(sd){}
    friend void swap(X& lhs, X& rhs)
    {
        if(&lhs==&rhs)
            return;
        std::lock(lhs.m,rhs.m); // 1
        std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
// 2
        std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
// 3
        swap(lhs.some_detail,rhs.some_detail);
    }
};
#include <mutex>
#include <thread>
 struct bank_account {
    explicit bank_account(int balance) : balance(balance) {}
    int balance;
    std::mutex m;
};
 void transfer(bank_account &from, bank_account &to, int amount)
{
    if(&from == &to) return; // avoid deadlock in case of self transfer

    // lock both mutexes without deadlock
    std::lock(from.m, to.m);
    // make sure both already-locked mutexes are unlocked at the end of scope
    std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
 // equivalent approach:
//    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
//    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
//    std::lock(lock1, lock2);

    from.balance -= amount;
    to.balance += amount;
}
 int main()
{
    bank_account my_account(100);
    bank_account your_account(50);

    std::thread t1(transfer, std::ref(my_account), std::ref(your_account), 10);
    std::thread t2(transfer, std::ref(your_account), std::ref(my_account), 5);

    t1.join();
    t2.join();
}

在这里插入图片描述

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

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

相关文章

synchronized锁的升级

synchronized锁优化的背景 用锁能够实现数据的安全性&#xff0c;但是会带来性能的下降 无锁能够基于线程并行提升程序性能&#xff0c;带来安全性的下降 java5 synchronized默认是重量级锁&#xff0c;java6以后引入偏向锁和轻量锁&#xff0c;java15 逐步废弃了偏向锁 …

机器学习实战(第二版)读书笔记(4)——seq2seq模型注意力机制(BahdanauAttention,LuongAttention)详解

一、Seq2seq模型 机器学习实战(第二版)读书笔记(1)——循环神经网络&#xff08;RNN&#xff09; 中详细介绍了RNN如下图1所示&#xff0c;可以发现RNN结构大多数对序列长度比较局限&#xff0c;对于机器翻译等任务(输入输出长度不想等N to M)&#xff0c;RNN没办法处理&…

SVN使用:Mac电脑中修改SVN输出信息为英文的方法

前言 作为软件开发人员&#xff0c;关于项目代码管理以及维护想必都不陌生&#xff0c;尤其是在团队协作的时候&#xff0c;多人开发维护同一个项目更是需要代码管理。关于项目代码管理维护工具&#xff0c;常用的就是Git、SVN等管理工具。本篇文章只来分享一下关于SVN的配置设…

C语言学习笔记-常量

“常量”的广义概念是&#xff1a;‘不变化的量’。例如&#xff1a;在计算机程序运行时&#xff0c;不会被程序修改的量。 以上是百度百科上对常量的部分定义。C语言的学习过程中将会接触很多的常量&#xff0c;不同类型的常量其定义、用法等会有所差异。要搞清楚他们的相似与…

如何恢复已删除的文件?5分钟搞定的简单方法。

本文介绍如何使用文件恢复程序恢复已删除的文件。它包括与恢复已删除文件相关的提示。 如何恢复已删除的文件 从硬盘驱动器恢复已删除的文件并不是一件疯狂的事情&#xff0c;但一旦您意识到文件已被删除&#xff0c;就尝试恢复会有所帮助。被删除的文件通常不会被真正删除&am…

终于有人把数据仓库讲明白了

数仓概念 ⚫ 数据仓库&#xff08;英语&#xff1a;Data Warehouse&#xff0c;简称数仓、DW&#xff09;,是一个用于存储、分析、报告的数据系统。 ⚫ 数据仓库的目的是构建面向分析的集成化数据环境&#xff0c;分析结果为企业提供决策支持&#xff08;Decision Support&am…

Linux入门教程|| Linux 忘记密码解决方法|| Linux 远程登录

很多朋友经常会忘记Linux系统的root密码&#xff0c;linux系统忘记root密码的情况该怎么办呢&#xff1f;重新安装系统吗&#xff1f;当然不用&#xff01;进入单用户模式更改一下root密码即可。 步骤如下&#xff1a; 重启linux系统 3 秒之内要按一下回车&#xff0c;出现如…

解决Error: Electron failed to install correctly, please delete......报错的问题

问题 在启动electron项目的时候&#xff0c;报mlgb错 Error: Electron failed to install correctly, please delete node_modules/electron and try installing again 搞了 好久 才解决 原因 升级Electron到7.0.0&#xff0c;提示Electron failed to install correctly, p…

python数据可视化开发(3):使用psutil和socket模块获取电脑系统信息(Mac地址、IP地址、主机名、系统用户、硬盘、CPU、内存、网络)

系列文章目录 python开发低代码数据可视化大屏&#xff1a;pandas.read_excel读取表格python实现直接读取excle数据实现的百度地图标注python数据可视化开发(1)&#xff1a;Matplotlib库基础知识python数据可视化开发(2)&#xff1a;pandas读取Excel的数据格式处理 文章目录系…

Linux下监控类命令:ps,du,top,df,free详解

Linux下监控类命令top命令top信息解释top参数使用ps命令ps信息解释ps参数使用du和dffree命令top命令 top命令&#xff0c;是Linux下常用的性能分析工具&#xff0c;能够实时显示系统中各个进程的资源占用状况&#xff0c;一般系统资源导致的崩溃问题可以使用top实时监控各进程…

魔兽世界服务器架设开服教程(巫妖王之怒外网详细教程)

魔兽世界服务器架设开服教程&#xff08;巫妖王之怒外网详细教程&#xff09;首先需要了解魔兽各个重要文件详细情况说明不管是任何一个魔兽世界GM想要将服务器修改成为自己想要的样子&#xff0c;首先要做的一件事情就是了解自己的服务器文件是说明意思&#xff0c;对于大多数…

2023年java面试之设计模式

1.什么是设计模式设计模式&#xff0c;是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。2.为什么要学习设计模式看懂源代码&#xff1a;如果你不懂设计模式去看Jd…

云原生周刊 | 使用 K8s 可视化工具集来调试业务 | 2023-1-30

开源项目推荐 k8z k8z 意在 K8s 业务层面&#xff0c;提供一个方便好用的 K8s 集群可视化工具集。目前包含以下功能&#xff1a; 终端&#xff1a;连接到集群任意 Pod 容器上&#xff0c;方便调试Tcpdump&#xff1a;对集群内容器进行 tcpdump 抓包&#xff0c;可直接展示抓…

七天实现一个go web框架

目录引流为什么要用web框架复习下net/http库以及http.Handler接口代码结构General.go启动&#xff01;上下文必要性封装前context.go拆分router封装后启动&#xff01;前缀树路由Trie 树目标实现前缀树修改router改变ServeHTTP实现分组控制Group对象的属性其余实现中间件实现其…

云计算|OpenStack|社区版OpenStack安装部署文档(三 --- 身份认证服务keystone安装部署---Rocky版)

一&#xff0c; 什么是keystone keystone是openstack的关键必选组件之一&#xff0c;其作用是提供身份认证服务&#xff0c;所有的身份认证信息都是存储在controller节点的数据库内。 具体的关于keystone的介绍可以参见官方文档&#xff1a;OpenStack Docs: Identity servic…

设备树中的pin 信息,是在什么时候被初始化的?

一、开发环境 SOC : IMX6ULL系统内核&#xff1a;4.1.15 二、问题描述 Linux 内核提供了pinctrl 和gpio 子系统用于GPIO 驱动。pinctrl_ctrl 子系统从设备树中获取pin 信息&#xff0c;然后配置pin复用 和pin电气特征&#xff08;上/下拉&#xff0c;速度&#xff0c;驱动能…

python小游戏——像素鸟代码开源

♥️作者&#xff1a;小刘在这里 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的&#xff0c;绽放&#xff0c;愿所有的美好&#…

特斯拉Q4财报:底部反弹70%,为信仰打call

北京时间2023年1月26日美股盘后&#xff0c;探案君的信仰之股——特斯拉公布了2022年第四季度财报&#xff0c;无论营收还是利润依然吊打华尔街预期&#xff0c;这就是特斯拉&#xff0c;当然这也很特斯拉&#xff01; 一、整体业绩&#xff0c;很特斯拉 营收方面&#xff1a…

JavaScript基础复盘4

JavaScript作用域 JavaScript作用域就是代码名字&#xff08;变量&#xff09;在某个范围内起作用和效果 目的是为了提高程序的可靠性&#xff0c;减少命名冲突。 JS没有块级作用域&#xff0c;{}内写的变量外部也可以使用。 作用域链 作用域链&#xff1a;内部函数访问外部函数…

拯救OOM~ 字节自研 Android 虚拟机内存管理优化黑科技 mSponge

本文描述的虚拟机内存管理优化方案&#xff0c;是从应用侧视角对 Android 虚拟机内存管理进行改造&#xff0c;优化了虚拟机对 LargeObjectSpace 的内存管理策略&#xff0c;间接增加其它内存空间使用上限。改造后的方案&#xff0c;32 位运行环境 LargeObjectSpace 的内存使用…