Linux--线程池
目录
Linux线程池
线程池的概念
线程池的优点
线程池的应用场景
线程池的实现
Linux线程池
线程池的概念
线程池是一种线程的使用模式。
其存在的主要原因就为:线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。
线程池的优点
- 线程池避免了再处理短时间任务时创建于销毁线程的代价。
- 线程池不仅能够保证内核的充分利用,还能防止过分调度。
注意:线程池可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景
线程池常见的应用场景如下:
- 需要大量的线程来完成任务,且完成任务的时间比较短。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
相关现实场景应用:
- WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。
- 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
线程池的实现
下面我们用图示,表达一个简单的线程池,线程池中提供了一个任务队列,以及若干个线程(多线程)。
- 线程池中的多个线程负责从任务队列当中拿任务,并将拿到的任务进行处理。
- 线程池对外提供一个Push接口,用于让外部线程能够将任务Push到任务队列当中。
线程池的代码如下:
.hpp (ThreadPool.hpp)
#include <iostream>
#include <unistd.h>
#include <queue>
#include <stdlib.h>
#include <pthread.h>class ThreadInfo
{
public:pthread_t tid_;std::string name_;
};static const int defalutnum = 5;template <class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_);}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty(){return tasks_.empty();}std::string GetThreadName(pthread_t tid){for(const auto& ti:threads_){if(ti.tid_ == tid){return ti.name_;}}return "None";}
public:static void *HandlerTask(void *args) // 带 static 是因为类内的函数第一个参数是默认为 this 指针{ThreadPool<T> *tp = static_cast<ThreadPool*>(args);std::string name = tp->GetThreadName(pthread_self());while(true){tp->Lock();while(tp->IsQueueEmpty()){tp->ThreadSleep();}// 分配到任务T t = tp->Pop();std::cout<< 分配到了一个任务 : " << t << "," << name << "run" << std::endl;tp->Unlock();sleep(1);// 后续的任务处理部分// run();}}void Start(){int num = defalutnum;for(int i=0; i < num; i++){threads_[i].name_ = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid_), nullptr, HandlerTask, this);}}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup();Unlock();}
public:ThreadPool(int num = defalutnum) :threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}
private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;
};
知识回顾:为什么线程池中需要有互斥锁和条件变量?
因为线程池中的任务队列是会被多个执行流同时访问的临界资源,因此为了避免数据二义性,就会创建互斥锁与条件变量来进行保护。
线程池当中的线程要从任务队列里拿任务,前提条件是任务队列中必须要有任务,因此线程池当中的线程在拿任务之前,需要先判断任务队列当中是否有任务,若此时任务队列为空,那么该线程应该进行等待,直到任务队列中有任务时再将其唤醒,因此我们需要引入条件变量。
当外部线程向任务队列中Push一个任务后,此时可能有线程正处于等待状态,因此在新增任务后需要唤醒在条件变量下等待的线程。
注意:
- 当某线程被唤醒时,其可能是被异常或是伪唤醒,或者是一些广播类的唤醒线程操作而导致所有线程被唤醒,使得在被唤醒的若干线程中,只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足被唤醒条件,所以在判断任务队列是否为空时,应该使用while进行判断,而不是if。
- 这里的唤醒是用的pthread_cond_signal而不是pthread_cond_broadcast,那是因为,如果外部向队列中push了一个任务,但我们却使用pthread_cond_broadcast,将所有的等待线程全部唤醒,但实际情况却仅需要一个线程,那这肯定会资源的浪费。一瞬间唤醒大量的线程可能会导致系统震荡,这叫做惊群效应。所以在唤醒线程时最好还是使用pthread_cond_signal函数唤醒一个正在等待的线程即可。
- 当线程从任务队列中拿到任务后,该任务就已经属于当前线程了,与其他线程已经没有关系了。因此应该在解锁之后再进行处理任务,而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间,所以我们不要将其放到临界区当中。
- 还有就是设计的效率问题,如果我们将处理任务的操作也放在临界区来处理,那么当某一线程从任务队列中拿到任务后,其他线程还需要等待该线程将任务处理完后,才有机会进入临界区。这样的设计虽然也叫做线程池,但显然效率是远没有仅仅将拿取任务部分放在临界区的效率高。
为什么线程池中的线程执行例程需要设置为静态方法?
使用pthread_create函数创建线程时,需要为创建的线程传入一个HandlerTask(执行例程),该HandlerTask只有一个参数类型为void*的参数,以及返回类型为void*的返回值。
而此时HandlerTask作为类的成员函数,该函数的第一个参数是隐藏的this指针,因此这里的HandlerTask函数,虽然看起来只有一个参数,而实际上它有两个参数,此时直接将该HandlerTask函数作为创建线程时的执行例程是不行的,无法通过编译。
静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将HandlerTask设置为静态方法,此时HandlerTask函数才真正只有一个参数类型为void*的参数。
但是在静态成员函数内部无法调用非静态成员函数,而我们需要在HandlerTask函数当中调用该类的某些非静态成员函数,比如Pop。因此我们需要在创建线程时,向Routine函数传入的当前对象的this指针,此时我们就能够通过该this指针在HandlerTask函数内部调用非静态成员函数了。
主函数:.cpp (proc.cc)
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"int main()
{std::cout << "process running..." << std::endl;sleep(3);ThreadPool<int> *tp = new ThreadPool<int>();tp->Start();srand(time(nullptr) ^ getpid());while(true){//1. 构建任务int x = rand() % 10 + 1;tp->Push(x);//2. 交给线程池处理std::cout << "proc thread make task: " << std::endl;sleep(1);}
}
makefile
Threadpool:proc.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f Threadpool
运行效果如下: