本章目标
1.单例模式的线程池
1.单例模式的线程池
1.1线程池的实现
在前面,我们通过匿名管道实现过主从模式的进程池,但是对于进程来说,它的消耗相对于线程来说,是非常的大的.它不仅需要创建内核的数据结构,同时也需要拷贝处一份虚拟地址空间.光着两项它,就已经占了不少的内存和时间了.
我们进行选择实现的线程池,它都是在一个进程内部的执行流.用同一个虚拟地址空间.能够更加轻量的去完成任务.因为对于线程来说,它主要是在用户态就能跑完,也不需要过多的系统调用.
着也就导致了,它并不会占用过多的内核资源.
对于线程池,我们同样,会在任务到来之前启动一批线程.在任务没有到来之前同样会让所有任务休眠

而我们的线程它同样会有产生任务,或者消耗任务的线程.所以它本质上依旧是一个生产消费者模型.在这里.我们就可以使用我们前一节写的阻塞队列或者循环队列的方式去实现这个线程池.我们今天就选择阻塞队列方式去实现.
我们下面用的所有的组件,从线程,条件变量.锁,日志,所有模块都是封装好的面向对象的方式.去实现的.我们在这里就只说这个线程池的具体实现方式.其他组件的实现可以去看前几章具体的情况.
cpp
private:
std::vector<Thread> _pool; // 线程池
std::queue<T> _task_queue; // 任务队列
Mutex _lock;
Cond _cond;
bool _isrunning;
int _sleep_thread; // 在条件变量上的线程
int _server_thread; // 线程个数
我们用一个vector去实现这个对线程的管理
线程池的初始化
cpp
Thread_Pool(int server_thread = Thread_num)
: _server_thread(server_thread), _sleep_thread(0), _isrunning(false)
{
WINDOWS_LOG_INITAL()
for (int i = 0; i < server_thread; i++)
{
_pool.emplace_back([this]() -> void
{ this->handertask(); });
}
}
我们开的线程的个数可以自己规定,但是这里给了个缺省参数是5个.
同时对于这个线程的构造选择用emplace back.这个对于自定义的类型可以减少一次拷贝赋值.直接在容器里面构造.我们底层的Thread会到时候回调我们这个lambda表达式.而我们lambda表达式由传了一格函数,它会回调到,我们线程池的一格所有的公共处理方法之上
cpp
void handertask()
{
char name[1024];
memset(name, 0, sizeof name);
pthread_getname_np(pthread_self(), name, sizeof name);
while (true)
{
T task;
{
Mutex_Grard guard(_lock);
// 线程正在运行 1.任务队列不能为空,不休眠,如果为空进入条件变量等待
while (_task_queue.empty() && _isrunning)
{
_sleep_thread++;
_cond.wait(_lock);
_sleep_thread--;
}
if (_task_queue.empty() && !_isrunning)
{
// 线程退出,如果还有任务继续执行,没有就解锁退出
// _lock.unLock();
break;
}
task = _task_queue.front();
_task_queue.pop();
}
LOG(LOGLEVEL::INFO) << "thread-" << name << " 正在执行任务";
task(); // 规定任务这样执行
LOG(LOGLEVEL::DEBUG) << "task finish" << task.Result();
}
LOG(LOGLEVEL::DEBUG) << "thread-quit";
}
在这里,我们使用RAII的风格的锁.
在当前任务队列当中的任务个数如果为空,并且线程池正在运行.我们就让它进入条件变量等待.
这个地方就是我们的消费者方法.如果当前线程池如果,没有运行.我们就直接break,让线程自己主动退出.让它顺利的走到退出逻辑.
对于外部传进来的任务,我们选择以task()的方式约定好,就这么执行.这也就说明一件事.我们外部传进来的任务.一定是个函数.或者是个类,类里面要用仿函数去实现对应的重载.
cpp
void Enqueue(T task)
{
{
Mutex_Grard guard(_lock);
_task_queue.push(task);
if (_sleep_thread > 0)
_cond.signal();
}
}
对于生产任务的方式.同样需要加锁.如果生产完任务之后,如果,我们在条件变量上还有等待的线程.需要唤醒它,让它去消费对应生产的任务.
cpp
void Stop()
{
{
Mutex_Grard guard(_lock);
_isrunning = false;
if (_sleep_thread > 0)
_cond.broadcast();
}
}
void Join()
{
for (auto &e : _pool)
{
e.Join();
}
}
退出和回收.回收逻辑很好说,直接将进程池当中的所有线程都等待了就行.但是这个线程池退出,我们需要设置标记位的同时,要让将所有在条件变量上等待的线程都唤醒
cpp
if (_task_queue.empty() && !_isrunning)
{
// 线程退出,如果还有任务继续执行,没有就解锁退出
// _lock.unLock();
break;
}
让他们去走这段逻辑.这样线程才能够正常退出.
这个地方可以主动解锁,但是不需要,对于RAII的锁来说,它一旦出作用域就会主动的解锁.
线程池实现
1.2单例模式
所谓的单例模式就是全局上整个进程从开始到结束.一个类只能够有一个对象.
如果想去实现这个单例,最主要要考虑的是对于一个对象它创建是在什么时机会被创建.
一般来说只有两个
1.当这个进程刚开始跑起来的时候会被创建.
2.当真正被调用了才会被创建.
这里也就对应两种单例模式的实现.
饿汉模式,与懒汉模式
这两个相对来说,饿汉式是比较好实现的.
1.2.1饿汉模式
我们先说这个.所谓的饿汉就是提前加载.也就是对应着我们说的情况1.
而这个刚开始跑起来就创建的对象往往是全局的,或者是静态的对象.对于前者,我们就不考虑了,我们做不到,我们是能够把这个对象声明成静态的.
然后再它调用的时候时候直接返回这个指针.
cpp
static Thread_Pool<T> singletion1; // 饿汉单例
//类外初始化
template <typename T>
Thread_Pool<T> Thread_Pool<T>::singletion1;
应为这个对象是整个进程就有一个,所以我们需要提供一个属于类整体的静态函数去直接返回.这个单例
cpp
static Thread_Pool<T> &Getinstance(int a)
{
(void)a;
return singletion1;
}
对于单例模式来说,它的全局上只能够有一个对象,所以,它需要将自身的拷贝函数以及赋值运算符重载直接禁止,将构造函数私有化.
cpp
Thread_Pool(const Thread_Pool &) = delete;
Thread_Pool &operator=(const Thread_Pool &) = delete;
对于饿汉模式的单例,它的核心点是预先加载.
1.2.2懒汉模式
懒汉模式就是我们说的第二种,在真正调用的时候才会创建.但是应为它是在程序运行中的时候才会被创建,要做的工作也就更多.它需要保证自己的线程安全.但是应为它是懒加载,在实际中的应用,也就更多.
cpp
static Thread_Pool<T> *singletion; // 懒汉单例
static Mutex singletionlock; // 懒汉锁
// 静态成员类外初始化
template <typename T>
Thread_Pool<T> *Thread_Pool<T>::singletion = nullptr;
template <typename T>
Mutex Thread_Pool<T>::singletionlock;
cpp
static Thread_Pool<T> *Getinstance()
{
// 防止堆积去抢锁,提高效率
if (singletion == nullptr)
{
{
Mutex_Grard guard(singletionlock);
if (singletion == nullptr)
{
singletion = new Thread_Pool<T>();
singletion->Start();
LOG(LOGLEVEL::INFO) << "线程池第一次使用";
}
}
}
return singletion;
}
具体实现方式如上,在这里加锁的逻辑能够理解,防止有多个线程同时进入.但是这么做有性能消耗.位了解决这个问题,我们设置了双重判断的方式,这里的解决方法还有很多.也可以设置标记位进行判断.
除了这种懒汉单例还有一种
cpp
static Thread_Pool<T> *Getinstance()
{
static Thread_Pool<T> singletion;
return &singletion;
}
懒汉模式实现单例的方式,核心是拖着,直到调用才加载,懒加载
单例模式实现的线程池