【Linux】寿司线程池{单例模式之懒汉模式下的线程池}

news2024/11/16 19:38:14

文章目录

  • 回顾单例模式
  • 0.多线程下的单例模式的意义
  • 1.什么是单例模式
    • 1.0设计模式
    • 1.1C++单例模式的介绍及原理
    • 1.2拷贝构造和赋值重载的处理
    • 1.3if (nullptr == ptr),nullptr放在比较运算符的前面?
    • 1.4实现单例模式的方式
  • 2.实现懒汉方式的单例模式
    • 2.1单线程的单例模式
    • 2.2多线程的单例模式
  • 3.总结线程池
  • 4.代码
    • singleThreadPool.hpp
    • singleTestMain.cc

回顾单例模式

在cpp专栏,我们已经讲过单例模式。特殊类设计[下] — 单例模式

0.多线程下的单例模式的意义

在多线程环境下,单例模式的目的是确保无论有多少线程尝试访问或创建类的实例,都只有一个实例被创建,并且这个实例可以被所有线程共享和访问。这是因为在多线程编程中,如果没有适当的同步机制,多个线程可能同时尝试创建类的实例,从而导致产生多个实例,这与单例模式的初衷相违背。

单例模式在多线程环境下的实现需要特别注意线程安全性。如果没有正确的同步,多个线程可能会同时检查单例是否已经被创建,并且都得出否定的结论,从而都尝试创建新的实例。这会导致产生多个实例,破坏单例模式的约束。

因此,多线程下的单例模式的主要目的是:

确保唯一性:不论何时何地,整个应用程序范围内都只有一个类的实例存在。
线程安全:确保在多线程环境下,类的实例的创建和访问都是安全的,不会出现数据竞争或不一致的情况。
全局访问点:提供一个全局的访问点来获取这个唯一的实例,使得任何需要的地方都可以方便地获取和使用这个实例。
为了实现线程安全的单例模式,通常需要使用互斥锁(如std::mutex)或其他同步机制来确保在创建实例时的线程安全。这样,即使多个线程同时尝试访问或创建单例,也只有一个线程能够成功创建实例,而其他线程将获取已经创建好的实例。

1.什么是单例模式

1.0设计模式

在这里插入图片描述

IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式

1.1C++单例模式的介绍及原理

单例模式是一种 “经典的, 常用的, 常考的” 设计模式.

单例模式是一种设计模式,它保证一个类仅有一个实例,并提供一个全局访问点来访问该实例。这种模式在许多场景下都很有用,例如,你可能需要一个唯一的配置管理器、资源管理器或线程池。

在C++中,实现单例模式的基本原理如下:

将类的构造函数设为私有,以防止外部代码通过new来创建类的实例。
在类内部提供一个静态的私有成员变量来保存类的唯一实例。
提供一个公开的静态成员函数(通常命名为getInstance或Instance),用于返回类的唯一实例。如果实例尚未创建,该函数将负责创建它。
实现一个简易的单例模式至少要具备的条件

要实现一个简易的单例模式,你需要满足以下条件:

私有构造函数:确保类的实例不能从类外部被创建。
静态私有成员变量:用于存储类的唯一实例。
公开的静态成员函数:用于获取类的唯一实例。这个函数应该在第一次被调用时创建实例,并在后续的调用中返回已创建的实例。
以下是一个简单的C++用懒汉模式的单例模式实现示例:

cpp
class Singleton {  
private:  
    // 私有构造函数,防止外部创建实例  
    Singleton() {}  
    // 静态私有成员变量,保存唯一实例  
    static Singleton* instance;  
  
public:  
    // 公开的静态成员函数,用于获取唯一实例  
    static Singleton* getInstance() {  
        if (!instance) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
  
    // 假设的某个成员函数  
    void someFunction() {  
        // ...  
    }  
  
    // 析构函数设为公有,但通常不需要外部直接调用  
    ~Singleton() {  
        // 清理资源  
    }  
};  
// 初始化静态成员变量  
Singleton* Singleton::instance = nullptr;

在这个示例中,Singleton类只有一个私有构造函数,因此不能从类外部创建其实例。getInstance函数是获取Singleton类唯一实例的入口点。如果instance为nullptr(即尚未创建实例),则getInstance会创建一个新的实例。在后续调用中,getInstance将直接返回已创建的实例。

注意:上述实现不是线程安全的。在多线程环境下,多个线程可能同时检查instance是否为nullptr并尝试创建实例,从而导致创建多个实例。为了解决这个问题,可以使用互斥锁(如std::mutex)或其他同步机制来确保在任意时刻只有一个线程可以执行创建实例的代码。此外,为了避免内存泄漏,通常还需要提供一个全局的清理函数来删除单例实例。

1.2拷贝构造和赋值重载的处理

在C++单例模式的实现中,拷贝构造函数和赋值操作符通常是不需要的,因此将它们设为私有并禁止外部使用是一个常见的做法。这是因为单例模式要求一个类只有一个实例,而拷贝构造和赋值操作会允许创建类的多个实例,这与单例模式的原则相违背。

对于拷贝构造函数,你可以这样处理:

cpp
class Singleton {  
private:  
    // 禁止拷贝构造  
    Singleton(const Singleton&) = delete;  
    // ... 其他成员 ...  
};

对于赋值操作符,你也可以禁止它:

cpp
class Singleton {  
private:  
    // 禁止赋值操作  
    Singleton& operator=(const Singleton&) = delete;  
    // ... 其他成员 ...  
};

= delete是C++11引入的特性,用于显式地禁止某个成员函数的使用。编译器将尝试调用这些函数时会产生编译错误。

通过禁止拷贝构造和赋值操作,你确保了单例模式的正确性,因为即使尝试进行拷贝或赋值,编译器也会阻止这样的操作。

此外,对于单例模式来说,还需要考虑线程安全的问题。在多线程环境下,需要确保getInstance函数的线程安全性,以避免多个线程同时创建实例。这通常可以通过使用互斥锁(如std::mutex)或其他同步机制来实现。

最后,为了防止内存泄漏,你可能还需要在程序结束时删除单例实例。这可以通过在单例类中添加一个静态的清理函数来实现,该函数在程序结束时调用单例实例的析构函数。但是,请注意,在现代C++中,通常依赖智能指针(如std::shared_ptr)和程序终止时的全局对象析构顺序来自动管理单例实例的生命周期,而不需要显式地编写清理代码。

1.3if (nullptr == ptr),nullptr放在比较运算符的前面?

在C++中,将nullptr放在比较运算符==或!=的前面是一种良好的编程习惯,尤其是在与指针进行比较时。这样做的好处主要体现在两个方面:

防止误写为赋值运算符:
将nullptr放在前面可以防止你不小心将比较运算符==误写为赋值运算符=。如果nullptr在后面,例如ptr = nullptr,当你意图进行比较而误写为赋值时,编译器不会产生错误,但代码的逻辑会完全改变。这可能导致难以察觉的逻辑错误。通过将nullptr放在前面,你可以更容易地识别和纠正这种错误,因为nullptr = ptr这样的写法在语法上是错误的,编译器会立即报错。

代码一致性和可读性:
将常量放在比较运算符的左边可以增强代码的一致性。在C++中,常量通常应该放在比较运算符的左侧,这样可以使代码看起来更加统一和整洁。此外,这种写法也符合一些编程规范和风格指南,有助于提高代码的可读性。

举个例子,假设你有一个指针ptr,你应该这样写比较语句:

cpp
if (nullptr == ptr) {  
    // ptr 是 nullptr  
}
而不是这样:

cpp
if (ptr == nullptr) {  
    // ptr 是 nullptr  
}

虽然第二种写法在逻辑上是正确的,但第一种写法提供了额外的安全性,防止了可能的误写。

然而,值得注意的是,现代编译器和IDE通常都有强大的代码检查和重构工具,可以帮助你避免这类错误。因此,即使你将nullptr放在比较运算符的右侧,只要你的编程环境配置得当,也可以有效地避免误写为赋值运算符的问题。不过,遵循将常量放在比较运算符左侧的约定仍然是一个好习惯,因为它可以提高代码的一致性和可读性。

1.4实现单例模式的方式

饿汉实现方式和懒汉实现方式

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。不用的时候就准备好了,要用的时候直接拿来用。
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度。用的时候再准备。

2.实现懒汉方式的单例模式

2.1单线程的单例模式

static singleThreadPool<T> *getThreadPoolInstance(int threadNum = g_threadNum)
{
    if (nullptr == ptrThreadPool)
    {
        ptrThreadPool = new singleThreadPool<T>(threadNum);
    }
    return ptrThreadPool;
}

2.2多线程的单例模式

static singleThreadPool<T> *getThreadPoolInstance(int threadNum = g_threadNum)
{
    if (nullptr == ptrThreadPool)
    {
        ptrThreadPool = new singleThreadPool<T>(threadNum);
    }
    return ptrThreadPool;
}
  1. 单线程下的单例模式代码拿到多线程场景中,需要考虑的【线程安全问题】只发生在第一次创建单例时,即第一次可能有多个线程在执行if语句,这样就可能有不止一个线程获取到了单例,基于这种情况,我们进行了第一步改版。如下。
static singleThreadPool<T> *getThreadPoolInstance(int threadNum = g_threadNum)
{
    {
        lockGuard lockguard(&mutex);
        if (nullptr == ptrThreadPool)
        {
            ptrThreadPool = new singleThreadPool<T>(threadNum);
        }
    }
    return ptrThreadPool;
}
  1. 这样并不完全对。我们还要考虑,第一次的某个线程成功获取了单例,即这个线程拿着指向一个单例对象的ptrThreadPool走了,之后的线程再次调用getThreadPoolInstance接口想获取到这个唯一的单例时,需要申请锁–执行if–if不满足–解锁–返回ptrThreadPool。问题来了,之后的线程虽然没有创建单例但是获取到了这个唯一的单例,即之后的线程可以获取到唯一的单例,多线程下的单例模式似乎已经搞定了。但是,这样会存在大量的申请和释放锁的行为,这毫无意义且浪费资源,即之后的线程在获取单例时,if一定是不满足的,这时压根没必要进行加锁和解锁,但是为了保证第一次创建单例是安全的,加锁的行为必须有,之后的线程申请到了锁那还好,如果申请不到还要等待⇒ 这毫无意义且浪费资源。做进一步优化如下。
static singleThreadPool<T> *getThreadPoolInstance(int threadNum = g_threadNum)
    {
        if (nullptr == ptrThreadPool)
        {
            lockGuard lockguard(&mutex);
            if (nullptr == ptrThreadPool)
            {
                ptrThreadPool = new singleThreadPool<T>(threadNum);
            }
        }
        return ptrThreadPool;
    }

这样写的优势

假定【后续线程】:第一次已经有某个线程创建了单例即单例已存在。那么这样写的优势:有效减少后续线程进行加锁检测的问题,拦截大量后续线程直接访问锁的行为
在这里插入图片描述

3.总结线程池

  1. 设计一个线程池,这个线程池有多个线程在运行,一旦接收到任务,则某个线程就会去获取并执行任务。
  2. 可能有多个线程充当派发任务的一方,这时就引入了【单例模式】版的线程池,旨在让不同的线程共享唯一的一个线程池实例,任务方线程们都向这个单例线程池派发任务。
  3. 上面的代码我们只创建了一个任务派送方【main线程】,但是单例模式我们已经写好了,读者们可以创建多个任务派送方测试单例模式是否正确。
  4. 由于我们设计的主线程是死循环,我们并没有维护单例的回收,单例的回收我们在特殊类设计[下] — 单例模式已经讲过。在这里插入图片描述

4.代码

其余代码与【Linux】认识线程池 AND 手撕线程池(正常版)相同,点击获取即可。

singleThreadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"

const int g_threadNum = 3;

template <class T>
class singleThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &lock;
    }
    void waitCond()
    {
        pthread_cond_wait(&cond, &lock);
    }
    bool isEmpty()
    {
        return _taskQueue.empty();
    }
    T getTask()
    {
        T t = _taskQueue.front();
        _taskQueue.pop();
        return t;
    }
    static void *startRoutine(void *args)
    {
        ThreadInfo *td = (ThreadInfo *)args;
        singleThreadPool<T> *tp = (singleThreadPool<T> *)td->_ptrThreadPool;
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex());
                while (tp->isEmpty())
                    tp->waitCond();
                task = tp->getTask();
            }
            task(td->_threadName);
        }
    }

private:
    // 构造函数
    singleThreadPool(int thread_num = g_threadNum)
        : _threadNum(thread_num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);

        for (int i = 1; i <= _threadNum; i++)
        {
            // 初始化列表区域 对象还未存在 走到函数块{}内 对象已存在 可以使用this指针
            _threads.push_back(new Thread(i, startRoutine, this));
        }
    }
    singleThreadPool(const singleThreadPool<T> &copy) = delete;
    const singleThreadPool<T> &operator=(const singleThreadPool<T> &copy) = delete;

public:
    // 考虑 多线程使用单例 的情况
    static singleThreadPool<T> *getThreadPoolInstance(int threadNum = g_threadNum)
    {
        if (nullptr == ptrThreadPool)
        {
            lockGuard lockguard(&mutex);
            if (nullptr == ptrThreadPool)
            {
                ptrThreadPool = new singleThreadPool<T>(threadNum);
            }
        }
        return ptrThreadPool;
    }

    void run()
    {
        for (auto &iter : _threads)
        {
            iter->start();
            // std::cout << iter->name() << " 启动成功" << std::endl;
            logMsg(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }
    void pushTask(const T &task)
    {
        lockGuard lockguard(&lock);
        _taskQueue.push(task);
        pthread_cond_signal(&cond);
    }
    ~singleThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

private:
    int _threadNum;
    std::vector<Thread *> _threads;
    std::queue<T> _taskQueue;
    pthread_mutex_t lock;
    pthread_cond_t cond;

    volatile static singleThreadPool<T> *ptrThreadPool;
    懒汉是调用的时候才会赋值,如果创建了对象这个指针实际已经指向了单例,如果指针一开始被优化到寄存器里了,那第二次后续线程访问这个指针就是null,后续不从内存里读取的话就会一直是null了
    static pthread_mutex_t mutex;
};

// 类内声明 类外初始化
template <typename T>
singleThreadPool<T> *singleThreadPool<T>::ptrThreadPool = nullptr;

template <typename T>
pthread_mutex_t singleThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;

singleTestMain.cc

#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>

#include "singleThreadPool.hpp"
#include "Task.hpp"

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());

    // ThreadPool<Task> *tp = new ThreadPool<Task>();
    // ThreadPool<Task> *tp = ThreadPool<Task>::getThreadPoolInstance();

    //tp->run();
    singleThreadPool<Task>::getThreadPoolInstance()->run();

    while (true)
    {
        // 生产数据/制作任务 -- 耗费时间
        int x = rand() % 10 + 1;
        usleep(1000);
        int y = rand() % 5 + 1;
        Task t(x, y, [](int x, int y) -> int
               { return x + y; });

        logMsg(DEBUG, "Main-Pro 发送任务: %d+%d=未知", x, y);

        // 推送任务到线程池中
        // tp->pushTask(t);
        singleThreadPool<Task>::getThreadPoolInstance()->pushTask(t);
        sleep(1);
    }
    return 0;
}

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

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

相关文章

StarRocks实战——携程火车票指标平台建设

目录 前言 一、早期OLAP架构与痛点 二、指标平台重构整体设计 2.1 指标查询过程 2.1.1 明细类子查询 2.1.2 汇总类子查询 2.1.3 “缓存” 2.2 数据同步 三、Starrocks使用经验分享 3.1 建表经验 3.2 数据查询 3.3 函数问题 四、查询性能大幅提升 五、 后续优化方…

算法——分治(快速排序)

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 此篇文章与大家分享分治算法关于排序排序的专题 对于快速排序在我个人主页专栏 <排序> 有详细的介绍,此专题对快排进行了优化操作,并介绍了优化后的快排的几种运用 如果有不足的或者错误的请…

利用Lora调整和部署 LLM

使用 NVIDIA TensorRT-LLM 调整和部署 LoRA LLM 大型语言模型 (LLM) 能够从大量文本中学习并为各种任务和领域生成流畅且连贯的文本&#xff0c;从而彻底改变了自然语言处理 (NLP)。 然而&#xff0c;定制LLM是一项具有挑战性的任务&#xff0c;通常需要完整的培训过程&#xf…

C++ 2024-4-1 作业

#include <iostream> using namespace std;class A { public:int a;A(int a):a(a){cout<<"A的有参构造"<<endl;} }; class B:virtual public A { public:int b;B(int a,int b):A(a),b(b){cout<<"B的有参构造"<<endl;} }; cl…

反截屏控制技术如何防止信息通过手机拍照泄漏?

反截屏控制技术为企业数据安全提供了重要的防护措施。通过以下几点&#xff0c;有效阻止了信息通过拍照等方式的泄漏&#xff1a; 反截屏控制开启&#xff0c;用户启动截屏操作时&#xff0c;允许非涉密内容截屏操作&#xff0c;但所有涉密内容窗口会自动隐藏&#xff0c;防止涉…

openstack云计算(一)————openstack安装教程,创建空白虚拟机,虚拟机的环境准备

1、创建空白虚拟机 需要注意的步骤会截图一下&#xff0c;其它的基本都是下一步&#xff0c;默认的即可 ----------------------------------------------------------- 2、在所建的空白虚拟机上安装CentOS 7操作系统 &#xff08;1&#xff09;、在安装CentOS 7的启动界面中…

Vue依赖注入,详细解析

Prop 逐级透传问题​ 通常情况下&#xff0c;当我们需要从父组件向子组件传递数据时&#xff0c;会使用 props。想象一下这样的结构&#xff1a;有一些多层级嵌套的组件&#xff0c;形成了一颗巨大的组件树&#xff0c;而某个深层的子组件需要一个较远的祖先组件中的部分数据。…

【JavaWeb】Day32.MySQL概述

什么是数据库 数据库&#xff1a;英文为 DataBase&#xff0c;简称DB&#xff0c;它是存储和管理数据的仓库。 像我们日常访问的电商网站京东&#xff0c;企业内部的管理系统OA、ERP、CRM这类的系统&#xff0c;以及大家每天都会刷的头条、抖音类的app&#xff0c;那这些大家所…

element-ui breadcrumb 组件源码分享

今日简单分享 breadcrumb 组件的源码实现&#xff0c;主要从以下三个方面&#xff1a; 1、breadcrumb 组件页面结构 2、breadcrumb 组件属性 3、breadcrumb 组件 slot 一、breadcrumb 组件页面结构 二、breadcrumb 组件属性 2.1 separator 属性&#xff0c;分隔符&#xff…

【洛谷 P8695】[蓝桥杯 2019 国 AC] 轨道炮 题解(映射+模拟+暴力枚举+桶排序)

[蓝桥杯 2019 国 AC] 轨道炮 题目描述 小明在玩一款战争游戏。地图上一共有 N N N 个敌方单位&#xff0c;可以看作 2D 平面上的点。其中第 i i i 个单位在 0 0 0 时刻的位置是 ( X i , Y i ) (X_i, Y_i) (Xi​,Yi​)&#xff0c;方向是 D i D_i Di​ (上下左右之一, 用…

零基础如何自学人工智能?推荐优秀的学习路径及方法

人工智能&#xff08;AI&#xff09;是一个广泛且复杂的领域&#xff0c;自学AI可能是一项艰巨的任务&#xff0c;但只要有兴趣和决心&#xff0c;这绝对是可能的。以下是一个零基础自学人工智能的学习路径&#xff0c;旨在帮助那些只有兴趣&#xff0c;但缺乏背景知识的人。 *…

[图解]DDD领域驱动设计伪创新-通用语言05

0 00:00:01,060 --> 00:00:04,370 甚至有的人把这个当成恩典 1 00:00:08,730 --> 00:00:11,500 他认为这个对技术人员有好处 2 00:00:13,010 --> 00:00:14,790 他掌握了主动权 3 00:00:15,730 --> 00:00:16,501 这样的话 4 00:00:16,501 --> 00:00:18,430 你…

【Android Studio】上位机-安卓系统手机-蓝牙调试助手

【Android Studio】上位机-安卓系统手机-蓝牙调试助手 文章目录 前言AS官网一、手机配置二、移植工程三、配置总结 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 AS官网 AS官网 一、手机配置 Android Studio 下真机调试 二、移植工程 Anro…

有时候在mac上使用IDEA无法访问系统里的文件,比如字体等。

有时候在mac上使用IDEA无法访问系统里的文件&#xff0c;比如字体等。 这里可以打开磁盘访问权限。

网络抓包专题

网络抓包原理 一. 什么是抓包&#xff1f; 在应用的开发调试中&#xff0c;查看软件实际运行时HTTP/HTTPS通信的请求数据和返回数据&#xff0c;从而分析问题的过程就叫做抓包。 通常我们说的抓包主要是分为两种&#xff1a; 使用 Wireshark 抓取传输层的 TCP/UDP 通信包。使…

ideaSSM 校园兼职招聘平台bootstrap开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 idea 开发 SSM 校园兼职招聘平台是一套完善的信息管理系统&#xff0c;结合SSM框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff…

深度学习理论基础(二)深度神经网络DNN

目录 一、基础知识点Ⅰ 参数部分Ⅱ 模型部分 二、深度神经网络模型搭建1. 准备数据集2. 划分数据集3. 搭建模型4. 训练网络5. 测试网络6. 保存与导入模型 神经网络通过学习大量样本的输入与输出特征之间的关系&#xff0c;以拟合出输入与输出之间的方程&#xff0c;学习完成后&…

ndk ffmpeg

报错&#xff1a; 解决办法&#xff1a; 报错 解决办法&#xff1a;

js猜拳游戏

文章目录 1. 演示效果2. 分析思路3. 代码实现3.1. 方式一3.2. 方式二 1. 演示效果 2. 分析思路 获取玩家的出拳(获取按钮的标签体)获取电脑的出拳(随机数)比较二者的出拳&#xff0c;将比较的结果设置到对应的 span 标签中 3. 代码实现 3.1. 方式一 将点击事件进行动态绑定…

Python网络爬虫(四):b站评论

首先来看一下采集的数据格式: 本文不对数据采集的过程做探讨,直接上代码。首先要在程序入口处bvids列表内替换成自己想要采集的视频bvid号,然后将self.cookies替换成自己的(需要字典格式),代码可以同时爬取多个视频的评论,且爬取的评论较为完整,亲测有效: im…