文章目录
- 🌈 一、线程池的概念
- 🌈 二、线程池的应用场景
- 🌈 三、线程池的实现
🌈 一、线程池的概念
-
线程池 (thread pool) 是一种利用池化技术的线程使用模式。
-
虽然创建线程的代价比创建进程的要小很多,但小并不意味着没有。如果每次处理任务都要创建线程,积少成多下,代价还是蛮高的。
-
而线程池就可以解决这种问题,提前先创建出一批线程,没任务时,让这些线程泡在池子里睡觉,来任务后,将这些线程从池子里叫起来干活。
- 水池里流淌的是水流,而线程池里流淌的则是执行流 (线程)。
-
不要被线程池这个称呼给套住了,本质上来说,线程池 = 任务队列 + 条件变量 + 多执行流。
- 当任务队列中没任务时,让一堆线程在指定条件变量下等待;
- 当任务队列中有任务时,会唤醒在条件变量下等待的线程,让被唤醒的线程从任务队列中获取任务。
-
说到底,线程池本质上就是个生产消费模型。
🌈 二、线程池的应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短。
- 例如 web 服务器完成网页请求,这种单个任务小,但任务量大的任务,就很适合使用线程池技术。
- 对性能要求苛刻的应用,要求服务器迅速响应客户请求。
- 接收突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
- 在没有线程池的情况下,如果没有线程池,短时间内将产生大量线程,可能使内存到达极限。
🌈 三、线程池的实现
-
当前要实现一个主线程不停的往任务队列中放任务,然后让线程池中的线程从任务队列中获取任务,再进行任务处理的一个简易线程池。
-
实现的这个简易的线程池整体分为线程池部分代码、任务类型部分代码、主线程部分代码这三部分。
-
这三部分代码可以分别写在三个文件中 (记得包含对应头文件),也可以放在一个文件中,此处为了方便演示就全写在一个文件中。
#include <queue>
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using std::cerr;
using std::cout;
using std::endl;
using std::queue;
/* ---------- 任务类型代码设计 ---------- */
class task
{
private:
int _x; // 左操作数
int _y; // 右操作数
char _op; // 操作符
public:
task(int x = 0, int y = 0, char op = 0)
: _x(x), _y(y), _op(op)
{}
// 处理任务的方法
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)
{
cerr << "除零错误" << std::endl;
return;
}
else
{
result = _x / _y;
}
break;
case '%':
if (_y == 0)
{
std::cerr << "模零错误" << std::endl;
return;
}
else
{
result = _x % _y;
}
break;
default:
cerr << "非法运算符" << endl;
return;
}
cout << "新线程 [" << pthread_self() << "] 获取任务成功, 任务的处理结果为: "
<< _x << " " << _op << " " << _y << " = " << result << endl;
}
~task()
{}
};
/* ---------- 线程池代码设计 ---------- */
#define NUM 5 // 任务队列的容量
template <typename T>
class thread_pool
{
private:
queue<T> _task_queue; // 任务队列
int _thread_num; // 线程池中线程的数量
pthread_mutex_t _mutex; // 线程池中的线程的互斥锁
pthread_cond_t _cond; // 线程池中的线程的条件变量
private:
// 判断任务队列是否为空
bool is_empty()
{
return _task_queue.size() == 0;
}
// 上锁
void lock_queue()
{
pthread_mutex_lock(&_mutex);
}
// 解锁
void unlock_queue()
{
pthread_mutex_unlock(&_mutex);
}
// 让线程去指定条件变量处等待等待
void wait()
{
pthread_cond_wait(&_cond, &_mutex);
}
// 唤醒在指定条件变量处等待的线程
void wakeup()
{
pthread_cond_signal(&_cond);
}
public:
// 线程池的构造函数
thread_pool(int num = NUM)
: _thread_num(num)
{
pthread_mutex_init(&_mutex, nullptr); // 初始化互斥锁
pthread_cond_init(&_cond, nullptr); // 初始化条件变量
}
// 线程池中的线程所要执行的函数
static void *routine(void *arg)
{
pthread_detach(pthread_self());
thread_pool *self = (thread_pool *)arg;
// 不断从任务队列获取中任务,然后处理这些任务
while (true)
{
self->lock_queue(); // 获取任务前, 先对任务队列上锁
while (self->is_empty()) // 如果任务队列为空, 则让线程去休眠
self->wait();
T task; // 定义任务对象
self->pop(&task); // 用定义好的任务对象从任务队列中获取任务
self->unlock_queue(); // 获取任务后, 要对任务队列解锁
task.run(); // 处理获取到的任务
}
}
// 初始化线程池 (为线程池创建一批线程)
void thread_pool_init()
{
pthread_t tid;
for (int i = 0; i < _thread_num; i++)
pthread_create(&tid, nullptr, routine, this); // 传入 this 指针作为 routine 函数的参数
}
// 往任务队列放任务(主线程调用)
void push(const T &task)
{
lock_queue(); // 给任务队列上锁
_task_queue.push(task); // 往任务队列中放入任务
unlock_queue(); // 为任务队列解锁
wakeup(); // 来活了, 唤醒在线程池中休眠的线程
}
// 从任务队列获取任务(线程池中的线程调用)
void pop(T *task)
{
*task = _task_queue.front();
_task_queue.pop();
}
// 线程池的析构函数
~thread_pool()
{
pthread_mutex_destroy(&_mutex); // 销毁互斥锁
pthread_cond_destroy(&_cond); // 销毁条件变量
}
};
/* ---------- 主线程代码设计 ---------- */
int main()
{
srand((unsigned int)time(nullptr)); // 随机数种子
thread_pool<task> *tp = new thread_pool<task>; // 定义线程池对象
tp->thread_pool_init(); // 初始化线程池当中的线程
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); // 将构造的任务对象放入任务队列
cout << "主线程放入任务完毕, 放入的任务为: "
<< x << " " << op[index] << " " << y << " = ?" << endl;
}
return 0;
}
- 因为没有将屏幕这个临界资源也用锁保护起来,因此才会有一开始的主线程和新线程打印的内容混在一起的状况出现。
- 要解决这个问题,可以直接使用互斥锁将访问屏幕这个临界资源的临界区保护起来就行。