wordpress站点标题图片,南宁网站建站,怎样做投资理财网站,网站建设进项填那个服务1.线程池的概念
1.1.池化技术
池化技术指的是提前准备一些资源#xff0c;在需要时可以重复使用这些预先准备的资源。
在系统开发过程中#xff0c;我们经常会用到池化技术。通俗的讲#xff0c;池化技术就是#xff1a;把一些资源预先分配好#xff0c;组织到对象池中…1.线程池的概念
1.1.池化技术
池化技术指的是提前准备一些资源在需要时可以重复使用这些预先准备的资源。
在系统开发过程中我们经常会用到池化技术。通俗的讲池化技术就是把一些资源预先分配好组织到对象池中之后的业务使用资源从对象池中获取使用完后放回到对象池中。
这样做带来几个明显的好处
资源重复使用 减少了资源分配和释放过程中的系统消耗。比如在IO密集型的服务器上并发处理过程中的子线程或子进程的创建和销毁过程带来的系统开销将是难以接受的。所以在业务实现上通常把一些资源预先分配好如线程池数据库连接池Redis连接池HTTP连接池等来减少系统消耗提升系统性能。可以对资源的整体使用做限制。这个好理解相关资源预分配且只在预分配是生成后续不再动态添加从而限制了整个系统对资源的使用上限。类似一个令牌桶的功能。池化技术分配对象池通常会集中分配这样有效避免了碎片化的问题。
池化技术简单点来说就是提前保存大量的资源以备不时之需。
池化技术有两个特点提前创建和重复利用。 由于在实际应用当做分配内存、创建进程、线程都会设计到一些系统调用系统调用需要导致程序从用户态切换到内核态是非常耗时的操作。因此当程序中需要频繁的进行内存申请释放进程、线程创建销毁等操作时通常会使用内存池、进程池、线程池技术来提升程序的性能。 对连接或线程的复用并对复用的数量、时间等进行控制从而使得系统的性能和资源消耗达到最优状态。 池化技术 的本质空间换时间 1.2.线程池 线程池的基本概念是在应用程序启动时创建一定数量的线程并将它们保存在线程池中。当需要执行任务时从线程池中获取一个空闲的线程将任务分配给该线程执行。当任务执行完毕后线程将返回到线程池可以被其他任务复用。 线程池的设计思想是为了避免频繁地创建和销毁线程的开销以及控制并发执行的线程数量从而提高系统的性能和资源利用率。 线程池Thread Pool是一种并发编程中常用的技术用于管理和重用线程。它由线程池管理器、工作队列和线程池线程组成。
线程池的关键组成部分包括
线程池管理器ThreadPoolExecutor负责创建、管理和控制线程池。它负责线程的创建、销毁和管理以及线程池的状态监控和调度任务。工作队列BlockingQueue用于存储待执行的任务。当线程池中的线程都在执行任务时新的任务会被放入工作队列中等待执行。线程池线程Worker Thread实际执行任务的线程。线程池中会维护一组线程这些线程可以被重复使用从而避免了频繁创建和销毁线程的开销。
1.3.线程池的优点 线程池 的优点在于 高效、方便线程在使用前就已经创建好了使用时直接将任务交给线程完成。此外线程会被合理调度确保 任务与线程 间能做到负载均衡
重用线程线程池会在内部维护一组可重用的线程避免了频繁地创建和销毁线程的开销提高了线程的利用率。控制并发度线程池可以限制并发执行的线程数量防止系统过载。通过调整线程池的大小可以控制并发度避免资源消耗过大。提供线程管理和监控线程池提供了一些管理和监控机制例如线程池的创建、销毁、线程状态的监控等方便开发人员进行线程的管理和调试。提供任务队列线程池通常会使用任务队列来存储待执行的任务这样可以实现任务的缓冲和调度
线程池 中的线程数量不是越多越好因为线程增多会导致调度变复杂具体创建多少线程取决于具体业务场景比如 处理器内核、剩余内存、网络中的 socket 数量等
线程池 还可以配合 「生产者消费者模型」 一起使用做到 解耦与提高效率
1.4.线程池的应用场景
线程池 有以下几种应用场景
存在大量且短小的任务请求比如 Web 服务器中的网页请求使用 线程池 就非常合适因为网页点击量众多并且大多都没有长时间连接访问 对性能要求苛刻力求快速响应需求比如游戏服务器要求对玩家的操作做出快速响应突发大量请求但不至于使服务器产生过多的线程短时间内在服务器创建大量线程会使得内存达到极限造成出错可以使用 线程池 规避问题
2.线程池_V1朴素版——够用了
「朴素版」实现最基本的线程池功能直接使用系统提供的接口
所谓朴素版就是不加任何优化设计只实现 线程池 最基础的功能便于理解 线程池 创建 ThreadPool_v1.hpp 头文件 将 线程池 实现为一个类提供接口供外部调用
首先要明白 线程池 的两大核心一批线程 与 任务队列客户端发出请求新增任务线程获取任务执行任务因此 ThreadPool_v1.hpp 的大体框架如下
创建一批线程通过容器将它们管理起来创建任务队列存储就绪的任务设置互斥锁设置条件变量
互斥锁 的作用是 保证多个线程并访问任务队列时的线程安全而 条件变量 可以在 任务队列 为空时让一批线程进入等待状态也就是线程同步
注为了方便实现直接使用系统调用接口及容器比如 pthread_t、vector、queue 等 ThreadPool_v1.hpp #pragma once#include vector
#include string
#include queue
#include memory
#include unistd.h
#include pthread.h#define THREAD_NUM 10 // 线程池线程的默认个数template class T
class ThreadPool
{
public:ThreadPool(int num THREAD_NUM) // 线程数量: _threads(num), _num(num){// 初始化互斥锁和条件变量pthread_mutex_init(_mtx, nullptr);pthread_cond_init(_cond, nullptr);}~ThreadPool(){// 互斥锁、条件变量pthread_mutex_destroy(_mtx);pthread_cond_destroy(_cond);}void init(){// 其他信息初始化当前不需要}void start(){// 启动线程池// ...}// 提供给线程的回调函数static void *threadRoutine(void *args){// 业务处理// ...}private:std::vectorpthread_t _threads; // 存放线程的容器int _num; // 线程数量std::queueT _tasks; // 利用 STL 自动扩容的特性无需担心容量pthread_mutex_t _mtx; // 互斥锁pthread_cond_t _cond; // 条件变量
};
这里需要补充一点
类内使用多线程就得需要将提供给线程的回调函数需要设置为静态否则就会报错参数不匹配需要提前给 vector 扩容避免后面使用时发生越界访问 接下来就继续设计了 init()的设计 — 位于 ThreadPool 类 当前场景只需要初始化 互斥锁 和 条件变量在 构造函数 中完成就行了所以这里的 init() 函数不需要补充所以我们把它删掉即可 start()的设计 — 位于 ThreadPool 类 启动 线程池 需要先创建出一批线程这里直接循环创建即可
void start()
{// 创建一批线程并启动for(int i 0; i _num; i)pthread_create(_threads[i], nullptr, threadRoutine, nullptr);
}线程的回调函数 threadRoutine()的设计 — 位于 ThreadPool 类 这里进行简单测试打印当前线程的线程 ID 就行了并且直接 detach主线程无需等待次线程运行结束
// 提供给线程的回调函数
static void *threadRoutine(void *args)
{// 避免等待线程直接剥离pthread_detach(pthread_self());while (true){std::cout Thread Running... pthread_self() std::endl;sleep(1);}
}创建 main.cc 源文件测试线程池的代码 #include ThreadPool_V1.hpp
#include memoryint main()
{std::unique_ptrThreadPoolint ptr(ThreadPoolint());ptr-init();ptr-start();// 还有后续动作return 0;
}makefile test:main.ccg -o $ $^ -stdc11 -lpthread.PHONY:clean
clean:rm -rf test 编译并运行代码可以看到 确实创建了一批线程当主线程退出后其他次线程也就跟着终止了 好了接着设计 线程池 还需要提供一个重要的接口 pushTask()将用户需要执行的业务装载至 任务队列 中等待线程执行 装载任务 pushTask() — 位于 ThreadPool 类 // 装载任务
void pushTask(const T task)
{// 本质上就是在生产商品需要加锁保护pthread_mutex_lock(_mtx);_tasks.push(task);// 唤醒消费者进行消费pthread_cond_signal(_cond);//线程都会阻塞在这里条件变量里面pthread_mutex_unlock(_mtx);
}装载任务的本质就是在生产任务相当于用户充当生产者通过这个接口将任务生产至任务队列中而线程充当消费者从任务队列中获取任务并消费
所以线程的回调函数需要从 任务队列 中获取任务进行消费
检测是否有任务有 - 消费没有 - 等待 线程回调函数 threadRoutine() — 位于 ThreadPool 类 // 提供给线程的回调函数
static void *threadRoutine(void *args)
{// 避免等待线程直接剥离pthread_detach(pthread_self());while (true){// 任务队列是临界资源需要保护pthread_mutex_lock(_mtx);// 等待条件满足while(_tasks.empty())pthread_cond_wait(_cond, _mtx);T task _tasks.front();_tasks.pop();// task(); // 进行消费存疑pthread_mutex_unlock(_mtx);}
}注意 判断任务队列是否为空需要使用 while确保在多线程环境中不会出现问题
因为 任务队列、互斥锁、条件变量 是类内成员而这里的 threadRoutine() 函数是一个静态函数并没有 this 指针以访问类内成员可以采取传递 this 指针的方式解决问题 启动线程池 start() — 位于 ThreadPool 类 void start()
{// 创建一批线程并启动for(int i 0; i _num; i)pthread_create(_threads[i], nullptr, threadRoutine, this); // 传递 this 指针
}threadRoutine() 函数需要将参数 void* 转化为所在类对象的指针并通过该指针访问类内成员 线程回调函数 threadRoutine() — 位于 ThreadPool 类 // 提供给线程的回调函数
static void *threadRoutine(void *args)
{// 避免等待线程直接剥离pthread_detach(pthread_self());auto ptr static_castThreadPoolT*(args);while (true){// 任务队列是临界资源需要保护pthread_mutex_lock(ptr-_mtx);// 等待条件满足while(ptr-_tasks.empty())pthread_cond_wait(ptr-_cond, ptr-_mtx);T task ptr-_tasks.front();ptr-_tasks.pop();//task(); // 进行消费存疑pthread_mutex_unlock(ptr-_mtx);}
}
到这里也差不多了我们整合一下我们的代码 ThreadPool_v1.hpp #pragma once
#include iostream
#include vector
#include string
#include queue
#include memory
#include unistd.h
#include pthread.h#define THREAD_NUM 10 // 线程池线程的默认个数template class T
class ThreadPool
{
private:void lockQueue() // 加锁{pthread_mutex_lock(_mtx);}void unlockQueue() // 解锁{pthread_mutex_unlock(_mtx);}void threadWait() // 让当前线程进入阻塞队列等待{pthread_cond_wait(_cond, _mtx);}void threadWakeUp() // 唤醒第一个线程{pthread_cond_signal(_cond);}bool isEmpty() // 任务是不是为空{return _tasks.empty();}public:ThreadPool(int num THREAD_NUM) // 线程数量: _threads(num), _num(num){// 初始化互斥锁和条件变量pthread_mutex_init(_mtx, nullptr);pthread_cond_init(_cond, nullptr);}~ThreadPool(){// 互斥锁、条件变量pthread_mutex_destroy(_mtx);pthread_cond_destroy(_cond);}T popTask() // 清除任务{T task _tasks.front();_tasks.pop();return task;}// 装载任务void pushTask(const T task){// 本质上就是在生产商品需要加锁保护lockQueue();_tasks.push(task);// 唤醒消费者进行消费threadWakeUp();unlockQueue();}void start(){// 创建一批线程并启动for (int i 0; i _num; i)pthread_create(_threads[i], nullptr, threadRoutine, nullptr);}// 提供给线程的回调函数static void *threadRoutine(void *args){// 避免等待线程直接剥离pthread_detach(pthread_self());auto ptr static_castThreadPoolT *(args);while (true){// 任务队列是临界资源需要保护ptr-lockQueue();// 等待条件满足while (ptr-isEmpty())ptr-threadWait(); // 进来了就先排队T task ptr-popTask();ptr-unlockQueue(); // 解锁// 消费行为可以不用加锁一个商品只会被一个线程消费因为我们只唤醒了一个task();}}private:std::vectorpthread_t _threads; // 存放线程的容器int _num; // 线程数量std::queueT _tasks; // 利用 STL 自动扩容的特性无需担心容量pthread_mutex_t _mtx; // 互斥锁pthread_cond_t _cond; // 条件变量
};细节 轮到线程执行任务时不需要加锁这就好比你买桶泡面回家是不必担心别人会和你争抢可以慢慢消费同样的你也不应该占用锁资源主动让出锁资源以提高整体效率
上面那个写的可能有点不太好看下面重新写一个 ThreadPool_v1.hpp改良版本 #pragma once#include iostream
#include unistd.h
#include queue
#include pthread.h#define NUM 5//线程池
templateclass T
class ThreadPool
{
private:bool IsEmpty(){return _task_queue.size() 0;}void LockQueue(){pthread_mutex_lock(_mutex);}void UnLockQueue(){pthread_mutex_unlock(_mutex);}void Wait(){pthread_cond_wait(_cond, _mutex);}void WakeUp(){pthread_cond_signal(_cond);}
public:ThreadPool(int num NUM): _thread_num(num){pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(_mutex);pthread_cond_destroy(_cond);}//线程池中线程的执行例程static void* Routine(void* arg){pthread_detach(pthread_self());ThreadPool* self (ThreadPool*)arg;//不断从任务队列获取任务进行处理while (true){self-LockQueue();while (self-IsEmpty()){self-Wait();}T task;self-Pop(task);self-UnLockQueue();task.Run(); //处理任务}}void ThreadPoolInit(){pthread_t tid;for (int i 0; i _thread_num; i){pthread_create(tid, nullptr, Routine, this); //注意参数传入this指针}}//往任务队列塞任务主线程调用void Push(const T task){LockQueue();_task_queue.push(task);UnLockQueue();WakeUp();}//从任务队列获取任务线程池中的线程调用void Pop(T task){task _task_queue.front();_task_queue.pop();}
private:std::queueT _task_queue; //任务队列int _thread_num; //线程池中线程的数量pthread_mutex_t _mutex;pthread_cond_t _cond;
};
补充一下 1.为什么线程池中需要有互斥锁和条件变量 线程池中的任务队列是会被多个执行流同时访问的临界资源因此我们需要引入互斥锁对任务队列进行保护。 线程池当中的线程要从任务队列里拿任务前提条件是任务队列中必须要有任务因此线程池当中的线程在拿任务之前需要先判断任务队列当中是否有任务若此时任务队列为空那么该线程应该进行等待直到任务队列中有任务时再将其唤醒因此我们需要引入条件变量。 当外部线程向任务队列中Push一个任务后此时可能有线程正处于等待状态因此在新增任务后需要唤醒在条件变量下等待的线程。 注意 当某线程被唤醒时其可能是被异常或是伪唤醒或者是一些广播类的唤醒线程操作而导致所有线程被唤醒使得在被唤醒的若干线程中只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足被唤醒条件所以在判断任务队列是否为空时应该使用while进行判断而不是if。pthread_cond_broadcast函数的作用是唤醒条件变量下的所有线程而外部可能只Push了一个任务我们却把全部在等待的线程都唤醒了此时这些线程就都会去任务队列获取任务但最终只有一个线程能得到任务。一瞬间唤醒大量的线程可能会导致系统震荡这叫做惊群效应。因此在唤醒线程时最好使用pthread_cond_signal函数唤醒正在等待的一个线程即可。当线程从任务队列中拿到任务后该任务就已经属于当前线程了与其他线程已经没有关系了因此应该在解锁之后再进行处理任务而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间所以我们不要将其放到临界区当中。如果将处理任务的过程放到临界区当中那么当某一线程从任务队列拿到任务后其他线程还需要等待该线程将任务处理完后才有机会进入临界区。此时虽然是线程池但最终我们可能并没有让多线程并行的执行起来。 2.为什么线程池中的线程执行例程需要设置为静态方法 使用pthread_create函数创建线程时需要为创建的线程传入一个Routine执行例程该Routine只有一个参数类型为void*的参数以及返回类型为void*的返回值。 而此时Routine作为类的成员函数该函数的第一个参数是隐藏的this指针因此这里的Routine函数虽然看起来只有一个参数而实际上它有两个参数此时直接将该Routine函数作为创建线程时的执行例程是不行的无法通过编译。 静态成员函数属于类而不属于某个对象也就是说静态成员函数是没有隐藏的this指针的因此我们需要将Routine设置为静态方法此时Routine函数才真正只有一个参数类型为void*的参数。 但是在静态成员函数内部无法调用非静态成员函数而我们需要在Routine函数当中调用该类的某些非静态成员函数比如Pop。因此我们需要在创建线程时向Routine函数传入的当前对象的this指针此时我们就能够通过该this指针在Routine函数内部调用非静态成员函数了。 接下来我们来测试一下 我们将线程池进行了模板化因此线程池当中存储的任务类型可以是任意的但无论该任务是什么类型的在该任务类当中都必须包含一个Run方法当我们处理该类型的任务时只需调用该Run方法即可。
例如下面我们实现一个计算任务类 Task.hpp #pragma once#include iostream//任务类
class Task
{
public:Task(int x 0, int y 0, char op 0): _x(x), _y(y), _op(op){}~Task(){}//处理任务的方法void Run(){int result 0;switch (_op){case :result _x _y;break;case -:result _x - _y;break;case *:result _x * _y;break;case /:if (_y 0){std::cerr Error: div zero! std::endl;return;}else{result _x / _y;}break;case %:if (_y 0){std::cerr Error: mod zero! std::endl;return;}else{result _x % _y;}break;default:std::cerr operation error! std::endl;return;}std::cout thread[ pthread_self() ]: _x _op _y result std::endl;}
private:int _x;int _y;char _op;
}; 此时线程池内的线程不断从任务队列拿出任务进行处理而它们并不需要关心这些任务是哪来的它们只需要拿到任务后执行对应的Run方法即可。
#include Task.hpp
#include ThreadPool_v1.hppint main()
{srand((unsigned int)time(nullptr));ThreadPoolTask* tp new ThreadPoolTask; //线程池tp-ThreadPoolInit(); //初始化线程池当中的线程const char* op -*/%;//不断往任务队列塞计算任务while (true){sleep(1);int x rand() % 100;int y rand() % 100;int index rand() % 5;Task task(x, y, op[index]);tp-Push(task);}return 0;
}运行代码后一瞬间就有六个线程其中一个是主线程另外五个是线程池内处理任务的线程。 并且我们会发现这五个线程在处理时会呈现出一定的顺序性因为主线程是每秒Push一个任务这五个线程只会有一个线程获取到该任务其他线程都会在等待队列中进行等待当该线程处理完任务后就会因为任务队列为空而排到等待队列的最后当主线程再次Push一个任务后会唤醒等待队列首部的一个线程这个线程处理完任务后又会排到等待队列的最后因此这五个线程在处理任务时会呈现出一定的顺序性。 注意 此后我们如果想让线程池处理其他不同的任务请求时我们只需要提供一个任务类在该任务类当中提供对应的任务处理方法就行了。
3.线程池最终进阶版本——V2版本
3.1.单例模式的概念
1. 什么是单例模式
单例模式是指一个类只会创建一次对象的设计模式属于设计模式创建者模式中的一种。这个类提供了一种唯一访问该对象的方式也就是说这个类的实现只会在内存中出现一次。这样子的好处是防止频繁的创建对象导致内存资源浪费。
2. 单例模式的两种形式
饿汉式在类被加载时就会创建该类的实例对象
懒汉式在类被加载时不回创建该类的实例对象在首次要使用该实例时才会创建
3. 单例模式的特点
1.单例类只会有一个实例
2.单例类的实例由该类自己提供对外访问的方法
3.单例类的构造函数必须是私有的
3.2.单例模式的简单实现
单例模式 有两种实现方向饿汉 与 懒汉它们避免类被再次创建出对象的手段是一样的构造函数私有化、删除拷贝构造
只要外部无法访问 构造函数那么也就无法构建对象了比如下面这个类 Signal 单例类 Signal #pragma once#include iostreamclass Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;
};
这样子外面就不能创建类对象了当然这只实现了一半 还有另一半是 创建一个单例对象既然外部受权限约束无法创建对象那么类内是肯定可以创建对象的只需要创建一个指向该类对象的 静态指针 或者一个 静态对象再初始化就好了因为外部无法访问该指针所以还需要提供一个静态函数 getInstance() 以获取单例对象的信息至于具体怎么实现需要分不同方向饿汉 or 懒汉
#pragma once
#include iostreamclass Signal
{
private:// 构造函数私有化Signal(){ }// 删除拷贝构造Signal(const Signal ) delete;public:// 获取单例对象的句柄static Signal *getInstance(){return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 指向单例对象的静态指针static Signal *_sigptr;
};
注意 构造函数不能只声明需要实现即使什么都不写 为什么要删除拷贝构造 如果不删除拷贝构造那么外部可以借助拷贝构造函数拷贝构造出一个与 单例对象 一致的 “对象”此时就出现两个对象这是不符合 单例模式 特点的 为什么要创建一个静态函数 单例对象也需要被初始化并且要能被外部使用 调用链逻辑通过静态函数获取句柄静态单例对象地址- 通过地址调用该对象的其他函数 3.2.1.饿汉模式
张三总是很饿尽管饭菜还没准备好他就已经早早的把碗洗好了等到开饭时直接开干
饿汉模式 也是如此在程序加载到内存时就已经早早的把 单例对象 创建好了此时程序服务还没有完全启动也就是在外部直接通过 new 实例化一个对象具体实现如下
#pragma once#include iostream// 饿汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;public:static Signal *getInstance(){return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 指向单例对象的静态指针static Signal *_sigptr;
};Signal *Signal::_sigptr new Signal();
注在程序加载时该对象会被创建
这里的 单例对象 本质就有点像 全局变量在程序加载时就已经创建好了
外部可以直接通过 getInstance() 获取 单例对象 的操作句柄来调用类中的其他函数 main.cc #include iostream
#include Signal.hppint main()
{Signal::getInstance()-print();return 0;
}可以看到我们没有创建类对象都能调用这个静态函数。 这就实现了一个简单的 饿汉版单例类除了创建 static Signal* 静态单例对象指针 外也可以直接定义一个 静态单例对象生命周期随进程不过要注意的是getInstance() 需要返回的也是该静态单例对象的地址不能返回值因为拷贝构造被删除了并且需要在类的外部初始化该静态单例对象
#pragma once#include iostream// 饿汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;public:static Signal *getInstance(){return _sig;}void print(){std::cout Hello Signal! std::endl;}private:// 静态单例对象static Signal _sig;
};// 初始化
Signal Signal::_sig;
饿汉模式 是一个相对简单的单例实现方向只需要在类中声明在类外初始化就行了但它也会带来一定的弊端延缓服务启动速度 完全启动服务是需要时间的创建 单例对象 也是需要时间的饿汉模式 在服务正式启动前会先创建对象但凡这个单例类很大服务启动时间势必会受到影响大型项目启动时间就是金钱 并且由于 饿汉模式 每次都会先创建 单例对象再启动服务如果后续使用 单例对象 还好说但如果后续没有使用 单例对象那么这个对象就是白创建了在延缓服务启动的同时造成了一定的资源浪费 综上所述饿汉模式 不是很推荐使用除非图实现简单并且服务规模较小既然 饿汉模式 有缺点就需要改进于是就出现了 懒汉模式
3.2.2.懒汉模式 李四也是个很饿的人他也有一个自己的碗吃完饭后碗会脏但他不像张三那样极端李四比较懒只有等他吃饭的时候他才会去洗碗李四这种做法让他感到无比轻松。 在 懒汉模式 中单例对象 并不会在程序加载时创建而是在第一次调用时创建第一次调用创建后后续无需再创建直接使用即可
#pragma once#include iostream// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;public:static Signal *getInstance(){// 第一次调用才创建if (_sigptr nullptr){_sigptr new Signal();}return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 静态指针static Signal *_sigptr;
};// 初始化静态指针
Signal *Signal::_sigptr nullptr;
注意 此时的静态指针需要初始化为 nullptr方便第一次判断
饿汉模式 中出现的问题这里全都避免了
创建耗时 - 只在第一次使用时创建占用资源 - 如果不使用就不会被创建
懒汉模式 的核心在于 延时加载可以优化服务器的速度及资源占用
延时加载这种机制就有点像 「写时拷贝」就du你不会使用从而节省资源开销类似的还有 动态库、进程地址空间 等 当然懒汉模式 下也是可以正常使用 单例对象 的 3.2.3.饿汉模式VS懒汉模式
这样看来懒汉模式 确实优秀实现起来也不麻烦为什么会说 饿汉模式 更简单呢 这是因为当前只是单线程场景程序暂时没啥问题如果当前是多线程场景问题就大了如果一批线程同时调用 getInstance()同时认定 _sigptr 为空就会创建多个 单例对象这是不合理的
也就是说当前实现的 懒汉模式 存在严重的线程安全问题
如何证明 简单改一下懒汉模式的代码每创建一个单例对象就打印一条语句将代码放入多线程环境中测试 懒汉模式的获取单例对象句柄 getInstance() — 位于 Signal 类 #pragma once#include iostream// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;public:static Signal *getInstance(){// 第一次调用才创建if (_sigptr nullptr){std::cout 创建了一个单例对象 std::endl;_sigptr new Signal();}return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 静态指针static Signal *_sigptr;
};// 初始化静态指针
Signal *Signal::_sigptr nullptr; 源文件 main.cc 其中使用了 lambda 表达式来作为线程的回调函数重点在于查看现象
#include iostream
#include pthread.h
#include Signal.hppint main()
{// 创建一批线程pthread_t arr[10];for(int i 0; i 10; i){pthread_create(arr i, nullptr, [](void*)-void*{// 获取句柄auto ptr Signal::getInstance();ptr-print();return nullptr;}, nullptr);}for(int i 0; i 10; i)pthread_join(arr[i], nullptr);return 0;
}当前代码在多线程环境中同时创建了多个 单例对象因此是存在线程安全问题的 饿汉模式没有线程安全问题吗 没有因为饿汉模式下单例对象一开始就被创建了即便是多线程场景中也不会创建多个对象它们也做不到 3.2.4.懒汉模式线程安全版
有问题就解决解决多线程并发访问的利器是 互斥锁那就创建 互斥锁 保护单例对象的创建
#pragma once#include iostream
#include mutex// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;public:static Signal *getInstance(){// 加锁保护pthread_mutex_lock(_mtx);if (_sigptr nullptr){std::cout 创建了一个单例对象 std::endl;_sigptr new Signal();}pthread_mutex_unlock(_mtx);return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 静态指针static Signal *_sigptr;static pthread_mutex_t _mtx;
};// 初始化静态指针
Signal *Signal::_sigptr nullptr;// 初始化互斥锁
pthread_mutex_t Signal::_mtx PTHREAD_MUTEX_INITIALIZER;
注意 getInstance() 是静态函数互斥锁也要定义为静态的可以初始化为全局静态锁
依旧是借助之前的多线程场景测试一下改进后的 懒汉模式 代码有没有问题 没有问题 现在还面临最后一个问题效率问题
当前代码确实能保证只会创建一个 单例对象但即使后续不会创建 单例对象也需要进行 加锁、判断、解锁 这个流程要知道 加锁 也是有资源消耗的所以这种写法不妥
解决方案是DoubleCheck 双检查加锁
在 加锁 前再增加一层判断如此一来N 个线程顶多只会进行 N 次 加锁与解锁这是非常优雅的解决方案 获取静态对象句柄 getInstance() — 位于 Signal 类 static Signal *getInstance()
{// 双检查if(_sigptr nullptr){// 加锁保护pthread_mutex_lock(_mtx);if(_sigptr nullptr){std::cout 创建了一个单例对象 std::endl;_sigptr new Signal();}pthread_mutex_unlock(_mtx);}return _sigptr;
}为什么要两个if 单纯的 if 判断并不会消耗很多资源但 加锁 行为会消耗资源延缓程序运行速度双检查加锁 可以有效避免这个问题 懒汉模式最终版本1 #pragma once#include iostream
#include mutex// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;public:static Signal *getInstance(){// 双检查if (_sigptr nullptr){// 加锁保护pthread_mutex_lock(_mtx);if (_sigptr nullptr){std::cout 创建了一个单例对象 std::endl;_sigptr new Signal();}pthread_mutex_unlock(_mtx);}return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 静态指针static Signal *_sigptr;static pthread_mutex_t _mtx;
};// 初始化静态指针
Signal *Signal::_sigptr nullptr;// 初始化互斥锁
pthread_mutex_t Signal::_mtx PTHREAD_MUTEX_INITIALIZER;上面这个代码未必也太复杂了。 值得一提的是懒汉模式 还有一种非常简单的写法调用 getInstance() 时创建一个静态单例对象并返回因为静态单例对象只会初始化一次所以是可行的并且在 C11 之后可以保证静态变量初始化时的线程安全问题也就不需要 双检查加锁 了实现起来非常简单 懒汉模式最终版本2 #pragma once#include iostream
#include mutex// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal ) delete;public:static Signal *getInstance(){// 静态单例对象只会初始化一次并且生命周期随进程static Signal _sig;return _sig;}void print(){std::cout Hello Signal! std::endl;}
};
结果也是正常的所以如果当前的生产环境所支持的 C 版本为 C11 及以后在实现 懒汉模式 时可以选择这种简便的方式是非常不错的如果为了兼容性也可以选择传统写法
注意 静态变量创建时的线程安全问题在 C11 之前是不被保障的
3.3.线程池_V2最终版
有了 单例模式 的相关知识后就可以开始编写最终版线程池了
「最终版」将线程池改为 单例模式只允许存在一个线程池对象
这里选择 懒汉模式因为比较优秀并且为了确保兼容性选择 经典写法
也就是等到使用的时候再创建 首先是修改 ThreadPool 为单例模式然后提供一个获取 单例对象 的函数如果是第一次创建 单例对象就需要先创建对象。 获取单例对象的函数 // 获取线程池单例对象static ThreadPoolT *GetInstance(){if (nullptr tp_) // 如果线程池对象不存在则创建一个新的线程池对象{pthread_mutex_lock(lock_); // 加锁保证线程安全if (nullptr tp_) // 再次检查是否已经创建了线程池对象防止多线程环境下的竞争条件{std::cout log: singleton create done first! std::endl;tp_ new ThreadPoolT(); // 创建线程池对象}pthread_mutex_unlock(lock_); // 解锁}return tp_; // 返回线程池对象指针}// 线程池单例对象指针和互斥锁静态成员变量static ThreadPoolT *tp_;单例模式 改完了但现在面临一个尴尬的问题main.cc 无法直接将回调函数 callBack() 进行传递因为它根本无法创建对象 可以试试曲线救国将函数对象传递给 getInstance() 函数如果用户不传那就使用缺省参数也就是直接打印结果
总之修修改改后的线程池长这样 头文件 ThreadPool_V2.hpp #pragma once#include iostream
#include vector
#include string
#include queue
#include pthread.h
#include unistd.h// 线程信息结构体
struct ThreadInfo
{pthread_t tid; // 线程IDstd::string name; // 线程名称
};// 默认线程数量
static const int defalutnum 5;// 线程池模板类
template class T
class ThreadPool
{
private:// 互斥锁加锁函数void Lock(){pthread_mutex_lock(mutex_);}// 互斥锁解锁函数void Unlock(){pthread_mutex_unlock(mutex_);}// 唤醒等待的线程void Wakeup(){pthread_cond_signal(cond_);}// 线程休眠等待条件变量void ThreadSleep(){pthread_cond_wait(cond_, mutex_);}// 判断任务队列是否为空bool IsQueueEmpty(){return tasks_.empty();}// 根据线程ID获取线程名称std::string GetThreadName(pthread_t tid){for (const auto ti : threads_){if (ti.tid tid)return ti.name;}return None;}public:// 线程处理任务的函数static void *HandlerTask(void *args){ThreadPoolT *tp static_castThreadPoolT *(args);std::string name tp-GetThreadName(pthread_self());while (true){tp-Lock();while (tp-IsQueueEmpty()){tp-ThreadSleep();}T t tp-Pop();tp-Unlock();t.Run();std::cout name run, result: t.GetResult() std::endl;}}// 启动线程池中的所有线程void Start(){int num threads_.size();for (int i 0; i num; i){threads_[i].name thread- std::to_string(i 1);pthread_create((threads_[i].tid), nullptr, HandlerTask, this);}}// 从任务队列中取出一个任务T Pop(){T t tasks_.front();tasks_.pop();return t;}// 向任务队列中添加一个任务void Push(const T t){Lock();tasks_.push(t);Wakeup();Unlock();}// 获取线程池单例对象static ThreadPoolT *GetInstance(){if (nullptr tp_) // 如果线程池对象不存在则创建一个新的线程池对象{pthread_mutex_lock(lock_); // 加锁保证线程安全if (nullptr tp_) // 再次检查是否已经创建了线程池对象防止多线程环境下的竞争条件{std::cout log: singleton create done first! std::endl;tp_ new ThreadPoolT(); // 创建线程池对象}pthread_mutex_unlock(lock_); // 解锁}return tp_; // 返回线程池对象指针}private:// 构造函数初始化线程池可以指定线程数量默认为defalutnumThreadPool(int num defalutnum) : threads_(num){pthread_mutex_init(mutex_, nullptr); // 初始化互斥锁pthread_cond_init(cond_, nullptr); // 初始化条件变量}// 析构函数销毁线程池资源~ThreadPool(){pthread_mutex_destroy(mutex_); // 销毁互斥锁pthread_cond_destroy(cond_); // 销毁条件变量}// 禁止拷贝构造和赋值操作符确保线程池对象的单一性ThreadPool(const ThreadPoolT ) delete;const ThreadPoolT operator(const ThreadPoolT ) delete; // abc
private:// 线程信息列表std::vectorThreadInfo threads_;// 任务队列std::queueT tasks_;// 互斥锁和条件变量用于同步和通信pthread_mutex_t mutex_;pthread_cond_t cond_;// 线程池单例对象指针和互斥锁静态成员变量static ThreadPoolT *tp_;static pthread_mutex_t lock_;
};// 初始化线程池单例对象指针和互斥锁静态成员变量
template class T
ThreadPoolT *ThreadPoolT::tp_ nullptr;
template class T
pthread_mutex_t ThreadPoolT::lock_ PTHREAD_MUTEX_INITIALIZER;注意这个代码要求Task类必须包含Run函数和GetResult函数 它要怎么使用呢 其实很简单就下面这3步是最关键的
ThreadPoolTask::GetInstance()-Start(); Task t();//构造一个任务对象ThreadPoolTask::GetInstance()-Push(t); Task.hpp #pragma once#include iostream//任务类
class Task
{
public:Task(int x 0, int y 0, char op 0): _x(x), _y(y), _op(op){}~Task(){}//处理任务的方法void Run(){switch (_op){case :_result _x _y;break;case -:_result _x - _y;break;case *:_result _x * _y;break;case /:if (_y 0){std::cerr Error: div zero! std::endl;return;}else{_result _x / _y;}break;case %:if (_y 0){std::cerr Error: mod zero! std::endl;return;}else{_result _x % _y;}break;default:std::cerr operation error! std::endl;return;}}int GetResult(){return _result;}private:int _x;int _y;char _op;int _result;
}; 此时 main.cc 想要使用线程池对象时就得通过 getInstance() 获取句柄然后才能进行操作 源文件 main.cc #include ThreadPool_v1.hpp
#include Task.hpppthread_spinlock_t slock;int main()
{// 如果获取单例对象的时候也是多线程获取的呢std::cout process runn... std::endl;sleep(3);ThreadPoolTask::GetInstance()-Start();srand(time(nullptr) ^ getpid());const char* ops -*/%;while(true){//1. 构建任务int x rand() % 10 1;usleep(10);int y rand() % 5;char op ops[rand()%5];Task t(x, y, op);ThreadPoolTask::GetInstance()-Push(t);//2. 交给线程池处理std::cout main thread make task: t.GetResult() std::endl;sleep(1);}
}
此时是可以获取结果a的也可以看到一批线程正在候等任务到达 如何证明当前的 单例模式 生效了 在调用 GetInstance() 之前查看正在运行中的线程数量调用完后再次查看如果线程数量从 1 个变成多个就证明 单例模式 是生效的延迟加载 主线程先睡眠3秒 3秒后开始 这就是线程池完全版本。