【Hello Linux】线程池
作者:@小萌新
专栏:@Linux
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:简单介绍linux中线程池概念
线程池
- Linux线程池
-
- 线程池的概念
- 线程池的优点
- 线程池的应用场景
- 线程池实现
Linux线程池
线程池的概念
线程池是一种线程使用模式。
如果我们每次申请一个线程操作系统都要执行一次申请函数 分配空间的话不可避免的会造成效率的降低 所以说为了提升效率我们使用线程池维护多个线程 等待着监督管理者分配可并发执行的任务
线程池的优点
- 线程池避免了在处理短时间任务时创建与销毁线程的代价
- 线程池不仅能够保证内核充分利用 还能防止过分调度
线程池的应用场景
- 需要大量的线程来完成任务 且完成任务的时间比较短
- 对性能要求苛刻的应用 比如要求服务器迅速响应客户请求
- 接受突发性的大量请求 但不至于使服务器因此产生大量线程的应用
线程池实现
我们下面使用代码实现一个简单的线程池
- 线程池中的线程从任务队列中拿任务并且处理
- 线程池只暴露给外界一个push接口来添加任务
代码如下
#pragma once #include <iostream>
#include <unistd.h>
#include <queue>
#include <pthread.h>
using namespace std; const int NUM = 5; template<class T>
class ThreadPool
{ private: queue<T> _task_queue; int _thread_num; pthread_mutex_t _mutex; pthread_cond_t _cond; private: bool IsEmpty() { return _task_queue.size() == 0; } bool IsFull() { return _task_queue.size() == _thread_num; } 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); pthread_cond_init(&_cond); } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_mutex_destroy(&_mutex); } public: static void* Routine(void* arg) { pthread_detach(pthread_self()); auto* 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); } } void Push(const T& task) { LockQueue(); _task_queue.push(task); UnlockQueue(); WakeUp(); } void Pop(T& task) { task = _task_queue.front(); _task_queue.pop(); }
};
为什么要有互斥锁和变量
线程池中的任务队列是会被多个进程访问的临界资源 所以说我们需要引入互斥锁来实现互斥
线程池当中的线程要从任务队列里拿任务 前提条件是任务队列中必须要有任务
因此线程池当中的线程在拿任务之前 需要先判断任务队列当中是否有任务 若此时任务队列为空 那么该线程应该进行等待 直到任务队列中有任务时再将其唤醒 因此我们需要引入条件变量
当外部线程向任务队列中Push一个任务后 此时可能有线程正处于等待状态 因此在新增任务后需要唤醒在条件变量下等待的线程
当外部线程向任务队列中Push一个任务后 此时可能有线程正处于等待状态 因此在新增任务后需要唤醒在条件变量下等待的线程
注意:
- 当某线程被唤醒时 其可能是被异常或是伪唤醒 或者是一些广播类的唤醒线程操作而导致所有线程被唤醒 使得在被唤醒的若干线程中 只有个别线程能拿到任务 此时应该让被唤醒的线程再次判断是否满足被唤醒条件 所以在判断任务队列是否为空时 应该使用while进行判断 而不是if
pthread_cond_broadcast
函数的作用是唤醒条件变量下的所有线程 而外部可能只Push了一个任务 我们却把全部在等待的线程都唤醒了 此时这些线程就都会去任务队列获取任务 但最终只有一个线程能得到任务 所以说我们只需要使用pthread_cond_signal
函数唤醒一个进程就行- 当线程从任务队列中拿到任务后 该任务就已经属于当前线程了 与其他线程已经没有关系了 因此应该在解锁之后再进行处理任务 而不是在解锁之前进行 因为处理任务的过程可能会耗费一定的时间 所以我们不要将其放到临界区当中
- 如果将处理任务的过程放到临界区当中 那么当某一线程从任务队列拿到任务后 其他线程还需要等待该线程将任务处理完后 才有机会进入临界区 此时虽然是线程池 但最终我们可能并没有让多线程并行的执行起来
为什么Routine要设置为静态方法
使用pthread_create函数创建线程时 需要为创建的线程传入一个Routine(执行例程) 该Routine只有一个参数类型为void的参数以及返回类型为void的返回值
但是我们说过作为如果作为类的成员函数它是会有一个隐藏的参数 this指针 所以说如果我们不设置静态方法的话 就没有办法传递这两个参数 所以说要设置静态
那么如果我们不设置静态 使用全局函数呢?
如果我们使用全局函数的话 我们就没办法传递tihs指针进去 从而导致函数内部无法调用Pop函数等类内部函数
此外将线程运行函数暴露为全局函数的话也有被其他人调用的风险 所以综合考虑来说 我们应该使用static设置静态成员
任务类型的设计
们将线程池进行了模板化 因此线程池当中存储的任务类型可以是任意的 但无论该任务是什么类型的 在该任务类当中都必须包含一个Run方法 当我们处理该类型的任务时只需调用该Run方法即可
我们在下面实现一个计算机方法
#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;
};
主线程
主线程就负责不断向任务队列当中Push任务就行了 此后线程池当中的线程会从任务队列当中获取到这些任务并进行处理
#include "Task.hpp"
#include "ThreadPool.hpp"int main()
{srand((unsigned int)time(nullptr));ThreadPool<Task>* tp = new ThreadPool<Task>; //线程池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;
}
我们在运行代码之后便会出现下面的情况
此后我们如果想让线程池处理其他不同的任务请求时 我们只需要提供一个任务类 在该任务类当中提供对应的任务处理方法就行了