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

时间:2024-04-01 09:12:09

文章目录

  • 回顾单例模式
  • 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;

    static singleThreadPool<T> *ptrThreadPool;
    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;
}