线程池&&单例模式

线程池的概念

线程池是一种线程使用模式。

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

  • 这避免了在处理短时间任务时创建与销毁线程的代价。
  • 线程池不仅能够保证内核的充分利用,还能防止过分调度。

tips:可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景

  • 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个 Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

线程池的实现

下面我们实现一个简单的线程池,线程池中提供了一个任务队列,以及若干个线程(多线程)。

  • 线程池中的多个线程负责从任务队列当中拿任务,并将拿到的任务进行处理。
  • 线程池对外提供一个Push接口,用于让外部线程能够将任务Push到任务队列当中。

线程池代码如下:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include <string>

static const int defaultnum = 5;

struct ThreadInfo
{
    pthread_t tids;
    std::string NameThread;
};

template<class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&lock);
    }

    void UnLock()
    {
        pthread_mutex_unlock(&lock);
    }

    void Wait()
    {
        pthread_cond_wait(&cond, &lock);
    }

    void WakeUp()
    {
        pthread_cond_signal(&cond);
    }

    bool IsEmptyTask()
    {
        return _TasksQueue.size() == 0;
    }

public:
    ThreadPool(int num = defaultnum): _threads(num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);
    }

    void ThreadStart()
    {
        int n = _threads.size();
        for(int i = 0; i < n; i++)
        {
            pthread_create(&_threads[i].tids, nullptr, ThreadTasks, this);
            _threads[i].NameThread = "thread-" + std::to_string(i);
        }
    }

    std::string GetThreadName(pthread_t tid)
    {
        for(auto& e : _threads)
        {
            if(tid == e.tids)
            {
                return e.NameThread; 
            }
        }

        return "none";
    }

    static void *ThreadTasks(void* args)
    {
        ThreadPool<T>* TP = static_cast<ThreadPool<T>*>(args);

        while(true)
        {
            std::string Name = TP->GetThreadName(pthread_self());

            TP->Lock();
            while(TP->IsEmptyTask())
            {
                TP->Wait();
            }

            T t = TP->pop();

            TP->UnLock();

            t();

            std::cout << Name.c_str() << ' ' << std::endl;
	        t.GetTask();
        }
    }

    T pop()
    {
        T t = _TasksQueue.front();
        _TasksQueue.pop();

        return t;
    }

    void push(const T &task)
    {
        Lock();
        _TasksQueue.push(task);
        WakeUp();
        UnLock();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }
private:
    std::vector<ThreadInfo> _threads;
    std::queue<T> _TasksQueue;

    pthread_mutex_t lock;
    pthread_cond_t cond;
};

为什么线程池中需要有互斥锁和条件变量?

使用互斥锁的原因:STL容器一开始被设计时,就是为了追求效率,并没有考虑线程安全,多线程场景,pop数据时可能产生并发问题

使用条件变量的原因:

为什么线程池中的线程执行例程需要设置为静态方法?

Routine作为类的成员函数,该函数的第一个参数是隐藏的this指针,因此这里的Routine函数,虽然看起来只有一个参数,而实际上它有两个参数,此时直接将该Routine函数作为创建线程时的执行例程是不行的,无法通过编译。

静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将Routine设置为静态方法,此时Routine函数才真正只有一个参数类型为void*的参数。

但是在静态成员函数内部无法调用非静态成员函数,而我们需要在Routine函数当中调用该类的某些非静态成员函数,比如Pop。因此我们需要在创建线程时,向Routine函数传入的当前对象的this指针,此时我们就能够通过该this指针在Routine函数内部调用非静态成员函数了。

任务类型的设计

cpp 复制代码
#pragma once

#include "ThreadPool.hpp"

enum
{
    EXITCODE = 0,
    DIVZERO,
    MODZERO
};

class Task
{
public:
    Task(int x, int y, char oper, int exitcode_ = EXITCODE) : _data1(x), _data2(y), _oper(oper), exitcode(exitcode_)
    {}

    void run()
    {

        switch (_oper)
        {
        case '+':
            result = _data1 + _data2;
            break;

        case '-':
            result = _data1 - _data2;
            break;

        case '*':
            result = _data1 * _data2;
            break;

        case '/':
            if(_data1 == 0 | _data2 == 0)
            {
                exitcode = DIVZERO;
            }
            else
            {
                result = _data1 / _data2;
            }
            break;

        case '%':
            if(_data1 == 0 | _data2 == 0)
            {
                exitcode = MODZERO;
            }
            else
            {
                result = _data1 % _data2;
            }
            break;

        default:
            std::cout << "Symbol mismatch!" << std::endl;
            break;
        }
        
        SolveTask();
    }

    std::string _To_String()
    {
        std::string str;
        str += "[exitcode: ";
        str += std::to_string(exitcode);
        str += "]";
        str += " ";
        str += std::to_string(_data1);
        str += " ";
        str += _oper;
        str += " ";
        str += std::to_string(_data2);

        return str;
    }

    void GetTask()
    {
        std::cout << _data1 << " " << _oper << " " << _data2 << " = ?" << std::endl;
    }

    void SolveTask()
    {
        std::cout << _To_String() << " = " << result << std::endl;
    }

    void operator()()
    {
        run();
    }

    ~Task()
    {}

private:
    int _data1;
    int _data2;
    char _oper;

    int exitcode;
    int result;
};

主线程逻辑

主线程就负责不断向任务队列当中Push任务就行了,此后线程池当中的线程会从任务队列当中获取到这些任务并进行处理。

cpp 复制代码
#include "SingletonThreadPool.hpp"
#include "Task.hpp"
#include <unistd.h>

std::string oper = "+-*/%";
 
int main()
{
    srand(time(nullptr));

    SingletonThreadPool<Task>* STP = new SingletonThreadPool<Task>(5);
    STP->ThreadStart();

    int len = oper.size(); 
    while(true)
    {
        sleep(1);
        int data1 = rand() % 10;
        int data2 = rand() % 10 + 1;

        char op = oper[rand() % len];

        Task t(data1, data2, op);
        t.push(t);

        t.GetTask();

    }
    return 0;
}

运行代码后一瞬间就有六个线程,其中一个是主线程,另外五个是线程池内处理任务的线程。

注意: 此后我们如果想让线程池处理其他不同的任务请求时,我们只需要提供一个任务类,在该任务类当中提供对应的任务处理方法就行了。

线程安全的单例模式

什么是单例模式

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

什么是设计模式

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

单例模式的特点

某些类, 只应该具有一个对象(实例), 就称之为单例. 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.

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

饿汉方式就是直接在类中将需要使用的对象先申请好

懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度.

例如:

cpp 复制代码
#pragma once

class Singleton
{
public:
    Singleton()
    {
        std::cout << "对象创建成功" << std::endl;
    }

    static Singleton& GetInstance()
    {
        return num;
    }
    
private:
    static Singleton num;
};
cpp 复制代码
#include <iostream>

using namespace std;
#include "singleton.hpp"

//static int num = 10;

Singleton init1;
Singleton init2;
Singleton init3;
Singleton init4;
Singleton init5;
Singleton init6;


int main()
{
    printf("启动!!!\n");    

    return 0;
}

可以看到饿汉式的单例模式会在程序启动之前对象就创建好了

饿汉方式实现单例模式

cpp 复制代码
template <typename T> class Singleton 
{
    static T data;
public:
    static T* GetInstance() {
        return &data;
    } 
};

只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例

懒汉方式实现单例模式

cpp 复制代码
template <typename T> class Singleton
{
    static T* inst; 
public:
    static T* GetInstance() {
        if(inst == NULL) {
            inst = new T();
        }
        return inst;
    } 
};

存在一个严重的问题, 线程不安全.

第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.

懒汉方式实现单例模式(线程安全版本)

cpp 复制代码
// 懒汉模式, 线程安全 
template <typename T> 
class Singleton { 
    volatile static T* inst;  // 需要设置 volatile 关键字, 否则可能被编译器优化. 
    static std::mutex lock; 
public: 
    static T* GetInstance()
    { 
        if(inst == NULL)  //避免占用CPU和操作系统资源,做无意义的事,提高效率
        {  
            lock.lock();          // 使用互斥锁, 保证多线程情况下也只调用一次 new.
            if (inst == NULL) 
            { 
                inst = new T(); 
            } 
            lock.unlock(); 
        } 
        return inst; 
    } 
}; 

注意事项:

  1. 加锁解锁的位置

  2. 双重 if 判定, 避免不必要的锁竞争

  3. volatile关键字防止过度优化

将上述线程池代码改为单例模式

cpp 复制代码
#pragma once

#include <iostream>
#include <pthread.h>
#include <queue>
#include <vector>
#include <string>

static const int defaultnum = 5;

class ThreadInfo
{
public:
    pthread_t tid;
    std::string threadname;
};

template <class T>
class SingletonThreadPool
{
    void Lock()
    {
        pthread_mutex_lock(&lock);
    }
    void UnLock()
    {
        pthread_mutex_unlock(&lock);
    }
    void Wait()
    {
        pthread_cond_wait(&cond, &lock);
    }

    void WakeUp()
    {
        pthread_cond_signal(&cond);
    }

    bool IsEmptyThreadPool()
    {
        return _tasksqueue.size() == 0;
    }


public:
    static void* RoutineTasks(void* args)
    {
        SingletonThreadPool<T> *TP = static_cast<SingletonThreadPool<T>*>(args);

        while(true)
        {
            std::string name = TP->GetThreadName(pthread_self());
            TP->Lock();
            if(TP->IsEmptyThreadPool())
            {
                TP->Wait();
            }

            T t = TP->pop();
            TP->UnLock();

            t();

            std::cout << name << ' ' << std::endl;
            t.GetTask();

        }
    }

public:
    
    void ThreadStart()
    {
        int num = _threads.size();
        for(int i = 0; i < num; i++)
        {
            pthread_create(&_threads[i].tid, nullptr, RoutineTasks, this);
            _threads[i].threadname = "thread-" + std::to_string(i);
        }
    }

    T pop()
    {
        T task = _tasksqueue.front();
        _tasksqueue.pop();

        return task;
    }

    std::string GetThreadName(pthread_t tid)
    {
        for(const auto& e : _threads)
        {
            if(tid == e.tid)
            return e.threadname;
        }

        return "none";
    }

    void push(const T& task)
    {
        Lock();
        _tasksqueue.push(task);
        WakeUp();
        UnLock();
    }

    static SingletonThreadPool<T>* GetInStance()
    {
        //避免占用CPU和操作系统资源,做无意义的事,提高效率
        if(inst == nullptr)
        {
            //避免多线程模式下,同时申请多个inst
            pthread_mutex_lock(&slock);
            if(inst == nullptr)
            {
                inst = new SingletonThreadPool<T>;
            }
            pthread_mutex_unlock(&slock);
        }

        return inst;
    }

private:
    SingletonThreadPool(int num = defaultnum):_threads(num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);
    }

    ~SingletonThreadPool()
    {
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

    //防拷贝
    SingletonThreadPool(const SingletonThreadPool<T>& STP) = delete;
    SingletonThreadPool<T>& operator=(const SingletonThreadPool<T>& STP) = delete;

private:
    std::vector<ThreadInfo> _threads;
    std::queue<T> _tasksqueue;
    
    pthread_mutex_t lock;
    pthread_cond_t cond;

    static SingletonThreadPool<T> *inst;
    static pthread_mutex_t slock;
}; 

template<class T>
SingletonThreadPool<T>* SingletonThreadPool<T>::inst = nullptr;

template<class T>
pthread_mutex_t SingletonThreadPool<T>::slock = PTHREAD_MUTEX_INITIALIZER;
cpp 复制代码
#include "SingletonThreadPool.hpp"
#include "Task.hpp"
#include <unistd.h>

std::string oper = "+-*/%";
 
int main()
{
    srand(time(nullptr));

    SingletonThreadPool<Task>::GetInStance()->ThreadStart();

    int len = oper.size(); 
    while(true)
    {
        sleep(1);
        int data1 = rand() % 10;
        int data2 = rand() % 10 + 1;

        char op = oper[rand() % len];

        Task t(data1, data2, op);
        SingletonThreadPool<Task>::GetInStance()->push(t);

        t.GetTask();
    }

    return 0;
}
相关推荐
你又食言了哦11 分钟前
linux下使用wireshark捕捉snmp报文
linux·网络·wireshark
LCY13321 分钟前
python 与Redis操作整理
开发语言·redis·python
暮乘白帝过重山24 分钟前
路由逻辑由 Exchange 和 Binding(绑定) 决定” 的含义
开发语言·后端·中间件·路由流程
PingdiGuo_guo33 分钟前
C++动态分配内存知识点!
开发语言·c++
阿沁QWQ35 分钟前
STL中emplace实现原理是什么?
c++
fpcc39 分钟前
设计心得——数据结构的意义
数据结构·c++
人类群星闪耀时1 小时前
5G赋能远程医疗:从愿景到现实的技术变革
开发语言·5g·php
雪落山庄1 小时前
LeetCode100题
java·开发语言·数据结构
Y.O.U..1 小时前
力扣HOT100——102.二叉树层序遍历
数据结构·c++·算法·leetcode