C++多线程:单例模式与共享数据安全(七)

1、单例设计模式
  • 单例设计模式,使用的频率比较高,整个项目中某个特殊的类对象只能创建一个

  • 并且该类只对外暴露一个public方法用来获得这个对象。

  • 单例设计模式又分懒汉式和饿汉式,同时对于懒汉式在多线程并发的情况下存在线程安全问题

    • 饿汉式:类加载的准备阶段就会将static变量、代码块进行实例化,最后只暴露一个public方法获得实例对象。

    • 懒汉式:当需要用到的时候再去加载这个对象。这时多线程的情况下可能存在线程安全问题

  • 对于饿汉式这里不做具体的解释,本节只讨论多线程与懒汉式的线程安全问题

2、单线程下的懒汉模式
2.1、单例对象的创建:
  • 将类指针对象进行静态私有化,并且在类外初始化这个对象为空;静态能保证的是这个对象属于这个类不属于任何一个对象
  • 私有化空构造器防止可以实例化对象
  • 对外暴露一个public方法获取该对象,如果在获取时发现该对象为空,那么进行实例化,否则直接返回
  • 因此可以看到实例化只有一次,多次获取到的对象的地址属于同一个
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void test1()
{
    Single_Instance *instance1 = Single_Instance::get_Instance();
    Single_Instance *instance2 = Single_Instance::get_Instance();
    instance1->func();
    instance2->func();
}

# 输出
func(), &instance = 0x5652eefede70
func(), &instance = 0x5652eefede70
2.2、单例对象的析构
  • 很明显上面的代码缺少一个析构函数,并且似乎无从下手找一个合适的时机对其进行析构,只能等待程序运行结束操作系统回收?
  • 其实可以通过内部类的方式进行析构
    • 首先在单例类内部进行私有化一个内部类
    • 对外暴露的public获取instance的对象接口在new实例化对象的时候创建一个内部类静态成员
    • 内部类静态成员的好处是只有一份
    • 当作用域结束时内部类就会负责析构掉主类的静态成员对象
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void test1()
{
    Single_Instance *instance1 = Single_Instance::get_Instance();
    Single_Instance *instance2 = Single_Instance::get_Instance();
    instance1->func();
    instance2->func();
}
#输出
func(), &instance = 0x558eb768de70
func(), &instance = 0x558eb768de70
inner_class::~inner_class(), 析构Single_Instance::instance对象
3、单例模式与多线程
  • 单例模式的对象可能会被多个线程使用,但是又必须保证这个单例的对象只有一份

  • 不能重复创建、也必须保证这个对象在多线程使用过程中不会因为创建而产生数据安全问题,即多线程抢占的创建这一个对象

class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test2()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}

在这里插入图片描述
可以看到实例化不止一个单例对象,这一现象违反了单例的思想,因此需要在多线程抢占创建时进行互斥(mutex)

3.1、解决方案(一)
  • 使用互斥量的方式,对线程访问获取对象进行阻塞
  • 但是不难发现问题,其实这个对象只创建一次,之后的访问单纯的获取这个对象也要进行加锁逐个排队访问临界区,这一现象导致效率极低
std::mutex mutex_lock;
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        std::unique_lock<std::mutex> uniqueLock(mutex_lock);
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test2()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}
3.2、解决方式(二)

双重检查机制(DCL)进行绝对安全解决

  • 双重检查:
    • 首先在锁外面加入一个if判断,判断这个对象是否存在,如果存在就没有必要上锁创建,直接返回即可
    • 如果对象不存在,首选进行加锁,然后在if判断对象是否存在,这个if的意义在于当多个线程阻塞在mutex锁头上时
    • 突然有一个线程1创建好了,那么阻塞在mutex锁头上的线程2、3、4…都不用再继续创建,因此在加一个if判断

这里还需要解释一下volatile关键字:

  • volatile关键字的作用是防止cpu指令重排序,重排序的意思就是干一件事123的顺序,cpu可能重排序为132

  • 为什么需要防止指令重排序,因为对象的new过程分为三部曲:

    (1)分配内存空间、(2)执行构造方法初始化对象、(3)将这个对象指向这个空间;

    由于程序运行CPU会进行指令的重排序,如果执行的指令是132顺序,A线程执行完13之后并没有完成对象的初始化、而这时候转到B线程;B线程认为对象已经实例化完毕、其实对象并没有完成初始化!产生错误

  • 但这个问题在C++11中已经禁止了重排序,因此不需要使用volatile关键字,但在Java和一些其他语言中可能有,Java中这个关键字是针对即时编译器JIT进行指令重排序的

static Single_Instance *get_Instance(){
    if(instance == NULL){
        std::unique_lock<std::mutex> uniqueLock(mutex_lock);
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
    }
    return instance;
}

只需要把上面的代码改成这个样子即可

4、std::call_once()
  • std::call_once()是C++11引入的函数,该函数的功能就是保证一个方法只会被调用一次。

    • 参数二:一个函数名func

    • 参数一:std::once_flag一个标记,本质是一个结构体。该标志可以用于标记参数二该函数是否已经调用过了

    • 参数三:参数二函数的参数

  • std::call_once()具有互斥量的这种能力,且效率上比mutex互斥量效率更高,因此也可以使用这个函数对单例的线程安全进行保证

    • 当call_once调用过一次之后,std::once_flag将会被修改标记(已调用),那么之后都不会在调用
  • 下面看个代码举例,可以看到create_Instance()函数中对于这个函数只执行了一次,完全ojbk。

class Single_Instance {
private:
    static Single_Instance *instance;
    static std::once_flag instance_flag;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:

    static void create_Instance(){
        instance = new Single_Instance();
        static inner_class innerClass;
    }
    static Single_Instance *get_Instance(){
        std::call_once(instance_flag, create_Instance);
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;
std::once_flag Single_Instance::instance_flag;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test3()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

稀碎从零算法笔记Day35-LeetCode:字典序的第K小数字

要考虑完结《稀碎从零》系列了哈哈哈 这道题和【LC.42 接雨水】&#xff0c;我愿称之为【笔试界的颜良&文丑】 题型&#xff1a;字典树、前缀获取、数组、树的先序遍历 链接&#xff1a;440. 字典序的第K小数字 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1…

el-upload上传图片图片、el-load默认图片重新上传、el-upload初始化图片、el-upload编辑时回显图片

问题 我用el-upload上传图片&#xff0c;再上一篇文章已经解决了&#xff0c;el-upload上传图片给SpringBoot后端,但是又发现了新的问题&#xff0c;果然bug是一个个的冒出来的。新的问题是el-upload编辑时回显图片的保存。 问题描述&#xff1a;回显图片需要将默认的 file-lis…

从0配置React

在本地安装和配置React项目&#xff0c;您可以使用create-react-app这个官方推荐的脚手架工具。以下是安装React的步骤&#xff0c;包括安装Node.js、使用create-react-app创建React应用&#xff0c;以及启动开发服务器。 下载安装node.js运行以下命令&#xff0c;验证Node.js…

施耐德 Unity Pro PLC 编程软件介绍

Unity Pro 软件基本介绍 Unity Pro 是施耐德中大型 PLC 的编程软件&#xff08;<–> 对应西门子 Step7&#xff09; 支持的 PLC&#xff1a;施耐德中大型 PLC 中型 PLC&#xff1a;Premium、M340&#xff08;<–> 对应西门子 S7-300、S7-1200&#xff09;大型 PL…

精读 Generating Mammography Reports from Multi-view Mammograms with BERT

精读&#xff08;非常推荐&#xff09; Generating Mammography Reports from Multi-view Mammograms with BERT&#xff08;上&#xff09; 这里的作者有个叫 Ilya 的吓坏我了 1. Abstract Writing mammography reports can be errorprone and time-consuming for radiolog…

C++项目——集群聊天服务器项目(十)点对点聊天业务

本节来实现C集群聊天服务器项目中的点对点聊天业务&#xff0c;一起来试试吧 一、点对点聊天业务 聊天服务器中一个重要的功能就是实现点对点聊天&#xff0c;客户端发送的信息包含聊天业务msgid、自身 的id和姓名、聊天对象的id号以及聊天信息&#xff0c;例如&#xff1a; …

uniapp uni.scss中使用@mixin混入,在文件引入@include 样式不生效 Error: Undefined mixin.(踩坑记录一)

问题&#xff1a; 在uni.scss文件定义mixin 2. 在vue文件引入: 3. 出现报错信息: 4. 问题思考&#xff1a; 是不是需要引入uni.scss &#xff1f; 答案不需要 uni.scss是一个特殊文件&#xff0c;在代码中无需 import 这个文件即可在scss代码中使用这里的样式变量。uni-app的…

ubuntu23.10配置RUST开发环境

系统版本: gcc版本 下载rustup安装脚本: curl --proto https --tlsv1.2 https://sh.rustup.rs -sSf | sh下载完成后会自动执行 选择默认安装选项 添加cargo安装目录到环境变量 vim ~/.bashrc 默认已添加 使用环境变量立即生效 source ~/.bashrc 执行rust开发环境,在终端输入…

Vitepress部署到GitHub Pages,工作流

效果&#xff1a; 第一步&#xff1a; 部署 VitePress 站点 | VitePress 执行 npm run docs:build&#xff0c;npm run docs:preview&#xff0c;生成dist文件 第二步&#xff1a; 手动创建.gitignore文件&#xff1a; node_modules .DS_Store dist-ssr cache .cache .temp *…

KMP哈希算法

KMP算法 KMP算法是一种字符串匹配算法&#xff0c;用于匹配模式串P在文本串S中出现的所有位置。 例如S“ababac”,P"aba",那么出现的所有位置是1 3 KMP算法将原本O&#xff08;n^2&#xff09;的字符串匹配算法优化到了O&#xff08;n&#xff09;&#xff0c;其精…

MySQL使用C语言连接

要使用C语言连接mysql&#xff0c;需要使用mysql官网提供的库&#xff0c;大家可以去MySQL官网下载。 1.引入库 1.选择Download Archivs 因为我们要连接的是C语言&#xff0c;所以选择Connector/C。 选择合适的版本下载&#xff0c;我这里 下载完之后&#xff0c;我们使用rz命…

AtCoder Beginner Contest 347 (ABCDEF题)视频讲解

A - Divisible Problem Statement You are given positive integers N N N and K K K, and a sequence of length N N N, A ( A 1 , A 2 , … , A N ) A(A_1,A_2,\ldots,A_N) A(A1​,A2​,…,AN​). Extract all elements of A A A that are multiples of K K K, divi…

鸿蒙HarmonyOS应用开发之HID DDK开发指导

场景介绍 HID DDK&#xff08;HID Driver Develop Kit&#xff09;是为开发者提供的HID设备驱动程序开发套件&#xff0c;支持开发者基于用户态&#xff0c;在应用层开发HID设备驱动。提供了一系列主机侧访问设备的接口&#xff0c;包括创建设备、向设备发送事件、销毁设备。 …

腾讯云2核4G服务器优惠价格165元一年,限制500GB月流量

腾讯云轻量2核4G5M服务器租用价格165元1年、252元15个月、三年900元&#xff0c;配置为轻量2核4G5M、5M带宽、60GB SSD盘、500GB月流量、上海/广州/北京&#xff0c;腾讯云优惠活动 yunfuwuqiba.com/go/txy 腾讯云轻量2核4G5M服务器租用价格 腾讯云&#xff1a;轻量应用服务器1…

SpringBoot + Vue3邮件验证码功能的实现

后端 SpringBootmavenmysqlIDEA 后端负责编写邮件发送的接口逻辑&#xff0c;具体流程如下: 引入相关依赖配置邮箱信息编写邮件发送服务接口OK 引入依赖 <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail --> <dependen…

基于FreeRTOS系统的STM32简易遥控器设计

项目说明 该项目是一个基于FreeRTOS系统的Stm32遥控器设计。使用该项目主要是自己学习FreeRTOS的使用&#xff0c;以及模块化编程的思想。这个项目应该长期会有更新。 项目开源 github:https://github.com/snqx-lqh/Stm32RemoteControl gitee:https://gitee.com/snqx-lqh/S…

conda 创建 python3.10.12 环境

conda 创建 python3.10.12 环境 介绍使用前置条件&#xff1a;安装 conda配置环境变量验证 Conda 安装结果创建环境&#xff1a;python激活 Anaconda 环境 验证 Python 版本。 介绍 Conda是一个开源的包管理和环境管理系统&#xff0c;由Continuum Analytics公司开发。它可以安…

【InternLM 实战营第二期笔记】InternLM1.8B浦语大模型趣味 Demo

体验环境 平台&#xff1a;InternStudio GPU&#xff1a;10% 配置基础环境 studio-conda -o internlm-base -t demo 与 studio-conda 等效的配置方案 conda create -n demo python3.10 -y conda activate demo conda install pytorch2.0.1 torchvision0.15.2 torchaudio2…

使用MySQL和PHP创建一个公告板

目录 一、创建表 二、制作首页&#xff08;创建主题以及显示列表&#xff09; 三、制作各个主题的页面&#xff08;输入回帖和显示列表&#xff09; 四、制作消息的查询界面 五、制作读取数据库信息的原始文件 六、制作数据重置页面 七、效果图 八、问题 1、目前无法处…

轻量应用服务器16核32G28M腾讯云租用优惠价格4224元15个月

腾讯云16核32G服务器租用价格4224元15个月&#xff0c;买一年送3个月&#xff0c;配置为&#xff1a;轻量16核32G28M、380GB SSD盘、6000GB月流量、28M带宽&#xff0c;腾讯云优惠活动 yunfuwuqiba.com/go/txy 活动链接打开如下图&#xff1a; 腾讯云16核32G服务器租用价格 腾讯…