单例及线程池的实现及感悟分享

news2025/1/21 17:54:30

        

碎碎念:

        有快3个月没有写博客了,这段时间很多事,抽不出空来写博客。后面的博客可能风格也会转变,从理论到实战为主,尽量减少源码的解读。(看完源码后面自己也忘了QAQ),本篇博客主要介绍线程池的实现及其前置单例模式的一些实现方法,其中使用了恋恋辰风大佬的一些例子。

单例模式

        在众多的设计模式中,尽管因为语言特性而使某些设计模式缺乏实用性。但是单例模式却是使用最为广泛的设计模式之一。在需要大规模数据通信的场景,又或者需要重复构造释放以及各种工厂模式,句柄。单例模式都在其中展现出了优势。例如,如果你想进行线程间通信,如果仅仅是处理轻量级的数据交换或同步任务,那么你可以选择使用future,task,promise等,但是如果是大规模数据,实现一个单例缓冲区Buffer,构造一个生产者消费者模型则是方法之一。值得一提的是,在观看boost库的实现中,我发现了一种写法可以些许提高性能,也就是消费者自身维护一个buffer,在消费完之后,直接和单例类中的buffer进行swap,而无需从单例buffer中取数据,这种方法可以减少锁的竞争和访问延迟。。单例模式一般常见的来说有懒汉式和饿汉式两种,懒汉式就是在getInstance函数执行的时候如果未创建单例,则创建单例。饿汉式则是将单例模式的创建放在了构造函数当中。如果是单线程无所谓,但是在多线程的情况下,我并不是很推荐使用懒汉式(即使他使用双锁)。这是因为new本身不是一个原子操作,他可能出现重排序问题从而先返回了但是还没实际构建,这样的风险非常大。而懒汉式由于是在构造函数中实现则不存在这个问题。这两种我都不是很常用,我常用的两种一种是返回局部变量,这种非常简单,而且可靠性较高,但是注意不要在C++11标准之前使用,会有风险,而11之后修复了这个问题。第二种是使用once_flag和call_once配合来实现,call_once会保证once_flag只被初始化一次。下面来看一下具体的实现,为了观看方便我就不分开写了:

#include<mutex>
#include<memory>

class SingleClass {

public:
	
	static SingleClass& getInstance() {
		static SingleClass instance;
		return instance;
	}
	static std::shared_ptr<SingleClass> getInstance2() {
		std::call_once(flag_, [&]() {
			instance_ = std::make_shared<SingleClass>();
			});
			return instance_;
	}
private:
	static std::once_flag flag_;
	SingleClass() = default;
	SingleClass(const SingleClass&) = delete;
	SingleClass& operator =(const SingleClass&) = delete;
	static std::shared_ptr<SingleClass>  instance_;
};

std::once_flag SingleClass::flag_;
std::shared_ptr<SingleClass> SingleClass::instance_ = nullptr;

        这里有几个注意的点,getInstance都要返回引用,一定一定一定要返回引用,不然他就是走的拷贝,我们已经删除了就过不了而且就算是过了也不能走拷贝。

        如果需要单例多可以写个模板:

#pragma once
#include <memory>
#include <mutex>
#include <iostream>
using namespace std;

template<typename T>
class Singleton{

public:
    static shared_ptr<T> getInstance(){
        static std::once_flag s_flag;
        std::call_once(s_flag,[&]{
            instance_ = make_shared<T>();
        });
        return instance_;
    }
    
protected:
    Singleton()=default;
    Singleton(const Singleton& ) = delete;
    Singleton& operator= (const Singleton& ) = delete;
    static shared_ptr<T> instance_; 
};
template<typename T>
shared_ptr<T> Singleton<T>::instance_ = nullptr;

这个只要继承就好了,他比较好的就是继承之后不用删除拷贝构造和重写赋值了,因为构造由内而外,先构造父类,父类那里直接禁了子类也用不了这俩了。但是需要注意的是,如果子类自身的构造函数放在private里记得把这个模板设为友元,不然拿不到构造函数。

线程池

        线程池的主要功能是管理线程的生命周期,以减少频繁创建和销毁线程的开销。在线程池创建时,会启动一定数量的线程,并让这些线程通过任务队列来获取待执行的任务。工作线程会不断地循环获取任务并执行,从而提高了任务处理的效率。线程池在销毁时,确保所有子线程都完成任务后才退出,这一点非常重要。如果主线程在子线程还未完成任务时就退出,可能会导致未完成的任务丢失或程序状态不一致。因此,在设计线程池时,必须确保在主线程退出之前,所有子线程的任务都已经处理完毕。在多线程程序设计中,通常需要确保主线程在所有子线程完成任务后才退出。线程池通过管理工作线程的生命周期和同步机制(如使用`join`或条件变量)确保所有任务完成后,主线程才能安全退出。我们来看一下主要的实现,这里使用了恋恋辰风博主写的一个线程池,只完成了主要功能。也便于理解:

线程池主要代码

#ifndef __THREAD_POOL_H__
#define __THREAD_POOL_H__

#include <atomic>
#include <condition_variable>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>

class NoneCopy {

public:
    ~NoneCopy(){}
protected:
    NoneCopy(){}
private:
    NoneCopy(const NoneCopy&) = delete;
    NoneCopy& operator=(const NoneCopy&) = delete;
};

class ThreadPool : public NoneCopy {
public:
    //继承基类NoneCopy就不需要写如下删除了
    //ThreadPool(const ThreadPool&) = delete;
    //ThreadPool& operator=(const ThreadPool&) = delete;

    static ThreadPool& instance() {
        static ThreadPool ins;
        return ins;
    }

    using Task = std::packaged_task<void()>;


    ~ThreadPool() {
        stop();
    }

    template <class F, class... Args>
    auto commit(F&& f, Args&&... args) -> 
        std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {
        using RetType = decltype(std::forward<F>(f)(std::forward<Args>(args)...));
        if (stop_.load())
            return std::future<RetType>{};

        auto task = std::make_shared<std::packaged_task<RetType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        std::future<RetType> ret = task->get_future();
        {
            std::lock_guard<std::mutex> cv_mt(cv_mt_);
            tasks_.emplace([task] { (*task)(); });
        }
        cv_lock_.notify_one();
        return ret;
    }

    int idleThreadCount() {
        return thread_num_;
    }

private:
    ThreadPool(unsigned int num = std::thread::hardware_concurrency())
        : stop_(false) {
            {
                if (num <= 1)
                    thread_num_ = 2;
                else
                    thread_num_ = num;
            }
            start();
    }
    void start() {
        for (int i = 0; i < thread_num_; ++i) {
            pool_.emplace_back([this]() {
                while (!this->stop_.load()) {
                    Task task;
                    {
                        std::unique_lock<std::mutex> cv_mt(cv_mt_);
                        this->cv_lock_.wait(cv_mt, [this] {
                            return this->stop_.load() || !this->tasks_.empty();
                            });
                        if (this->tasks_.empty())
                            return;

                        task = std::move(this->tasks_.front());
                        this->tasks_.pop();
                    }
                    this->thread_num_--;
                    task();
                    this->thread_num_++;
                }
                });
        }
    }
    void stop() {
        stop_.store(true);
        cv_lock_.notify_all();
        for (auto& td : pool_) {
            if (td.joinable()) {
                std::cout << "join thread " << td.get_id() << std::endl;
                td.join();
            }
        }
    }

private:
    std::mutex               cv_mt_;
    std::condition_variable  cv_lock_;
    std::atomic_bool         stop_;
    std::atomic_int          thread_num_;
    std::queue<Task>         tasks_;
    std::vector<std::thread> pool_;
};

#endif  // !__THREAD_POOL_H__

       继承

class NoneCopy {

public:
    ~NoneCopy(){}
protected:
    NoneCopy(){}
private:
    NoneCopy(const NoneCopy&) = delete;
    NoneCopy& operator=(const NoneCopy&) = delete;
};

        这里继承也就是我们前面说的模板继承方式,不过他并没有在模板类中创建getinsance而是禁止拷贝构造和拷贝赋值。往下看

    static ThreadPool& instance() {
        static ThreadPool ins;
        return ins;
    }

    using Task = std::packaged_task<void()>;


    ~ThreadPool() {
        stop();
    }

        第一个没什么好说的 ,析构也没什么好说的,主要看第二个Task,他可不是乱写的哈,这里是因为投递的任务返回类型不同,参数也不同统一拿void()包装一下,内部函数就是通过Bind绑定过参数的任务了,直接调用就好了。对了这里要给不太了解的朋友说一下,这个packaged_task可以和future绑定,只要返回future,使用线程池调用者就可以通过future拿到函数运行后的返回值。接下来看看构造函数:

构造函数

ThreadPool(unsigned int num = std::thread::hardware_concurrency())
        : stop_(false) {
            {
                if (num <= 1)
                    thread_num_ = 2;
                else
                    thread_num_ = num;
            }
            start();
    }

        这里也没什么好说的,建议是开的线程数等于你的CPU核数。过多的线程会造成线程竞争,浪费时间片。但是1核我们也不能让他太寒碜变成单线程了吧,至少开两个吧。。。

 start函数

    void start() {
        for (int i = 0; i < thread_num_; ++i) {
            pool_.emplace_back([this]() {
                while (!this->stop_.load()) {
                    Task task;
                    {
                        std::unique_lock<std::mutex> cv_mt(cv_mt_);
                        this->cv_lock_.wait(cv_mt, [this] {
                            return this->stop_.load() || !this->tasks_.empty();
                            });
                        if (this->tasks_.empty())
                            return;

                        task = std::move(this->tasks_.front());
                        this->tasks_.pop();
                    }
                    this->thread_num_--;
                    task();
                    this->thread_num_++;
                }
                });
        }
    }

        这也就是线程就射设置线程,让他循环的执行任务,{}作用域的作用主要是提前结束对共享资源的掌控,要是想用延迟锁也可以,直接让他加完锁再解锁,只是这种方便一点。而且对于lock_guard这种不支持加锁解锁的,也可以控制加锁范围。非常方便十分推荐。这里需要注意的是如果他等待挂起的时候如果是准备退出了,那么他就处理完自己的函数就退出了。忘了说了这里一定要用emlace_back。因为push_back其实是拷贝,那就会存在两个线程一个还回收不掉。

stop函数

    void stop() {
        stop_.store(true);
        cv_lock_.notify_all();
        for (auto& td : pool_) {
            if (td.joinable()) {
                std::cout << "join thread " << td.get_id() << std::endl;
                td.join();
            }
        }
    }

       这里也没什么好说的,就是设置退出标志服,然后唤醒所有挂起线程,再回收。

Commit函数

        这是线程池中最关键的函数,为什么不同返回值什么不同的参数函数可以投递到一个队列中.我们来看一下。

template <class F, class... Args>
    auto commit(F&& f, Args&&... args) -> 
        std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {
        using RetType = decltype(std::forward<F>(f)(std::forward<Args>(args)...));
        if (stop_.load())
            return std::future<RetType>{};

        auto task = std::make_shared<std::packaged_task<RetType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        std::future<RetType> ret = task->get_future();
        {
            std::lock_guard<std::mutex> cv_mt(cv_mt_);
            tasks_.emplace([task] { (*task)(); });
        }
        cv_lock_.notify_one();
        return ret;
    }

        首先他使用了auto来确定返回值的类型,怎么做到的呢?

auto commit(F&& f, Args&&... args) 
-> std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))>

这个首先他返回的是一个future嘛,这样调用者才可以通过future拿到结果。里面就是用decltype确定类型,什么类型呢?f函数调用的类型。后面args是参数,...是展开。这样就可以输入多个参数然后确定类型了。注意这里一定要用完美转发来确保他的参数左值右值性质不变。往后就是如果线程准备退出了就直接return。

        auto task = std::make_shared<std::packaged_task<RetType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        std::future<RetType> ret = task->get_future();
        {
            std::lock_guard<std::mutex> cv_mt(cv_mt_);
            tasks_.emplace([task] { (*task)(); });
        }

        然后task用bind进行绑定参数,这样就可以不输入参数直接调用了,但是他还有返回值怎么办呢?那就用lambda表达式直接包装一层就行了,注意这里一定要捕获task要不就&,最好不要捕获this。不然会导致潜在的访问悬空指针的问题,除非你确保this在任务执行期间依然有效。就算他是单例模式,在主程序挂掉之前不会失效,我仍然还是建议不要捕获this。然后往下直接唤醒返回就行了。

结语

        放假之后抽空写了一下自己的一些理解和感悟,希望后面能找一个好一点的实习。后面主要会写车载网络的一些知识,也穿插一下和普通TCP/IP的区别,DDS和SOME/IP的一些实战。源码还是等工作后有空再写吧。

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

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

相关文章

金融项目实战 07|Python实现接口自动化——连接数据库和数据清洗、测试报告、持续集成

目录 一、投资模块&#xff08;投资接口投资业务&#xff09; 二、连接数据库封装 和 清洗数据 1、连接数据库 2、数据清洗 4、调用 三、批量执行测试用例 并 生成测试报告 四、持续集成 1、代码上传gitee 2、Jenkin持续集成 一、投资模块&#xff08;投资接口投资业务…

Video-RAG:一种将视频RAG新框架

1. 摘要及主要贡献点 摘要&#xff1a; 检索增强生成&#xff08;RAG&#xff09;是一种强大的策略&#xff0c;通过检索与查询相关的外部知识并将其整合到生成过程中&#xff0c;以解决基础模型生成事实性错误输出的问题。然而&#xff0c;现有的RAG方法主要集中于文本信息&…

2024嵌入式系统的未来发展与技术洞察分享

时间如白驹过隙&#xff0c;不知不觉又是一年&#xff0c;这一年收获满满。接下来&#xff0c;将本年度对技术的感悟和洞察分析如下&#xff0c;希望对大家有所帮助。 在过去几十年里&#xff0c;嵌入式系统技术迅速发展&#xff0c;成为现代电子设备和智能硬件的核心组成部分。…

对人型机器人的研究和展望

目录 概述 1 核心软硬件部件 1.1 运动控制部分 1.1.1 减速机 1.1.2 编码器 1.1.3 直流无刷电机 1.2 智能仿生手 1.3 控制板卡 2 人型机器人的应用 3 未来展望 概述 如果现在有人问&#xff1a;当前那个行业最火&#xff1f;毫无疑问答案肯定是人型机器人了。当前各类机…

创建 pdf 合同模板

创建 pdf 合同模板 一、前言二、模板展示三、制作过程 一、前言 前段时间要求创建“pdf”模板&#xff0c;学会了后感觉虽然简单&#xff0c;但开始也折腾了好久&#xff0c;这里做个记录。 二、模板展示 要创建这样的模板 三、制作过程 新建一个“Word”&#xff0c;这里命…

欧拉(Euler 22.03)安装ProxySQL

下载离线安装包 proxysql-2.0.8-1-centos7.x86_64.rpm 链接: https://pan.baidu.com/s/1R-SJiVUEu24oNnPFlm9wRw 提取码: sa2w离线安装proxysql yum localinstall -y proxysql-2.0.8-1-centos7.x86_64.rpm 启动proxysql并检查状态 systemctl start proxysql 启动proxysql syste…

Comsol 空气耦和超声表面波法检测PMMA表面裂纹

空气耦合超声表面波法是一种常用于检测材料表面裂纹的无损检测技术。下面是一些步骤&#xff0c;您可以使用这种方法来检测PMMA&#xff08;聚甲基丙烯酸甲酯&#xff09;表面裂纹&#xff1a; 1. 准备工作&#xff1a;准备一台超声波检测设备&#xff0c;包括超声发射器和接收…

Unsafe

1. 概念介绍 sun.misc.Unsafe 是 Java 中的一个特殊类&#xff0c;它提供了一组低级别的、不安全的操作&#xff0c;这些操作通常是 JVM 内部使用的。由于这些操作非常强大且危险&#xff0c;因此 Unsafe 类被设计为只能在受信任的代码中使用。 2. 主要功能和用途 内存操作&a…

STM32-CAN总线

1.CAN总线简介 CAN总线是由BOSCH公司开发的一种简洁易用、传输速度快、易扩展、可靠性高的串行通信总线 2.CAN总线特征 两根通信线&#xff08;CAN_H、CAN_L&#xff09;&#xff0c;线路少&#xff0c;无需共地差分信号通信&#xff08;相对的是单端信号&#xff09;&#…

Linux初识:【版本控制器Git】【调试器gdb/cgdb使用】

目录 一.版本控制器Git 1.1版本控制器 1.2Git的操作 1.2.1从远端仓库到本地 1.2.2工作区到本地暂存区 1.2.3本地暂存区到本地仓库 1.2.4本地仓库到远程仓库 1.2.5 .gitignore 1.2.6Windows上操作&#xff08;需要安装Tortoisegit&#xff09; 1.2.7同步远端和当地 二调…

【MATLAB源码-第259期】基于matlab的64QAM调制解调锁相环环载波同步仿真,对比前后星座图,输出锁相环响应曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. 概述 在现代数字通信系统中&#xff0c;为了提高频谱利用率和数据传输效率&#xff0c;经常采用多阶调制技术。64QAM&#xff08;64阶正交幅度调制&#xff09;便是其中的一种&#xff0c;它通过将数据映射到64个不同的复…

BEVFusion论文阅读

1. 简介 融合激光雷达和相机的信息已经变成了3D目标检测的一个标准&#xff0c;当前的方法依赖于激光雷达传感器的点云作为查询&#xff0c;以利用图像空间的特征。然而&#xff0c;人们发现&#xff0c;这种基本假设使得当前的融合框架无法在发生 LiDAR 故障时做出任何预测&a…

大模型LLM-微调 RAG

RAG小结 这篇文章是一篇关于大型语言模型&#xff08;LLMs&#xff09;增强技术的综述论文&#xff0c;特别聚焦于检索增强生成&#xff08;Retrieval-Augmented Generation, RAG&#xff09;这一领域。详细考察了RAG的发展、技术基础、关键技术、评估框架以及未来的研究方向。…

TongESB7.1.0.0如何使用dockercompose运行镜像(by lqw)

文章目录 安装准备安装 安装准备 1.安装好docker和dockercompose&#xff1a; docker、docker-compose安装教程&#xff0c;很详细 2.上传好安装相关文件 安装 使用以下命令导入管理端镜像和运行时镜像 docker load -i tongesb_manage_7100.tar docker load -i tongesb_se…

Acwing-基础算法课笔记之基础算法(二分)

Acwing-基础算法课笔记之基础算法&#xff08;二分&#xff09; 一、二分查找的概念1、使用二分的条件2、二分查找的算法流程 二、左闭右闭写法[left,right]三、左闭右开写法[left,right)四、浮点数的二分 一、二分查找的概念 1、使用二分的条件 1、必须是数组&#xff08;顺…

PHP教育系统小程序

&#x1f310; 教育系统&#xff1a;全方位学习新体验&#xff0c;引领未来教育风尚 &#x1f680; 教育系统&#xff1a;创新平台&#xff0c;智慧启航 &#x1f4f1; 教育系统&#xff0c;一款深度融合科技与教育的创新平台&#xff0c;匠心独运地采用先进的ThinkPHP框架与U…

蓝桥杯R格式--高精度算法模拟

#include <bits/stdc.h> using namespace std; int pos,p1; int ib[1566]; int an[1567]; int n; string a,b; int main() {cin>>n>>a;for(int ia.size()-1; i>0; i--){if(a[i]!.){pos;b.insert(b.end(),a[i]);///string 插char用insert/push_back} …

【电视盒子】HI3798MV300刷机教程笔记/备份遥控码修复遥控器/ADB/线刷卡刷/电视盒子安装第三方应用软件

心血来潮&#xff0c;看到电视机顶盒满天飞的广告&#xff0c;想改造一下家里的电视盒子&#xff0c;学一下网上的人刷机&#xff0c;但是一切都不知道怎么开始&#xff0c;虽然折腾了一天&#xff0c;以失败告终&#xff0c;还是做点刷机笔记。 0.我的机器 年少不会甄别&…

实战经验:使用 Python 的 PyPDF 进行 PDF 操作

文章目录 1. 为什么选择 PyPDF&#xff1f;2. 安装 PyPDF3. PDF 文件的合并与拆分3.1 合并 PDF 文件3.2 拆分 PDF 文件 4. 提取 PDF 文本5. 修改 PDF 元信息6. PDF 加密与解密6.1 加密 PDF6.2 解密 PDF 7. 页面旋转与裁剪7.1 旋转页面7.2 裁剪页面 8. 实战经验总结 PDF 是一种非…

C++11的多线程

目录 引言 thread类的简单介绍 接口解读 使用范例 move的作用--将资源“夺舍” 原子性操作库(atomic) lock_guard与unique_lock 前置知识&#xff1a;mutex锁&#xff08;类似linux下的ptrhead_mutex_t数据&#xff09; mutex的种类 1. std::mutex 2. std::recursive_…