线程池
通过之前的学习,了解了Linux
线程相关知识,我们可以创建新线程,让新线程去调用函数;
这样我们就可以给新线程分配任务,让新线程去执行,主线程等待
这里如果有任务,再去创建线程,让线程执行,这就会导致多次调用系统调用创建线程,进而影响缓冲局部性和整体性能。
线程池,是一种线程使用模式,通过预先创建线程来减少系统调用的次数。
线程池,和之前实现的进程池一样,预先创建并维护多个线程,这些线程等待分配任务(阻塞等待);当有任务时,唤醒其中的一些线程去执行这些任务;
这样不仅能保证内核的充分利用,还能防止过分调度
线程池使用场景:
- 需要大量线程来完成任务,且完成任务的时间比较短;例如
WEB
负服务器完成网页请求。 - 对性能要求苛刻的应用;例如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于服务器因此产生大量线程的应用。
对于线程池,可以简单分为两种:
- 固定数量线程池:创建固定数量的线程,循环从人物队列中获取任务对象,获取到任务对象之后执行任务对象中的接口。
- 浮动线程池,线程的数量可以根据任务对象数量动态调整。

设计与实现
简单了解了线程池,现在来设计一个创建固定线程个数的线程池。(这里面默认设置线程数量为5
)
首先,线程池肯定要有线程吧,这里就复用之前是封装好的Thread
,使用vector
将这些线程存储起来。
其次,也要有任务,这里就使用queue
来存储这些任务;
然后,为了保证任务队列中没有任务时,线程要阻塞等待;任务队列中有任务时,要有线程去处理任务;就要存在互斥信号量mutex
以及条件变量cond
(这里使用之前封装好的Mutex
和Cond
)
最后,在线程池中,我们也可以使用记录一下线程的数量,以及线程的运行状态。
cpp
template <typename T>
class Threadpool
{
private:
std::vector<Thread> _threadpool; // 线程
std::queue<T> _taskqueue; // 任务队列
Mutex _mutex; // 互斥量
Cond _cond; // 条件变量
int _num; // 线程数量
bool _runing; // 运行状态
};
1. 构造与析构函数
对于一个线程池,创建线程池对象时,要初始化其中的每一个线程;
要初始换线程,就要存在一个func_t
类型的函数(using func_t = std::function<void()>;
),这里我们可以使用lambda
表达式,其中去调用Threadpool
的类内方法HandlerTask
。
对于析构函数,我们要回收线程池中所有的线程;
这里就可以实现一个Join
方法,在使用时就可以调用Join
回收线程池中的线程;而在析构函数中也要调用Join
方法。
cpp
Threadpool(int num = default_num) : _num(num), _runing(false)
{
for (int i = 0; i < num; i++)
{
_threadpool.emplace_back(
[this]()
{
HandlerTask();
})
}
}
void HandlerTask()
{
// 线程执行函数...
}
void Join()
{
for(auto& thread : _threadpool)
{
thread.Join();
}
}
~Threadpool()
{
Join();
}
2. 运行线程池
这里我们复用Thread
的代码,在创建出线程池对象时,其实并没有创建线程,线程池也并没有运行起来;
这里就设计实现一个Start
方法,来启动线程池(创建线程),让线程池运行起来;并且实现HandlerTask
方法(存在BUG
)
启动线程池
实现
Start
方法很简单,只需要一次调用每一个线程的Start
方法,创建线程即可。
实现HandlerTask
HandlerTask
方法,线程池所创建的每一个线程都要执行方法;所以该方法就要实现线程从线程池中取任务,并且执行该任务。
- 如果线程池任务队列中存在任务,线程取出并执行该任务。
- 如果线程池任务队列没有任务,线程就要阻塞等待。
注意:任务队列对于多线程来说,就是临界资源;为了保证数据一致,线程在访问时就要现申请临界资源(申请失败也要阻塞等待)
cpp
void HandlerTask()
{
// 线程执行函数...
while (true)
{
LockGroup lgp(_mutex);
{
while (_taskqueue.empty())
{
_cond.Wait(_mutex.GetMutex());
}
// 取任务
T task = _taskqueue.front();
_taskqueue.pop();
}
// 处理任务
sleep(1);
}
}
void Start()
{
_runing = true;
for(auto& thread : _threadpool)
{
thread.Start();
}
}
到这里,实现的线程池已经可以运行起来了;这里测试由于没有像任务队列中存放任务,所有的线程都要阻塞等待。
cpp
int main()
{
Threadpool<int> tpl;
tpl.Start();
sleep(10);
return 0;
}

3. 终止线程池
上述代码,线程池已经可以运行起来了,但是,存在一个BUG
:
经过测试也不难发现,在程序sleep
10秒后,程序要退出,调用Threadpool
析构函数,主线程等待并回收新线程;
但是,此时所有的新线程都处于阻塞等待状态,主线程要阻塞等待新线程退出;所以就导致所有的线程都在阻塞等待。
所以,这里我们就要实现一个方法,来终止线程池
首先,终止线程池,将线程池的运行状态设置成
false
。
但是,将线程设置成false
之后,其他线程有的可能在执行任务,有的可能在阻塞等待;难道线程池都结束了,这些等待中的线程还用阻塞等待着吗?
所以,将线程池状态设置成
false
之后,要唤醒所有正在等待的线程
唤醒所有等待的线程之后,问题又来了:
- 如果此时任务队列中没有任务,这些线程就会再次进入循环,阻塞等待。
- 如果此时任务队列中存在任务,这些线程是直接退出,还是继续执行任务?
对于第一个问题,这里就需要修改上面的
HandlerTask
函数,至于如何修改,先看第二个问题;如果线程池终止了,此时线程池还存在任务,这里就要线程继续执行任务,直到将任务队列中的任务执行完。
所以,线程要阻塞等待的条件不再是 任务队列为空(
_taskqueue.empty()
);而是任务队列为空,并且当前线程池正在运行(_taskqueue.empty() && _runing)
。而线程阻塞等待的条件变了,那线程在取任务之前,任务队列就可能为空(线程池运行状态为
false
,任务队列为空);所以,在先去从任务队列中取任务之前,要先判断任务队列是否为空;如果为空,当前线程池就结束了,直接
break
即可;(break
后,函数结束,线程也就结束了)如果不为空,才取任务执行任务。
所以,要实现的Stop
、WeakUp
和修改后的HandlerTask
:
cpp
static int default_num = 5;
template <typename T>
class Threadpool
{
void Weakup()
{
LOG(Level::DEBUG) << "唤醒所有线程";
_cond.Broadcast();
}
public:
void HandlerTask()
{
while (true)
{
LockGroup lgp(_mutex);
{
while (_taskqueue.empty() && _runing)
{
_cond.Wait(_mutex.GetMutex());
}
// 取任务
if (_taskqueue.empty())
{
LOG(Level::DEBUG) << "线程退出";
break;
}
T task = _taskqueue.front();
_taskqueue.pop();
}
// 处理任务
sleep(1);
}
}
void Stop()
{
_runing = false;
Weakup();
}
private:
std::vector<Thread> _threadpool; // 线程
std::queue<T> _taskqueue; // 任务队列
Mutex _mutex; // 互斥量
Cond _cond; // 条件变量
int _num; // 线程数量
bool _runing; // 运行状态
};
这里实现Weakup
方法,不希望被调用,只在内部使用,就设置成私有。

单例模式
什么是单例模式呢?
就好比现实生活中,一个人只能存在一个合法夫妻一样;
在上述实现的线程池代码中,可以发现线程池整体还是很大的;如果存在多个线程池对象,是十分消耗内存资源的。
在很多服务器开发场景中,经常要让服务器加载很多数据,这是就需要一个单例的来管理这些数据。
简单来说,单例模式就是在内存中只能存在一个对象。
1. 饿汉模式
对于饿汉实现,抽象来说就是:
吃完饭,立即洗碗,在每次吃饭时都可以直接吃。
我们就可以理解为,先创建对象,这样在使用时就可以直接调用。
cpp
template <typename T>
class Singleton
{
static T data;
public:
static T *GetInstance()
{
return &data;
}
};
这样每次在使用时,通过类直接调用静态成员函数。
此外,单例模式只允许存在一个对象,所以拷贝构造,拷贝赋值等都要私有化
private
或者删除delete
。
2. 懒汉模式
对于懒汉模式,抽象来说:吃完饭,先不洗碗,在下次吃饭前再洗。
我们就可以理解为,先不创建对象,在要使用时再创建对象。
这里懒汉实现方式核心思想就是 :延时加载,在使用时在创建对象,优化服务器是启动速度
cpp
template <typename T>
class Singleton
{
static T *inst;
public:
static T *GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
3. 实现懒汉单例模式
对于懒汉模式,存在一个问题:线程不安全
在第一次调用GetInstance
时,多线程同时访问GetInstance
时,就会创建出多个对象
这里就要对其进行加锁
cpp
template <typename T>
class Singleton
{
volatile static T *inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T *GetInstance()
{
if (inst == NULL)
{ // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.
lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
这样实现的单例模式,使用起来也太不方便了,这里将其设计到线程池内部方便使用;
cpp
class Threadpool // 注:类名这里应该是 ThreadPool<T>(代码笔误,否则模板无法生效)
{
public:
static ThreadPool<T> *GetInstance()
{
// 第一次检查:未加锁,快速判断实例是否已存在(避免每次调用都加锁,提高效率)
if (inc == nullptr)
{
// 加锁:确保多线程下只有一个线程能进入"创建实例"的逻辑
LockGuard lockguard(_lock);
LOG(LogLevel::DEBUG) << "获取单例....";
// 第二次检查:加锁后再次判断(防止多线程同时通过第一次检查,重复创建实例)
if (inc == nullptr)
{
LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
inc = new ThreadPool<T>(); // 创建线程池实例
inc->Start(); // 启动线程池(初始化线程、开始接收任务等,具体逻辑在 Start() 中)
}
}
return inc; // 返回唯一实例指针
}
private:
// 静态成员:单例实例指针 + 互斥锁(保证线程安全)
static ThreadPool<T> *inc;
static Mutex _lock;
};
// 静态成员的类外初始化(模板类必须在类外显式初始化静态成员)
template <typename T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;
线程安全和重入
- 线程安全:多线程同时访问代码 / 共享资源时,结果与单线程执行一致,无数据损坏或逻辑混乱。核心解决 "共享资源竞争",常用互斥锁、原子操作、无共享设计实现。
- 重入问题:函数被中断后重新进入(如中断 / 嵌套调用)仍能正确执行。关键看是否依赖静态 / 全局变量(依赖则不可重入,仅用局部变量通常可重入)。
- 关系:可重入≠线程安全(可重入函数多线程调用仍可能因共享资源不安全),线程安全≠可重入(线程安全函数若加锁,重入可能死锁);理想状态是 "可重入且线程安全"(如纯函数)。