线程过多会带来调度开销,进⽽影响缓存局部性和整体性能。⽽线程池维护着多个线程,等待着监督管理者分配可并发执⾏的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利⽤,还能防⽌过分调度。
1.基础版线程池
1.1 初始化
我们实现的是 固定线程数量的线程池。
这个线程池的实现会用到之前自己实现的线程、互斥锁、条件变量、日志。
- 线程封装:【Linux】多线程创建及封装
- 互斥锁封装:【Linux】线程的互斥
- 条件变量封装:【Linux】线程同步和生产者消费者模型
- 日志实现:【Linux】手搓日志(附源码)
cpp
//ThreadPool.hpp文件
#include <iostream>
#include <vector>
#include <string>
#include <queue> //任务队列
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "MyLog.hpp"
namespace MyThreadPool
{
using namespace MyThread;
using namespace MyMutex;
using namespace MyCond;
using namespace MyLog;
class ThreadPool
{
public:
ThreadPool() {}
~ThreadPool() {}
private:
};
}
cpp
//Main.cc文件
#include "ThreadPool.hpp"
using namespace MyThreadPool;
int main()
{
return 0;
}
bash
#Makefile
threadpool:Main.cc
g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
rm -f threadpool
首先成员函数要有管理这些线程的容器,就用vector,还要一个变量设置线程池的线程数量,
我们自己实现的线程要传一个任务去初始化(具体看对应的博客)。

所以这里我们首先需要一个任务函数,假设这个任务就是先获取线程名再用日志打印。
cpp
void Handler() // 返回值为void,参数为空
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获得线程名
while (true)
{
LOG(LogLevel::DEBUG) << name << "运行中..."; // 用日志打印
sleep(1);
}
}
然后创建多线程的时候以lambda表达式调用这个Hanlder初始化,lambda的捕捉列表捕捉this。
cpp
static const int defaultnum = 5;
class ThreadPool
{
public:
ThreadPool(int num = defaultnum)
: _num(num)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back([this](){ Handler(); }); // Lambda表达式
}
}
void Handler() // 返回值为void,参数为空
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获得线程名
while (true)
{
LOG(LogLevel::DEBUG) << name << "运行中..."; // 用日志打印
sleep(1);
}
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程数量
};
1.2 启动
线程有了之后我们还要启动线程。
cpp
void Start()
{
for (auto &thread : _threads)
{
thread.Start();
}
}
启动线程后,线程就会处理自己的任务,下面是自己实现的Thread的逻辑。

在Main.cc验证一下。
cpp
#include "ThreadPool.hpp"
using namespace MyThreadPool;
int main()
{
Refresh_Log_To_Console(); //开启日志,往显示器打印
ThreadPool tp;
tp.Start();
sleep(100);
return 0;
}

任务放在任务队列里,任务队列用模板,然后线程获取任务,获取任务的过程要加锁,没有任务的时候在条件变量下等待。
cpp
template <typename T>
class ThreadPool
{
public:
ThreadPool(int num = defaultnum)
: _num(num)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back([this](){ Handler(); }); // Lambda表达式
}
}
void Handler() // 返回值为void,参数为空
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获得线程名
while (true)
{
T t;
{
LockGuard lg(&_mutex); // 加锁
while(_task.empty()) // 是while不是if
{
_cond.Wait(&_mutex);// 队列为空的时候在条件变量下等
}
//到这里时队列里肯定有任务
t = _task.front(); // 获取任务
_task.pop();
}
t(); // 处理任务时不用在临界区内部
}
}
void Start()
{
for (auto &thread : _threads)
{
thread.Start();
}
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程数量
std::queue<T> _task; // 任务队列
Mutex _mutex; // 锁
Cond _cond; // 条件变量
};
获取任务的过程要在临界区内部,但是线程处理任务的时候不用在临界区内部处理,因为这个任务已经属于这个线程了。
1.3 退出
线程退出我们要设置一个标记位_isrunning,初始化为false,线程运行时设为true。
cpp
void Stop()
{
_isrunning = false;
}
线程退出时可能处于等待状态,也可能正在处理任务。
如果线程正在处理任务,要把任务处理完才能行,所以当_isrunning为false并且任务队列为空时才能退出。
cpp
void Handler() // 返回值为void,参数为空
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获得线程名
while (true)
{
T t;
{
LockGuard lg(&_mutex); // 加锁
while (_task.empty())
{
_cond.Wait(&_mutex); // 在条件变量下等
}
if (!_isrunning && _task.empty())
{
LOG(LogLevel::INFO) << name << "已退出, 任务队列无数据";
break;
}
t = _task.front(); // 获取任务
_task.pop();
}
t(); // 处理任务时不用在临界区内部
}
}
如果线程在条件变量下等待,我们需要先唤醒所有在等待的线程,这里可以加一个变量记录休眠的线程的数量,有线程在休眠的再唤醒,因为_sleep_num也会被所有线程访问,所以这里可以加个锁保证它的安全。
cpp
private:
void WakeUpAll()
{
LockGuard lg(&_mutex);
if (_sleep_num > 0) // 有线程在休眠
{
LOG(LogLevel::DEBUG) << "唤醒所有线程";
_cond.Broadcast(); // 唤醒所有线程
}
}
public:
void Stop()
{
_isrunning = false;
WakeUpAll();
}
有可能线程是因为要退出才被唤醒的,并且此时判断队列为空,然后又进入了等待状态,一直醒不过来,就退出不了,所以在条件变量下等待的判断条件还要修改,完整代码如下。
cpp
template <typename T>
class ThreadPool
{
private:
void WakeUpAll()
{
LockGuard lg(&_mutex);
if (_sleep_num > 0) // 有线程在休眠
{
LOG(LogLevel::DEBUG) << "唤醒所有线程";
_cond.Broadcast(); // 唤醒所有线程
}
}
public:
ThreadPool(int num = defaultnum)
: _num(num),
_isrunning(false),
_sleep_num(0)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back([this](){ Handler(); }); // Lambda表达式
}
}
void Handler() // 返回值为void,参数为空
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获得线程名
while (true)
{
T t;
{
LockGuard lg(&_mutex); // 加锁
while (_task.empty() && _isrunning) // 队列不为空并且没被退出
{
_sleep_num++;
_cond.Wait(_mutex); // 在条件变量下等
_sleep_num--;
}
if (!_isrunning && _task.empty())
{
LOG(LogLevel::INFO) << name << "已退出, 任务队列无数据";
break;
}
t = _task.front(); // 获取任务
_task.pop();
}
// t(); // 处理任务时不用在临界区内部
}
}
void Start()
{
if (_isrunning)
return; // 线程已经启动就不要重复启动
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
}
}
void Stop()
{
_isrunning = false;
WakeUpAll();
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程数量
std::queue<T> _task; // 任务队列
Mutex _mutex; // 锁
Cond _cond; // 条件变量
bool _isrunning; // 是否在运行
int _sleep_num; // 在休眠的线程的数量
};
验证一下前面的逻辑,因为现在我们还没有任务,所以模板参数就传个int,把任务处理那一句注释掉,不然会报错。
cpp
#include "ThreadPool.hpp"
//using namespace MyThreadPool;
int main()
{
Refresh_Log_To_Console(); //开启日志,往显示器打印
ThreadPool<int> tp;
tp.Start();
sleep(1);
tp.Stop();
sleep(1);
return 0;
}


1.4 等待
线程退出后,主线程要Join线程,所以这里还要一个Join的接口。
cpp
void Join()
{
if (_isrunning == true) // 线程还在运行就直接返回
return;
for (auto &thread : _threads)
{
thread.Join();
}
}
cpp
//Main.cc文件
#include "ThreadPool.hpp"
using namespace MyThreadPool;
int main()
{
Refresh_Log_To_Console(); //开启日志,往显示器打印
ThreadPool<int> tp;
tp.Start();
sleep(1);
tp.Stop();
tp.Join();
return 0;
}

1.5 任务队列
我们要给线程池一个接口,把任务入进去,入任务是有条件的,当线程池还在运行的时候才能入,如果线程是都停止了就不要再入任务了;入了一个任务我们需要让线程知道,怎么才叫让线程知道呢?如果线程全在休眠就唤醒一个线程。
cpp
private:
void WakeUpOne()
{
LOG(LogLevel::DEBUG) << "唤醒一个线程";
_cond.Signal();
}
public:
bool Equeue(const T &task)
{
if (_isrunning)
{
LockGuard lg(&_mutex);
_task.push(task); // 往队列里入任务
if (_threads.size() == _sleep_num) // 线程全在休眠就叫醒一个
WakeUpOne();
return true;
}
return false;
}
现在我们需要加上任务,任务有如下两种形式。
cpp
//Task.hpp文件
#include <iostream>
#include <string>
#include <functional>
#include "MyLog.hpp"
using namespace MyLog;
// 任务形式2
using Task_2 = std::function<void()>; // 返回值void,参数为空的函数类型
void Flush()
{
LOG(LogLevel::DEBUG) << "我是一个刷新的任务";
}
// 任务形式1
class Task_1
{
public:
Task_1(int a, int b) : _a(a), _b(b), _result(0)
{
}
void Excute()
{
_result = _a + _b;
}
std::string ResultToString()
{
return std::to_string(_a) + "+" + std::to_string(_b) + "=" +
std::to_string(_result);
}
std::string DebugToString()
{
return std::to_string(_a) + "+" + std::to_string(_b) + "=?";
}
private:
int _a;
int _b;
int _result;
};
有了任务,主线程就可以用往队列里入任务,可以隔1秒入一个。
cpp
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace MyThreadPool;
int main()
{
Refresh_Log_To_Console(); // 开启日志,往显示器打印
ThreadPool<Task_2> tp;
tp.Start();
int task_num = 5;
while (task_num) // 往队列里入5个任务
{
tp.Equeue(Flush);
task_num--;
sleep(1);
}
tp.Stop();
tp.Join();
return 0;
}

到这里我们的线程池基础版就完成了。整体代码如下。
cpp
//ThreadPool.hpp文件
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "MyLog.hpp"
namespace MyThreadPool
{
using namespace MyThread;
using namespace MyMutex;
using namespace MyCond;
using namespace MyLog;
static const int defaultnum = 5;
template <typename T>
class ThreadPool
{
private:
void WakeUpAll()
{
LockGuard lg(&_mutex);
if (_sleep_num > 0) // 有线程在休眠
{
LOG(LogLevel::DEBUG) << "唤醒所有线程";
_cond.Broadcast(); // 唤醒所有线程
}
}
void WakeUpOne()
{
LOG(LogLevel::DEBUG) << "唤醒一个线程";
_cond.Signal();
}
public:
ThreadPool(int num = defaultnum)
: _num(num),
_isrunning(false),
_sleep_num(0)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back([this](){ Handler(); }); // Lambda表达式
}
}
void Handler() // 返回值为void,参数为空
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获得线程名
while (true)
{
T t;
{
LockGuard lg(&_mutex); // 加锁
while (_task.empty() && _isrunning) // 队列不为空并且没被退出
{
_sleep_num++;
_cond.Wait(_mutex); // 在条件变量下等
_sleep_num--;
}
if (!_isrunning && _task.empty())
{
LOG(LogLevel::INFO) << name << "已退出, 任务队列无数据";
break;
}
t = _task.front(); // 获取任务
_task.pop();
}
t(); // 处理任务时不用在临界区内部
}
}
void Start()
{
if (_isrunning)
return; // 线程已经启动就不要重复启动
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
}
}
void Join()
{
if (_isrunning == true)
return;
for (auto &thread : _threads)
{
thread.Join();
}
}
void Stop()
{
_isrunning = false;
WakeUpAll();
}
bool Equeue(const T &task)
{
if (_isrunning)
{
LockGuard lg(&_mutex);
_task.push(task); // 往队列里入任务
if (_threads.size() == _sleep_num) // 线程全在休眠就叫醒一个
WakeUpOne();
return true;
}
return false;
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程数量
std::queue<T> _task; // 任务队列
Mutex _mutex; // 锁
Cond _cond; // 条件变量
bool _isrunning; // 是否在运行
int _sleep_num; // 在休眠的线程的数量
};
}
2.单例模式
单例模式就是某个类只允许实例化出一个对象,在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中,此时往往要⽤⼀个单例的类来管理这些数据。
单例模式的类我们需要在语法上约束这个类,比如说可以把这个类的构造函数设置成private,或者禁用拷贝、赋值之类的接口,这样就没办法创建对象了。
那没办法创建对象怎么去创建一个对象呢?可以在类内以static的方式创建一个。
1.1 饿汉模式和懒汉模式
创建单例有两种创建方法,一种叫饿汉模式,一种叫懒汉模式。比如说拿吃饭洗碗的场景为例,
- 饿汉模式就是吃完饭立即去洗碗,这样下次吃饭的时候就可以立即吃
- 懒汉模式就是吃完饭了先不洗碗,等下次要吃饭之前在洗碗
下面是一个饿汉模式的伪代码。
cpp
//饿汉模式
template <typename T>
class Singleton
{
static T data; // 1
public:
static T *GetInstance()
{
return &data; // 2
}
};
在1处定义了一个static的T类型的对象,静态成员属于类,不属于对象,当Singleton类被加载到内存的时候对象自然就被创建出来了,因为这个data是私有的,所以提供了一个函数获取这个data,就是在2处。
被static修饰的变量作用域不变,生命周期会变成全局的,全局变量会在进程地址空间的全局数据区开辟空间,进程在加载时,会把自己的代码区、全局数据区在进程加载的时候就直接创建出来,像堆区和栈区是运行期间才创建的,所以饿汉模式下我们一旦把代码编译好加载到内存,这个对象就直接存在了,所以将来我们想用这个变量的时候直接用就可以了。
当这个类特别大的时候,把数据全部加载到内存效率很低。
大多数情况下我们都是选择懒汉模式,伪代码如下。
cpp
//懒汉模式
template <typename T>
class Singleton
{
static T *inst; // 1
public:
static T *GetInstance()
{
if (inst == NULL)
{
inst = new T(); // 2
}
return inst;
}
};
我们在1处定义一个static的T类型的对象的指针,我们在调用GetInstance函数的时候,先判断这个指针是否为空,为空就再创建对象,对象一旦创建,这个指针就不为空了,就不会再new对象了。懒汉模式的思想我们其实早就接触过了。
懒汉模式最核心的思想其实就是延时加载,从而优化服务器的启动速度。
1.2 将进程池改为单例模式
我们将进程池改为单例模式的前提是它可以被改,进程池我们不需要创建多的,一个就够了,所以可以改为单例模式。
- 构造函数私有化:构造函数还是要有的,因为我们还是要创建一个对象的。
- 禁用拷贝构造:不允许别人对线程池进行拷贝、赋值等操作
- 设置单例指针,设计获取单例的接口:要调用获取单例的接口,首先要有一个对象,但是要有对象必须要调用单例获取...所以获取单例的接口要设置为static的,就可以不需要类对象就能调用。
cpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "MyLog.hpp"
namespace MyThreadPool
{
using namespace MyThread;
using namespace MyMutex;
using namespace MyCond;
using namespace MyLog;
static const int defaultnum = 5;
template <typename T>
class ThreadPool
{
private:
void WakeUpAll()
{
LockGuard lg(&_mutex);
if (_sleep_num > 0) // 有线程在休眠
{
LOG(LogLevel::DEBUG) << "唤醒所有线程";
_cond.Broadcast(); // 唤醒所有线程
}
}
void WakeUpOne()
{
LOG(LogLevel::DEBUG) << "唤醒一个线程";
_cond.Signal();
}
void Start() // Start函数最好也设为私有
{
if (_isrunning)
return; // 线程已经启动就不要重复启动
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
}
}
ThreadPool(int num = defaultnum) // 构造函数私有
: _num(num),
_isrunning(false),
_sleep_num(0)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back([this](){ Handler(); }); // Lambda表达式
}
}
// 禁用赋值和拷贝
ThreadPool(const ThreadPool<T> &) = delete;
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
public:
// static的类不能访问类成员变量和函数,但是可以访问被static修饰的
static ThreadPool<T> *GetInstance()
{
if (_inc == nullptr)
{
LOG(LogLevel::DEBUG) << "首次使用, 创建对象";
_inc = new ThreadPool<T>; // 创建对象
_inc->Start(); // 创建之后就启动
}
return _inc;
}
void Handler() // 返回值为void,参数为空
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name)); // 获得线程名
while (true)
{
T t;
{
LockGuard lg(&_mutex); // 加锁
while (_task.empty() && _isrunning) // 队列不为空并且没被退出
{
_sleep_num++;
_cond.Wait(_mutex); // 在条件变量下等
_sleep_num--;
}
if (!_isrunning && _task.empty())
{
LOG(LogLevel::INFO) << name << "已退出, 任务队列无数据";
break;
}
t = _task.front(); // 获取任务
_task.pop();
}
t(); // 处理任务时不用在临界区内部
}
}
void Join()
{
if (_isrunning == true)
return;
for (auto &thread : _threads)
{
thread.Join();
}
}
void Stop()
{
_isrunning = false;
WakeUpAll();
}
bool Equeue(const T &task)
{
if (_isrunning)
{
LockGuard lg(&_mutex);
_task.push(task); // 往队列里入任务
if (_threads.size() == _sleep_num) // 线程全在休眠就叫醒一个
WakeUpOne();
return true;
}
return false;
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程数量
std::queue<T> _task; // 任务队列
Mutex _mutex; // 锁
Cond _cond; // 条件变量
bool _isrunning; // 是否在运行
int _sleep_num; // 在休眠的线程的数量
static ThreadPool<T> *_inc; // 单例指针
};
// static成员在类外初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::_inc = nullptr;
}
使用方式如下,只能调用GetInstance函数,而且只有首次使用时会创建。
cpp
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace MyThreadPool;
int main()
{
Refresh_Log_To_Console(); // 开启日志,往显示器打印
int task_num = 5;
while (task_num) // 往队列里入10个任务
{
ThreadPool<Task_2>::GetInstance()->Equeue(Flush); // 创建单例
task_num--;
sleep(1);
}
ThreadPool<Task_2>::GetInstance()->Stop();
ThreadPool<Task_2>::GetInstance()->Join();
return 0;
}

线程池现在是单例模式,而且此时是一个生产者多个消费者,但是如果线程池本身或被多个线程获取呢?也就是如果是多生产者多消费者怎么办呢?此时的单例获取并不是线程安全的,所以我们要加所。
但是这把锁不是之前的锁_mutex,我们还需要再定义一把锁用来保护这个单例,这把锁也要是static的,因为之前的锁_mutex是类内的成员属性,static的函数不能访问内类成员,还有一个原因是在我们创建单例的逻辑里,此时还不存在对象呢,那么此时对象里的锁_mutex也就不存在。
cpp
namespace MyThreadPool
{
//...
template <typename T>
class ThreadPool
{
private:
//...
public:
// static的类不能访问类成员变量和函数,但是可以访问被static修饰的
static ThreadPool<T> *GetInstance()
{
LockGuard lg(&_lock); // 保护单例
if (_inc == nullptr)
{
LOG(LogLevel::DEBUG) << "首次使用, 创建对象";
_inc = new ThreadPool<T>; // 创建对象
_inc->Start(); // 创建之后就启动
}
LOG(LogLevel::DEBUG) << "获取线程池单例";
return _inc;
}
//...
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程数量
std::queue<T> _task; // 任务队列
Mutex _mutex; // 锁
Cond _cond; // 条件变量
bool _isrunning; // 是否在运行
int _sleep_num; // 在休眠的线程的数量
static ThreadPool<T> *_inc; // 单例指针
static Mutex _lock; // 保护单例的锁
};
// static成员在类外初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::_inc = nullptr;
template <typename T>
Mutex ThreadPool<T>::_lock;
}
但是呢,这个单例就只获取一次,而线程却要每次都在这里申请锁还要等,效率太低,所以我们再加一个判断。
cpp
namespace MyThreadPool
{
//...
template <typename T>
class ThreadPool
{
private:
//...
public:
// static的类不能访问类成员变量和函数,但是可以访问被static修饰的
static ThreadPool<T> *GetInstance()
{
if (_inc == nullptr) // 双重判断
{
LockGuard lg(&_lock); // 保护单例
if (_inc == nullptr)
{
LOG(LogLevel::DEBUG) << "首次使用, 创建对象";
_inc = new ThreadPool<T>; // 创建对象
_inc->Start(); // 创建之后就启动
}
LOG(LogLevel::DEBUG) << "获取线程池单例";
}
return _inc;
}
//...
private:
//...
static ThreadPool<T> *_inc; // 单例指针
static Mutex _lock; // 保护单例的锁
};
// static成员在类外初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::_inc = nullptr;
template <typename T>
Mutex ThreadPool<T>::_lock;
}
3.死锁问题
死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占⽤不会
释放的资源⽽处于的⼀种永久等待状态。
申请⼀把锁是原⼦的,但是申请两把锁就不⼀定了。
假如现在线程A和B分别持有锁1和锁2,就会出现如下情况。
如上这种互相申请对方的锁而不释放自己的锁的情况就是死锁。
死锁产生的4个必要条件:
- 互斥条件:⼀个资源每次只能被⼀个执⾏流使⽤,就是用锁了,不用锁不就不会产生死锁了嘛。
- 请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放
- 不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺,比如一般情况下A申请的锁A自己释放,剥夺的就是A申请的锁B给释放了,然后B去申请锁。
- 循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系,破环这个条件就可以让申请锁1的才可以申请锁2这种解决方法。
避免死锁:破环这4个条件的任意一个
本篇分享就到这里,我们下篇见~
