目录
一.线程池相关概念及其优点
线程池一种线程使用模式,线程过多会带来这个调度的开销进而影响整体的性能,而线程池是提前准备好了线程等待着管理者进行分配任务。
1.线程池的优点主要有一下几个方面:
- 线程重用: 线程的创建和销毁的开销是巨大的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了这么多消费内存的开销,其线程执行速度也是突飞猛进的提升。
- 控制线程的并发数:线程池,控制线程池的并发数可以有效的避免大量的线程池争夺CPU资源而造成堵塞。
- 可以对线程进行管理: 线程池可以提供定时、定期、单线程、并发数控制等功能。比如通过ScheduledThreadPool线程池来执行S秒后,每隔N秒执行一次的任务。
2.线程池的应用场景:
- 需要大量的线程来完成任务,并且完成任务所需的时间比较短
- 对性能要求苛刻的应用,比如说要求服务器迅速响应客户端的请求
- 接受突发性的大量请求但不至于让服务器因此产生大量线程。
二.线程池的实现
再这里实现的是一个简单的线程池,再这个线程池当中有一个任务队列,线程从任务队列当中提前任务,已经若干个线程,一开始再特点的条件变量下进行等待。任务队列是典型的生产者-消费者模型,本模型至少需要两个工具:一个 mutex + 一个条件变量,或是一个 mutex + 一个信号量。mutex 实际上就是锁,保证任务的添加和移除(获取)的互斥性,一个条件变量是保证获取 task 的同步性:一个 empty 的队列,线程应该等待(阻塞)。
- 线程池当中多个线程负责从任务队列当中拿任务,并处理拿到的任务
- 线程池对外提供Push方法让外部将任务放入到任务队列当中,好让线程从任务队列当中拿到数据
对应线程池的实现
#pragma once #include "Task.hpp" #include <iostream> #include <pthread.h> #include <queue> #define NUM 5 template <class T> class ThreadPool { private: void LockQueue() { pthread_mutex_lock(&_mtx); } void UnlockQueue() { pthread_mutex_unlock(&_mtx); } void Wait() { pthread_cond_wait(&_cond, &_mtx); } void WakeUp() { pthread_cond_signal(&_cond); } bool IsEmpty() { return _task_queue.size() == 0; } public: static void *Routine(void *arg)//为什么必须是static方法在博客中细说 { auto *self = (ThreadPool<T> *)arg; pthread_detach(pthread_self());//分离线程 while (true) { self->LockQueue(); while (self->IsEmpty())//看此时的任务队列当中是否有任务 { self->Wait(); } T task; self->pop(&task); self->UnlockQueue(); task.Run();//处理任务 } } void Push(const T &val) { LockQueue(); _task_queue.push(val); UnlockQueue(); WakeUp();//唤醒在条件变量下等待的一个线程 } void pop(T *out) { *out = _task_queue.front(); _task_queue.pop(); } void InitThreadPool() { //初始化线程池并创建线程 pthread_t tid; for (int i = 0; i < _num; i++) { pthread_create(&tid, NULL, Routine, this); } } //销毁信号量 ~ThreadPool() { pthread_mutex_destroy(&_mtx); pthread_cond_destroy(&_cond); } //获取单例 static ThreadPool<T> *GetInstance() { static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; if (NULL == tp) { pthread_mutex_lock(&mtx);//多个线程可能同时进来 if (NULL == tp) { tp = new ThreadPool<T>(); } pthread_mutex_unlock(&mtx); } return tp; } private: ThreadPool(int num = NUM) : _num(num) { pthread_mutex_init(&_mtx, NULL); pthread_cond_init(&_cond, NULL); } //禁掉拷贝构造和赋值 ThreadPool(const ThreadPool<T>&tp)=delete; ThreadPool&operator=(const ThreadPool<T>&tp)=delete; static ThreadPool<T> *tp; pthread_mutex_t _mtx; pthread_cond_t _cond; int _num;//线程的数量 std::queue<T> _task_queue;//任务队列 }; template <class T> ThreadPool<T> *ThreadPool<T>::tp = NULL;
代码解释:
在这里需要条件变量和互斥锁的原因是:多个线程可能同时访问任务队列,此时这个任务队列就变成了临界资源,锁以我们需要引入互斥锁来保护临界资源。线程池中的线程去这个任务队列当中拿任务的前提必须是任务队列当中要有任务,如果此时我们只使用互斥锁那么就有可能出现互斥锁一直被一个线程占用着,这是不合理的因此我们需要引入条件变量。当外部向任务队列当中放入任务之后,需要唤醒在条件变量下等待的线程。在这里特别需要注意的有一下几点:
- 在判断任务队列是否为空的情况下我们不能使用if进行判断。因为线程此时可能是被伪唤醒而此时任务队列是空的又或者是一些广播唤醒比如pthread_cond_broadcast把所有的线程都唤醒了。此时可能就会出现问题,所以我们需要使用while进行判断而不是if
- 当线程从任务队列当中拿到任务之后这个任务就属于这个线程了,所以任务的处理过程不应该放在临界区里面,而应该放在临界区的外部。处理任务是需要时间的,
- 在唤醒线程时建议使用pthread_cond_signal而不是pthread_cond_broadcast,后者会将在该条件变量下等待的所有线程全部唤醒,但是这些线程去任务队列当中拿任务但是只有一个线程能够拿到任务,一瞬间唤醒全部的线程可能会导致系统震荡也叫做惊群问题。
可能信心的老铁会发现为什么线程执行的方法是静态的?
我们在学习C++的时候我们知道非静态的成员函数默认会传递this指针,并且是第一个参数这就和我们线程库的参数形式冲突了所以我们需要将其设置为静态的函数。但是这也引发了另外一个问题线程在执行Routine函数时无法调用其他的成员函数,静态的成员函数是属于整个类的而不是属于某个对象,所以了我们需要将this作为参数显示的传进去这样我们就可以在这个静态的成员函数类调用其他函数了
任务类
在这里我们线程池处理的任务,我们将其封装成一个类,这个类提供一个Run方法用于执行任务在这里为了简单只是简单的设置成为一个计算任务类:
#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; };
此时线程池中的线程只需要从任务队列中拿到任务然后执行Run方法即可.下面我们来看一下主线程的执行逻辑:主线程主要负责通过线程池提供的Push方法将任务放入到任务队列当中即可
#include<iostream> #include<unistd.h> #include"ThreadPool.hpp" int main() { ThreadPool<Task>*tp=ThreadPool<Task>::GetInstance(); tp->InitThreadPool(); const char*op="+-*/%"; while(true) { sleep(1); int x=rand()%100; int y=rand()%100; int index=rand()%5; Task t(x,y,op[index]); tp->Push(t); } return 0; }
我们发现代码运行的一瞬间就出现了6个线程,其中一个是主线程另外5个是线程池里面的5个线程。到此线程池就结束了,如果线程池需要处理其他的任务时我们只需要更改任务类,并且在这个任务类当中提供一个Run方法即可。